Skip to content

t1k:contribution-flush

FieldValue
Modulet1k-kit-feedback
Version1.2.2
Effortlow
Tools—

Keywords: contribution flush, contribution telemetry, flush contributions, record missed contributions

/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.

  • Manual: user asks to “flush contributions” / “record missed contribution scores”.
  • Auto-wired (recommended): t1k:triage invokes this at the END of a run, and t1k:my-score invokes it at the START (so lifetime totals reflect freshly-flushed items). The contribution-capture hook 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.

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:

Terminal window
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" && break
done

(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)”
Terminal window
node "<resolved-path>" reconcile

Asks 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:false with skipped:"gh-auth" → genuinely not authed; run gh auth login. Continue to Step 2 anyway (whatever capture DID record can still be flushed) and say so in the report.
  • reconciled:false with skipped:"gh-unreachable" → the GitHub API is down or rate-limiting you; your credentials are fine. Re-authenticating changes nothing — wait and re-run. The reason field says which (unreachable / rate-limited) and detail carries the raw gh stderr.
  • skipped:"gh-unavailable" → the failure did not match any known shape. The cause is unestablished — read detail before 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.

Terminal window
node "<resolved-path>" list

Output (JSON): { endpoint, pending: [ { ref_url, repo, kind, action, type, title, body_excerpt } ], count }.

  • endpoint:false → no telemetry endpoint configured → STOP, report No telemetry endpoint configured — nothing to flush.
  • count:0 → STOP, report No 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.)

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.

Terminal window
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 responseDispositionTracking file
201 / 200drop-recordedremoved + added to recorded ledger
403 triage_requires_closed_artifact / pr_not_mergedkeep (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 403dropremoved (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.

## 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 own t1k:issue / t1k:sync-back live POSTs and this flush can both fire safely — first wins.
  • Attribution — the script resolves the authed gh user (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 its mergedBy, an issue closed by a PR credits that PR’s mergedBy, 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.
  • gh must be authed — but read WHICH skip you got. record self-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) → read detail first. Until 2026-08-17 every failure reported gh-auth, so a GitHub 503 sent people to gh auth login for a problem login could not fix.
  • triage-backfill needs 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 every keep means 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’s t1k:my-score (lifetime totals). This skill only records this-batch scores.
  • Reconcile costs one gh api graphql per in-scope repo (~2s each, ~35s for the full T1K set) plus one gh repo list per 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.