A stateless, detection-only CLI that reads Claude Code transcript logs and produces a struggle leaderboard — the areas of a repo where Claude Code sessions show the deterministic signals of friction (rereads, failure streaks, oscillating edits, abandonment, etc.).
The default scan path is detection-only: it writes nothing, installs
nothing, persists nothing — it only prints a leaderboard to stdout. Cause
attribution is available as an opt-in via scan --diagnose (Slice 4);
synthesis, routing, and measurement are deferred to later slices.
Full manual: docs/CONSUMER_GUIDE.md — output formats, scoring modes, calibration, FAQ. Internals: docs/ARCHITECTURE.md.
npx harnessgap
Or build from source:
npm install
npm run build
node dist/cli.js scan
Requires Node >= 22.12.
harnessgap scan [options]
scan is the default command. It walks Claude Code transcripts under
~/.claude/projects/, filters to a repo, runs the detector, and prints a
leaderboard of struggle areas.
| Flag | Description |
|---|---|
--repo <path> |
Filter to sessions whose resolved main-repo root matches this path. The path itself is resolved to the project's main repo, so --repo <worktree> or --repo <subdir> matches the whole project (main checkout + all worktrees). Defaults to the main repo of the current working directory. |
--since <dur> |
Only sessions started within this lookback, e.g. 30d, 12h, 5m, 10s. Default: all sessions. |
--limit <n> |
Cap the number of sessions scanned. Useful for fast iteration. |
--json |
Emit the JSON envelope (for piping) instead of the human-readable leaderboard table. |
--calibrate |
Print per-signal distributions (min / p50 / p90 / max) plus active thresholds and scoring mode. Aggregate statistics only — no per-session detail. |
--bootstrap |
Force bootstrap (absolute-threshold) scoring mode instead of percentile. |
--diagnose |
Classify each flagged area into a typed cause (doc / config-doc / test-gap / refactor-flag / inherent-complexity) or unclassified, grounded by signal profile + doc-existence under docs_dirs. Opt-in; adds a CAUSE column to the table and a diagnoses field to --json. Default output is byte-identical to Slice 3 when off. Reads docs/ (local fs, path-confined, no symlinks). |
--config <path> |
Path to a .harnessgap.yml config file. Default: looks for .harnessgap.yml in the cwd. |
--claude-dir <path> |
Claude Code config directory (contains projects/). Default: ~/.claude. |
--version |
Print the harnessgap version and exit. |
--help |
Print help and exit. |
harnessgap reflect runs the detector on a single session (the one just
finished) and emits a ReflectFinding whose trip = flagged && !zero_edit
decides whether to prompt reflection. harnessgap init claude wires that into a
trip-gated Claude Code Stop hook so reflection happens automatically at session
end. The detection core is unchanged from Slice 1/2 — reflect reuses the same
detector + bootstrap mode.
harnessgap init claude
Installs three artifacts under <cwd>/.claude/ (idempotent — re-run to refresh):
- a fail-open Stop-hook wrapper — on every stop it runs
harnessgap reflect --transcript <just-finished> --format hook-stop; any fault short-circuits to{}so the hook never blocks on a harnessgap error. - an idempotent
settings.jsonmerge — appends the harnessgap command tohooks.Stopexactly once, preserving your existing hooks and keys. - the
/reflectcommand — guides filling oneReflectFrame(cost → missing context → one suggested change with a path-checkedtarget_path).
The hook blocks the stop only when trip is true, returning
{ "decision": "block", "reason": … } (the reason carries the finding summary
— top friction areas + active signals; no transcript prose). Otherwise it returns
{} and the session ends normally. stop_hook_active guards against loops. The
agent presents the recommendation in-session and the user acts — nothing is
auto-written to the repo.
| Flag | Description |
|---|---|
--transcript <path> |
Reflect on one given transcript file (the per-stop hook path). |
--latest |
Reflect on the most-recent finished session for --repo (the manual /reflect path). |
--repo <path> |
Target repo toplevel, used with --latest (resolved to the project's main repo, like scan). |
--exclude-session <id> |
Exclude a session id, used with --latest. |
--stop-hook-active |
Mark the Claude Code Stop hook as already active (short-circuit to allow). |
--format <json|hook-stop> |
Output form: the json ReflectFinding (default) or the Stop hook payload. |
--config <path> |
Path to a .harnessgap.yml config file. |
--claude-dir <path> |
Claude Code config directory (contains projects/). Default: ~/.claude. |
trip = flagged && !zero_edit reuses the bootstrap flag — calibrated for the
batch leaderboard, not a per-session interruption. At n=1 it may fire on
ordinary sessions (e.g. a single debug loop hitting reread + wall_clock).
Trip-gate sensitivity is the top open question for this slice: measure how often
the hook fires on clean sessions, and if too often, tighten it (drop the
≥ 2 signals disjunction, require ≥ k, or add a detector.reflect block). The
ReflectFrame recommendation is advisory and human-reviewed; path_verified is
self-attested.
harnessgap scan --diagnose adds grounded cause attribution on top of the
leaderboard. For each flagged area it picks one cause from a closed taxonomy,
grounded in (a) the area's signal profile, (b) whether a doc for the area
already exists under docs_dirs, and (c) an opt-in evidence projection (failed
exec counts by cmd-class + edited-file counts by file-class). It is a pure rule
engine — no LLM, no network, no git.
| Cause | Plain meaning | Grounding |
|---|---|---|
doc |
Missing or undiscoverable doc — the agent explores and re-reads the same files. | explore_ratio + reread elevated and no doc found under docs_dirs. |
config-doc |
Setup/config friction — failures concentrate on config-class commands. | failure_streak elevated and config-failures share ≥ config_share_floor. |
test-gap |
Missing or weak tests — the agent rewrites tests as behavior keeps failing, with no user corrections. | oscillation + failure_streak elevated, test-file edit share ≥ test_share_floor, corrections not elevated. |
refactor-flag |
Code-structure problem — the agent is corrected while editing code; an existing doc makes this stronger. | oscillation + corrections elevated, code-file edit share ≥ code_share_floor; doc-existence boosts confidence. |
inherent-complexity |
Genuinely hard — expensive per line, no specific signature fit. | wall_clock_per_line elevated and mean score ≥ score_floor. |
unclassified |
Nothing decisive. | Best cause below confidence_floor, and the inherent-complexity residual did not fire. |
Opt-in and byte-identical when off: the CAUSE column and the --json
diagnoses field appear only under --diagnose. Evidence collection runs only
under --diagnose; with it off, StruggleRecord.evidence is absent and default
output matches Slice 3 exactly.
- Fuzzy doc-match. A doc matches an area if the area's leaf segment (e.g.
billing) appears as a path segment or filename stem under anydocs_dirsentry. Token match will miss differently-named docs and over-match common tokens (utils,common). - Session→area evidence attribution. A session's evidence buckets map to
every area the session touches; v1 does not attribute files to specific areas.
This blurs
config-doc/test-gap/refactor-flagon sessions touching many areas. - No git churn. Code-stability is grounded by signals + doc-existence, not churn (deferred).
- Calibration is dogfood, not a promise. The five floors
(
confidence_floor, the three share floors,score_floor) are v1 priors. They must be pinned on a real repo before trusting individual causes — see issue #15.
See docs/CONSUMER_GUIDE.md "Diagnoser" for how to read the CAUSE column, and docs/ARCHITECTURE.md "Diagnoser" for the pipeline seam + fail-open + privacy contract.
Optional. scan runs with built-in defaults if no file is present. The file is
a YAML object with four top-level keys — detector, areas, docs_dirs, and
diagnose. Anything else is rejected. Deep-merged over the defaults (arrays
replace, they do not concatenate).
detector:
thresholds_as: percentile # percentile (default) | absolute
flag_pct: 90 # percentile mode: flag the top (100 - flag_pct)% of composites
bootstrap_session_floor: 30 # below this session count, bootstrap mode is automatic
bootstrap_flag_pct: 70 # bootstrap mode: flag if composite >= this OR >= 2 signals trip
reread_threshold: 5 # a file read count at/above this counts as a reread
correction_window_ms: 120000 # window after an edit in which a follow-up edit counts as a correction
signal_weights: # composite weights per signal
explore_ratio: 1.0
reread: 1.0
failure_streak: 1.0
corrections: 1.0
abandonment: 0.5 # low until task fingerprinting lands
oscillation: 1.2
wall_clock_per_line: 1.0
bootstrap_thresholds: # conservative absolute priors used in bootstrap mode
explore_ratio: 10
reread: 5
failure_streak: 3
corrections: 2
abandonment: true
oscillation: 2
wall_clock_per_line_ms: 300000
ambient: # repo-level elevated-baseline assessment (always printed)
breadth_floor: 4 # orientation path fires when median pre-edit dir-breadth >= this
file_depth_floor: 12 # ...or when median pre-edit file-depth >= this
struggle_rate_threshold: 0.30 # acute path fires when bootstrap struggle rate >= this
min_sessions: 10 # below this session count, baseline is "too few sessions"
severity_min_sessions: 20 # below this, an elevated finding is severity "unrated"
areas:
ignore: [node_modules, build, target, dist, .git, .next, vendor] # path prefixes excluded from area clustering
min_weight: 0.40 # minimum cumulative touch weight for an area to appear
min_depth: 2 # minimum path depth for an area key
touch_weights: { edit: 3, read: 2, exec: 1 } # weight of each touch kind
tail_fraction: 0.25 # fraction of a session's events that defines the abandonment tail
explore_ratio_min: 0.8 # abandonment explore-ratio gate
suppress_abandonment_when_no_exec: true
test_cmd_patterns: [test, spec, pytest, "npm test", "npm run test", make, "cargo test", "go test", jest, vitest]
docs_dirs: [docs] # repo-relative dirs searched for doc-existence grounding under --diagnose
diagnose: # cause-rule floors (Slice 4); v1 priors, calibrate via dogfood (issue #15)
confidence_floor: 0.5 # min score for a specific cause to win
config_share_floor: 0.5 # config-failures / total-failures bar for config-doc
test_share_floor: 0.5 # test-file-edits / total-edits bar for test-gap
code_share_floor: 0.5 # code-file-edits / total-edits bar for refactor-flag
score_floor: 70 # mean-score bar for the inherent-complexity residualKeys not shown here (synthesizer, router, tasks, repo) are not
shipped and will be rejected.
The detection leaderboard is validated by a manual dogfood gate, not an automated test. On a real repo with rich Claude Code session history, the user prepares, in advance:
- >= 5 areas they recall as struggle, and
- >= 5 areas they recall as non-struggle.
harnessgap scan must then satisfy all three against its leaderboard:
- Precision — of the tool's top 5 flagged areas, >= 3 are in the user's struggle set (>= 60%).
- Recall — of the user's >= 5 struggle areas, >= 3 are flagged (>= 60%).
- No false positives in the top 5 — none of the user's non-struggle areas appear in the top 5 flagged.
The labeled fixture corpus and snapshot test (see test/corpus.test.ts,
test/snapshot.test.ts) serve as the automated regression proxy for this gate.
harnessgap is built to run offline on private transcripts. Five guarantees:
- No network. No
fetch/http/https/net/undiciimports and nofetch()calls anywhere insrc/. Transcripts never leave the machine. Enforced bytest/egress.test.ts, which scans everysrc/**/*.tsfile for forbidden network imports and fetch calls, and runs in CI. - No disk writes (detection path).
scanandreflectwrite nothing to disk — they read transcripts and print to stdout. (harnessgap init claudeis the one exception: an explicit opt-in installer that writes the Stop-hook wrapper, asettings.jsonmerge, and the/reflectcommand under.claude/.) (OS-level page cache/swap are out of scope and common to any process that reads files.) - Pattern-catalog scrubbing. Secrets are scrubbed in the adapter, before events enter the pipeline, using a fixed pattern catalog (API keys, bearer tokens, private keys, connection strings, etc.).
- No raw prose in output. Only derived signal values and integer warning
counts are emitted. Raw message text, commands, and transcript line content
never appear in any output path (human table,
--json,--calibrate, warnings). - Stat-based repo resolution (no git invocation). The repo for each
session is found by walking up from its cwd and
stat-ing<ancestor>/.git— nogitprocess is spawned at all, so nothing lands in shell history. Worktree checkouts (.gitfile) resolve up to the main repo (.gitdirectory), so a project's main checkout and all worktrees aggregate together, and sessions whose cwd was a since-deleted worktree are recovered. Sibling worktrees (a checkout beside, not nested under, the main repo) are recovered by scanning candidate siblings'.git/worktrees/<name>/gitdirregistrations — still onlystat/readdir/ tiny text-file reads, with no naming-convention heuristic. The recovered checkout root relativizes those sessions' file paths, so sibling-worktree sessions aggregate under the same areas as the main checkout. Symlinks in transcript directories are rejected.
Runtime dependencies are exactly two, both no-egress (neither performs network I/O):
Dev dependencies (typescript, vitest, tsx, @types/node) are never shipped
and excluded from the published files set (["dist"]).
The audit is locked by test/packaging.test.ts, which runs
npm ls --all --omit=dev --json (via execFile, no shell) and asserts the
runtime dependencies object has exactly the keys commander and yaml. The
test/egress.test.ts gate additionally asserts no src/ file imports a network
module or calls fetch(). Both run in CI.
Full design: docs/superpowers/specs/archive/2026-07-12-harnessgap-detection-slice-design.md.
Diagnoser (Slice 4): docs/superpowers/specs/active/2026-07-18-harnessgap-diagnoser-design.md.