t1k:contribution-flush
| Field | Value |
|---|---|
| Module | t1k-kit-feedback |
| Version | 1.2.2 |
| Effort | low |
| Tools | — |
Keywords: contribution flush, contribution telemetry, flush contributions, record missed contributions
How to invoke
Section titled “How to invoke”/t1k:contribution-flush(no args)t1k:contribution-flush — Flush Missed Contribution Telemetry
Section titled “t1k:contribution-flush — Flush Missed Contribution Telemetry”The AI half of the deterministic-capture pipeline. The contribution-capture.cjs
PostToolUse:Bash hook deterministically records every gh issue|pr create|merge|close
against a T1K repo to a tracking file (<claudeDir>/telemetry/contribution-tracking.jsonl).
This skill reads that file, AI-scores each un-recorded artifact against the
shared rubric, and POSTs the scores to the telemetry worker — closing the gap when
the originating skill (t1k:issue / t1k:sync-back / t1k:triage) skipped its
live t1k:contribution-score POST.
A hook cannot judge quality; this skill can. That split is the whole design: the hook guarantees capture, this skill supplies the score.
Flush means “recover what actually happened”, not “recover what capture recorded”
(#745). Capture is event-sourced from Bash, so an artifact whose evidence never
reached a Bash call — output redirected to /dev/null inside a loop, a server-side
gh pr merge --auto, an issue auto-closed by a Fixes #N — is invisible to it, and
reading only the tracking file inherits every one of those blind spots. Step 1
reconciles against GitHub first, so the pending list reflects outcome rather than
observed intent.
When to use
Section titled “When to use”- Manual: user asks to “flush contributions” / “record missed contribution scores”.
- Auto-wired (recommended):
t1k:triageinvokes this at the END of a run, andt1k:my-scoreinvokes it at the START (so lifetime totals reflect freshly-flushed items). Thecontribution-capturehook also nudges (N contributions pending — run /t1k:contribution-flush) once the tracking file reaches its threshold.
This skill is fire-and-forget for callers: any failure is logged and never blocks the caller’s primary workflow.
Workflow
Section titled “Workflow”Step 0 — Resolve the script path (BOTH install scopes)
Section titled “Step 0 — Resolve the script path (BOTH install scopes)”t1k-maintainer installs to whichever scope the user chose — module.json
lists contribution-flush.cjs under files.scripts and does not mandate
project-local. On a global-scope install the script lands in ~/.claude/scripts/
and the project’s .claude/scripts/ never receives it, so a hardcoded
$CLAUDE_PROJECT_DIR/... path fails with MODULE_NOT_FOUND and the whole flush
is unrunnable. Resolve it, do not assume it — and test each candidate for
runnability, not mere existence: a stale project-local copy left over from
before core moved to global scope has no requires that resolve, so
[ -f "$c" ] alone cannot tell it apart from a healthy one and it wins the race
over a working global install sitting right beside it:
for c in "$CLAUDE_PROJECT_DIR/.claude/scripts/contribution-flush.cjs" \ ".claude/scripts/contribution-flush.cjs" \ "$HOME/.claude/scripts/contribution-flush.cjs"; do [ -f "$c" ] && node -e "require('$c')" 2>/dev/null && echo "$c" && breakdone(require(), not execution — the script guards its CLI entrypoint behind
require.main === module, which a node -e load never satisfies, so this only
proves the module’s own requires resolve. If every candidate exists but none
is runnable, the loop prints nothing — the existing “empty output → STOP”
handling below already covers that.)
Use the printed path verbatim in Steps 1, 2 and 4 (each Bash call is a fresh
shell, so a variable does not survive between steps — substitute the literal path).
Empty output → STOP and report
contribution-flush.cjs not found at project or global scope — is t1k-maintainer installed?
The script’s own resolveClaudeDir() still resolves the project telemetry dir
correctly when launched from the project cwd, so the global copy operates on the
right tracking file. Only the launcher path needs resolving.
Step 1 — Reconcile against GitHub (#745)
Section titled “Step 1 — Reconcile against GitHub (#745)”node "<resolved-path>" reconcileAsks GitHub what the authed user actually merged/closed in the in-scope repos over
the last 7 days, subtracts the tracking file and the recorded ledger, and enqueues
the remainder. Output (JSON): { reconciled, user, days, reposScanned, candidates, added, alreadyKnown, deferred, entries, truncatedRepos, errors }.
reconciled:falsewithskipped:"gh-auth"→ genuinely not authed; rungh auth login. Continue to Step 2 anyway (whatever capture DID record can still be flushed) and say so in the report.reconciled:falsewithskipped:"gh-unreachable"→ the GitHub API is down or rate-limiting you; your credentials are fine. Re-authenticating changes nothing — wait and re-run. Thereasonfield says which (unreachable/rate-limited) anddetailcarries the rawghstderr.skipped:"gh-unavailable"→ the failure did not match any known shape. The cause is unestablished — readdetailbefore acting on it, and do not assume auth.added:0→ nothing was missed; that is the healthy state, not an error.deferred > 0→ the per-run cap was hit; the remainder is picked up by the next reconcile. Mention it in the report.errors[]/truncatedRepos[]→ report them; they mean part of the sweep is unverified, which is exactly what silent under-counting looked like.
Options: --days N (1–30), --repo owner/name (repeatable; skips repo
enumeration), --dry-run (report only, no enqueue).
This step writes. It is the only part of flush that does, before record.
Attribution is filtered on mergedBy / the closing actor, never “everything closed
today” — an artifact someone else merged is not the operator’s credit.
Step 2 — List pending
Section titled “Step 2 — List pending”node "<resolved-path>" listOutput (JSON): { endpoint, pending: [ { ref_url, repo, kind, action, type, title, body_excerpt } ], count }.
endpoint:false→ no telemetry endpoint configured → STOP, reportNo telemetry endpoint configured — nothing to flush.count:0→ STOP, reportNo pending contributions to flush.(After a clean Step 1 this now means the ledger genuinely matches GitHub, not merely that capture saw nothing.)
(The script self-skips and prunes out-of-bounds entries — anything past
MAX_ATTEMPTS retries or older than MAX_AGE_DAYS is dropped, not listed.)
Step 3 — AI-score each pending entry
Section titled “Step 3 — AI-score each pending entry”Apply the shared 1–5 rubric (SSOT in t1k:contribution-score — do NOT redefine
it here) to each pending entry’s title + body_excerpt. Map by action:
action:"merge"|"close"→ triage credit (type:"triage-backfill"): score the artifact’s quality as merged/closed work.action:"create"→ authored credit (type:"issue"or"sync-back-pr"): score the artifact as filed.
Be conservative — when between two tiers, pick the lower one.
Build the scored array (one object per pending entry you chose to score):
[ { "ref_url": "https://github.com/<owner>/<repo>/(issues|pull)/<n>", "ai_score": 1-5, "ai_rationale": "<=300 chars" } ]Write it to a temp file, e.g. <claudeDir>/telemetry/.flush-scores.json.
Step 4 — Record
Section titled “Step 4 — Record”node "<resolved-path>" record --input <tmpfile>Output (JSON): { recorded, kept, dropped, throttled, retryAfterSeconds, results: [ { ref_url, disposition, reason, status, retry_after_seconds } ] }.
reason / status are present on every POSTed row; retry_after_seconds only
when the worker advertised one.
The script joins each score back to its tracking entry, POSTs via the SSOT body shape, and applies disposition:
| Worker response | Disposition | Tracking file |
|---|---|---|
| 201 / 200 | drop-recorded | removed + added to recorded ledger |
403 triage_requires_closed_artifact / pr_not_merged | keep (lifecycle-not-yet) | retained, attempts++ (bounded by MAX_ATTEMPTS/MAX_AGE_DAYS) |
| 429 (worker’s per-window POST cap) | keep (throttled) | retained, attempts NOT bumped |
| 0 / 5xx (transient network/server error) | keep (transient) | retained, attempts NOT bumped |
| 400 / other 403 | drop | removed (terminal) |
Three different causes share the keep disposition, so read reason and
status, never disposition alone — keep: pr_not_merged attempts=2 (403,
lifecycle-not-yet) and keep: transient(429) retry_after=1800s are different
situations with different operator actions. The 403 reasons are SSOT’d as
NOT_YET_REASONS in hooks/lib/contribution-telemetry.cjs. Throttling is also
summarised at the top level as throttled / retryAfterSeconds.
Before #890 the row carried only {ref_url, disposition}, and the guidance here
was to infer a throttle from a contiguous tail of keeps. That heuristic is
obsolete — the status is now reported directly. Prefer it.
Delete the temp scores file afterward.
Step 5 — Report
Section titled “Step 5 — Report”## Contribution Flush
**Reconciled in:** {added} (of {candidates} found on GitHub)**Recorded:** {recorded} **Kept (awaiting merge / transient):** {kept} **Dropped:** {dropped}
| Ref | Type | Score | Disposition | Reason ||-----|------|-------|-------------|--------|| ... one row per results[] entry, `reason` verbatim ... |If recorded == 0 && kept == 0 && dropped == 0: print Nothing to flush.
If throttled > 0, say so explicitly — {throttled} throttled by the worker's per-window cap, retry in {retryAfterSeconds}s — instead of reporting those rows
as “not yet mergeable”. Never report a bare keep count with no reason
breakdown: that is the exact unactionable output #890 fixed.
- Idempotent — the worker dedups on
(user, ref_url); re-flushing a recorded ref returns 200 and is dropped. The skill’s ownt1k:issue/t1k:sync-backlive POSTs and this flush can both fire safely — first wins. - Attribution — the script resolves the authed
ghuser (the kit operator), NOT the GitHub artifact author, so triage/merge credit lands on the operator. Reconcile applies the same rule from the other direction: a PR credits itsmergedBy, an issue closed by a PR credits that PR’smergedBy, and an issue closed by hand credits the closing actor. Work another contributor merged or closed is never swept in, including a merge the operator attempted and lost. - Scoring is the skill’s job — the rubric SSOT is
t1k:contribution-score. This skill orchestrates; it must not inline the POST body shape (the script owns that) nor redefine the rubric.
Gotchas
Section titled “Gotchas”ghmust be authed — but read WHICH skip you got.recordself-skips on three distinct causes, and only one of them is fixed by logging in:gh-auth(no token, or credentials rejected) →gh auth login;gh-unreachable(5xx / 429 / DNS / timeout) → wait and retry, auth is fine;gh-unavailable(unrecognised) → readdetailfirst. Until 2026-08-17 every failure reportedgh-auth, so a GitHub 503 sent people togh auth loginfor a problem login could not fix.triage-backfillneeds a CLOSED/MERGED artifact — open auto-armed PRs return 403 and are KEPT for the next flush; they record once they merge. This is expected, not an error. Not everykeepmeans this — a 429 window-cap or a transient 0/5xx also KEEPS and looks identical in the report; check for a contiguous tail (Step 4) before assuming lifecycle gating.- Do NOT call
/api/contributors/me— that’st1k:my-score(lifetime totals). This skill only records this-batch scores. - Reconcile costs one
gh api graphqlper in-scope repo (~2s each, ~35s for the full T1K set) plus onegh repo listper owner. It is a flush-time step by design — never move it into the capture hook, which runs after EVERY Bash call. - A reconcile that adds a lot on its first run is expected, not a bug: it is recovering artifacts the hook structurally could not see. The per-run cap defers the overflow to the next run rather than dropping it.