plumbbob 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +87 -86
  3. package/dist/cli-core.js +8 -8
  4. package/dist/cli.js +1 -1
  5. package/dist/lib/agents.js +26 -26
  6. package/dist/lib/buildlog.js +11 -11
  7. package/dist/lib/buildlogsync.js +8 -8
  8. package/dist/lib/check.js +14 -14
  9. package/dist/lib/commitbody.js +72 -0
  10. package/dist/lib/commitmsg.js +7 -7
  11. package/dist/lib/git.js +44 -15
  12. package/dist/lib/intent.js +20 -19
  13. package/dist/lib/latch.js +16 -16
  14. package/dist/lib/orient.js +36 -17
  15. package/dist/lib/reentry.js +3 -3
  16. package/dist/lib/settings.js +16 -16
  17. package/dist/lib/sidecar.js +54 -54
  18. package/dist/lib/templates.js +1 -1
  19. package/dist/verbs/agent.js +25 -25
  20. package/dist/verbs/build.js +9 -9
  21. package/dist/verbs/check.js +6 -6
  22. package/dist/verbs/checkpoint.js +48 -47
  23. package/dist/verbs/doctor.js +27 -26
  24. package/dist/verbs/finish.js +27 -40
  25. package/dist/verbs/handoff.js +9 -8
  26. package/dist/verbs/init.js +9 -9
  27. package/dist/verbs/park.js +3 -3
  28. package/dist/verbs/recover.js +11 -11
  29. package/dist/verbs/revert.js +12 -12
  30. package/dist/verbs/spike.js +17 -17
  31. package/dist/verbs/start.js +49 -19
  32. package/dist/verbs/status.js +8 -8
  33. package/dist/verbs/turn.js +16 -16
  34. package/dist/verbs/use.js +4 -4
  35. package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
  36. package/node_modules/checkride/CHANGELOG.md +401 -0
  37. package/node_modules/checkride/README.md +27 -9
  38. package/node_modules/checkride/dist/adapters.d.ts +12 -4
  39. package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
  40. package/node_modules/checkride/dist/adapters.js +21 -1
  41. package/node_modules/checkride/dist/adapters.js.map +1 -1
  42. package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
  43. package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
  44. package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
  45. package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
  46. package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
  47. package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
  48. package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
  49. package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
  50. package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
  51. package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
  52. package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
  53. package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
  54. package/node_modules/checkride/dist/baseline/fallow.js +2 -2
  55. package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
  56. package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
  57. package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
  58. package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
  59. package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
  60. package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
  61. package/node_modules/checkride/dist/baseline/history.js +232 -0
  62. package/node_modules/checkride/dist/baseline/history.js.map +1 -0
  63. package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
  64. package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
  65. package/node_modules/checkride/dist/baseline/index.js +2 -1
  66. package/node_modules/checkride/dist/baseline/index.js.map +1 -1
  67. package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
  68. package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
  69. package/node_modules/checkride/dist/baseline/store.js +50 -16
  70. package/node_modules/checkride/dist/baseline/store.js.map +1 -1
  71. package/node_modules/checkride/dist/cli.d.ts +5 -1
  72. package/node_modules/checkride/dist/cli.d.ts.map +1 -1
  73. package/node_modules/checkride/dist/cli.js +98 -25
  74. package/node_modules/checkride/dist/cli.js.map +1 -1
  75. package/node_modules/checkride/dist/config.d.ts +33 -4
  76. package/node_modules/checkride/dist/config.d.ts.map +1 -1
  77. package/node_modules/checkride/dist/config.js +21 -6
  78. package/node_modules/checkride/dist/config.js.map +1 -1
  79. package/node_modules/checkride/dist/digest/digest.js +1 -1
  80. package/node_modules/checkride/dist/doctor.d.ts +2 -0
  81. package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
  82. package/node_modules/checkride/dist/doctor.js +39 -1
  83. package/node_modules/checkride/dist/doctor.js.map +1 -1
  84. package/node_modules/checkride/dist/gate.d.ts +49 -4
  85. package/node_modules/checkride/dist/gate.d.ts.map +1 -1
  86. package/node_modules/checkride/dist/gate.js +143 -16
  87. package/node_modules/checkride/dist/gate.js.map +1 -1
  88. package/node_modules/checkride/dist/init.d.ts +8 -1
  89. package/node_modules/checkride/dist/init.d.ts.map +1 -1
  90. package/node_modules/checkride/dist/init.js +68 -7
  91. package/node_modules/checkride/dist/init.js.map +1 -1
  92. package/node_modules/checkride/dist/node-pin.d.ts +1 -1
  93. package/node_modules/checkride/dist/node-pin.js +1 -1
  94. package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
  95. package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
  96. package/node_modules/checkride/dist/orchestrator.js +118 -13
  97. package/node_modules/checkride/dist/orchestrator.js.map +1 -1
  98. package/node_modules/checkride/dist/pm/index.d.ts +1 -0
  99. package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
  100. package/node_modules/checkride/dist/pm/index.js.map +1 -1
  101. package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
  102. package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
  103. package/node_modules/checkride/dist/pm/launch.js +16 -0
  104. package/node_modules/checkride/dist/pm/launch.js.map +1 -1
  105. package/node_modules/checkride/dist/pm/translate.js +1 -1
  106. package/node_modules/checkride/dist/pm/translate.js.map +1 -1
  107. package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
  108. package/node_modules/checkride/dist/qa/mutation.js +4 -4
  109. package/node_modules/checkride/dist/recover.d.ts +48 -0
  110. package/node_modules/checkride/dist/recover.d.ts.map +1 -0
  111. package/node_modules/checkride/dist/recover.js +220 -0
  112. package/node_modules/checkride/dist/recover.js.map +1 -0
  113. package/node_modules/checkride/dist/smoke.d.ts +1 -1
  114. package/node_modules/checkride/dist/smoke.js +1 -1
  115. package/node_modules/checkride/dist/snippets.d.ts +1 -1
  116. package/node_modules/checkride/package.json +3 -2
  117. package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
  118. package/node_modules/checkride/skills/check/SKILL.md +9 -1
  119. package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
  120. package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
  121. package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
  122. package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
  123. package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
  124. package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
  125. package/node_modules/checkride/templates/shared/vale.ini +34 -0
  126. package/package.json +10 -9
  127. package/skills/build/SKILL.md +69 -64
  128. package/skills/doctor/SKILL.md +11 -11
  129. package/skills/finish/SKILL.md +20 -19
  130. package/skills/harvest/SKILL.md +13 -12
  131. package/skills/park/SKILL.md +10 -10
  132. package/skills/plan/SKILL.md +63 -54
  133. package/skills/recover/SKILL.md +6 -6
  134. package/skills/refine/SKILL.md +18 -16
  135. package/skills/revert/SKILL.md +6 -6
  136. package/skills/spike/SKILL.md +9 -9
  137. package/skills/status/SKILL.md +5 -5
  138. package/skills/step/SKILL.md +17 -12
  139. package/skills/verify/SKILL.md +45 -40
  140. package/templates/build-log.md +7 -7
  141. package/templates/intent.md +49 -39
  142. package/templates/spike-report.md +10 -10
@@ -1,16 +1,16 @@
1
- // `plumbbob turn` — the turn-ledger hook. Wired to the harness's
1
+ // `plumbbob turn`: the turn-ledger hook. Wired to the harness's
2
2
  // UserPromptSubmit event, it runs once per human prompt and maintains two of the
3
3
  // sidecar's git-excluded control files (flat files under `.plumbbob/`): it ticks
4
4
  // `TURN`, the monotonic count of human turns the checkpoint latch compares
5
5
  // against a step's entry stamp to know a human turn intervened since the step
6
- // began, and it rewrites `GRANT` from the literal prompt — minting a one-turn
6
+ // began, and it rewrites `GRANT` from the literal prompt, minting a one-turn
7
7
  // self-approval only when the human typed `/build --auto` or an `N-M` step
8
8
  // range, clearing it otherwise (those strings can only come from a human
9
- // keystroke, because `build` is disable-model-invocation — a grant the model
9
+ // keystroke, because `build` is disable-model-invocation: a grant the model
10
10
  // could forge would be no grant). When a step is in flight it also emits ONE
11
11
  // `UserPromptSubmit` additionalContext line so a fresh session (post-compaction,
12
12
  // or a scripted `-p` turn where no skill prose is in context) still knows the
13
- // beat — a tangent is parked, not chased. Guidance only: it blocks nothing. It
13
+ // beat: a tangent is parked, not chased. Guidance only: it blocks nothing. It
14
14
  // ALWAYS exits 0 and any failure degrades to no output, so neither the tick nor
15
15
  // the emit can ever wedge prompting.
16
16
  import { readFileSync, writeFileSync } from 'node:fs';
@@ -18,7 +18,7 @@ import { findSessionRoot, setGrant, stepPath, turnPath } from "../lib/sidecar.js
18
18
  /**
19
19
  * The hook entry point: apply the tick, then emit the in-flight-step nudge.
20
20
  *
21
- * Always returns 0 — a broken turn must never wedge a prompt, so any failure is
21
+ * Always returns 0: a broken turn must never wedge a prompt, so any failure is
22
22
  * swallowed and degrades to no output.
23
23
  */
24
24
  export function turn(cwd, _args) {
@@ -29,7 +29,7 @@ export function turn(cwd, _args) {
29
29
  process.stdout.write(context);
30
30
  }
31
31
  catch {
32
- // A broken turn must never wedge a prompt — swallow and exit 0.
32
+ // A broken turn must never wedge a prompt: swallow and exit 0.
33
33
  }
34
34
  return 0;
35
35
  }
@@ -59,7 +59,7 @@ export function stepInFlightContext(cwd) {
59
59
  * The in-flight step number, or null when no STEP marker is present.
60
60
  *
61
61
  * Absent means no step entered, or it already checkpointed. A garbled marker
62
- * reads as null — never an error, because the hook must not wedge a prompt.
62
+ * reads as null (never an error), because the hook must not wedge a prompt.
63
63
  */
64
64
  function inFlightStep(root) {
65
65
  try {
@@ -73,14 +73,14 @@ function inFlightStep(root) {
73
73
  /**
74
74
  * The testable core: apply one tick given the raw hook stdin.
75
75
  *
76
- * Split from the fd-0 read so the unit tests can drive it directly — an
76
+ * Split from the fd-0 read so the unit tests can drive it directly: an
77
77
  * in-process test cannot feed fd 0 (the same constraint checkpoint's `--body`
78
78
  * lives under).
79
79
  */
80
80
  export function applyTurn(cwd, raw) {
81
81
  const root = findSessionRoot(cwd);
82
82
  if (root === null)
83
- return 0; // no active session above cwd — silent no-op.
83
+ return 0; // no active session above cwd: silent no-op.
84
84
  writeFileSync(turnPath(root), `${readTurn(root) + 1}\n`);
85
85
  setGrant(root, grantFromPrompt(extractPrompt(raw)));
86
86
  return 0;
@@ -90,7 +90,7 @@ export function applyTurn(cwd, raw) {
90
90
  *
91
91
  * A grant is minted only from a literal `build` invocation the human typed:
92
92
  * `build` is disable-model-invocation, so this string can only come from a
93
- * human keystroke, never model-authored pressure — a grant the model can forge
93
+ * human keystroke, never model-authored pressure: a grant the model can forge
94
94
  * is no grant. A bounded range beats `--auto` (bounded wins); a bare
95
95
  * `/build`, or any non-invocation prompt, mints nothing. The namespaced
96
96
  * `/plumbbob:build` form is honored alongside the bare one.
@@ -101,10 +101,10 @@ export function applyTurn(cwd, raw) {
101
101
  *
102
102
  * Only the invocation's own arguments can mint: the tokens that follow it on
103
103
  * its line, up to the first token that isn't a sanctioned argument (`--auto`, a
104
- * step number, or a range — the argument-hint sanctions nothing else). An
104
+ * step number, or a range; the argument-hint sanctions nothing else). An
105
105
  * unrecognized flag ends the scan, so `/build --wip 2020-2024` mints
106
- * nothing — the `2020-2024` is never reached. An incidental range elsewhere in
107
- * the prompt — an issue number, a pasted `2020-2024` — is prose, not a grant.
106
+ * nothing: the `2020-2024` is never reached. An incidental range elsewhere in
107
+ * the prompt (an issue number, a pasted `2020-2024`) is prose, not a grant.
108
108
  * Trailing sentence punctuation on an argument (`/build 1-3.`) is still the
109
109
  * argument the human typed.
110
110
  */
@@ -129,8 +129,8 @@ export function grantFromPrompt(prompt) {
129
129
  continue;
130
130
  }
131
131
  if (/^\d+$/.test(word))
132
- continue; // a bare step number — keep scanning for a range/--auto
133
- break; // an unrecognized flag or free text — the arguments have ended
132
+ continue; // a bare step number: keep scanning for a range/--auto
133
+ break; // an unrecognized flag or free text: the arguments have ended
134
134
  }
135
135
  if (ceiling !== undefined)
136
136
  return `range ${ceiling}`;
@@ -170,7 +170,7 @@ function readTurn(root) {
170
170
  * Read the hook JSON from fd 0, degrading to '' on any failure.
171
171
  *
172
172
  * An interactive TTY never sends EOF, so skip the read there rather than hang
173
- * (mirrors checkpoint's `--body`); a read error degrades to '' — extractPrompt
173
+ * (mirrors checkpoint's `--body`); a read error degrades to '': extractPrompt
174
174
  * then clears the grant and the tick still lands.
175
175
  */
176
176
  function readStdin() {
package/dist/verbs/use.js CHANGED
@@ -1,10 +1,10 @@
1
- // `plumbbob use <slug>` — re-point the active-build cursor at an existing build
1
+ // `plumbbob use <slug>`: re-point the active-build cursor at an existing build
2
2
  // and resume it, `nvm use`-shaped: switching and resuming are the same one word.
3
3
  // The cursor is the single-line content of `.plumbbob/STATE`, the untracked
4
4
  // per-worktree file whose existence means a session is live and whose one line
5
- // names the build that session is on — so pointing it elsewhere IS the switch,
5
+ // names the build that session is on, so pointing it elsewhere IS the switch,
6
6
  // and one-active-per-worktree holds by construction (one line cannot name two
7
- // builds). It validates the target folder exists, and warns — but allows —
7
+ // builds). It validates the target folder exists, and warns (but allows)
8
8
  // leaving a build with a step in flight: the in-flight markers live per build,
9
9
  // so that state survives the switch and resumes the next time you `use` the
10
10
  // build.
@@ -15,7 +15,7 @@ import { activeBuild, hasSession, intentPath, listBuilds, setActiveBuild, stepPa
15
15
  * Switch the active-build cursor to the named build.
16
16
  *
17
17
  * Refuses without a session, without a slug, or when the target folder has no
18
- * intent.md; notes — but allows — a step left in flight on the build being left.
18
+ * intent.md; notes (but allows) a step left in flight on the build being left.
19
19
  */
20
20
  export function use(cwd, args) {
21
21
  const root = findRepoRoot(cwd);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "checkride",
3
3
  "displayName": "checkride",
4
- "version": "0.10.2",
4
+ "version": "0.12.2",
5
5
  "description": "Bounded, contract-aware readers for what checkride already writes: triage a red gate and read quality signal without loading megabytes of raw artifacts.",
6
6
  "author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
7
7
  "homepage": "https://robmclarty.github.io/checkride/",
@@ -4,6 +4,400 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.12.2] - 2026-08-12
8
+
9
+ ### Added
10
+
11
+ - **A `prose-voice` example** under `examples/`: the `prose` slot end to end —
12
+ a legacy doc grandfathered at eight findings, violations that trip
13
+ `Repo.Drift` in fresh markdown and `Repo.ThereIs`/`Repo.Latin` inside a
14
+ TypeScript doc comment, a ratchet scenario pruning the baseline, and the
15
+ `docs/voice` exemplars the AGENTS.md stanza points writing sessions at. The
16
+ e2e harness grows a `proseRules` violation field so an example asserts
17
+ *which* vale rule fired, not merely that `prose` went red.
18
+
19
+ ### Fixed
20
+
21
+ - The `prose` exemplars pre-flight now says `is a file, not a directory` when
22
+ a file sits at the configured path, instead of the false `does not exist`.
23
+ - Re-resolved `nanoid` to 3.3.18 for GHSA-2v37-7h3g-55p8 (high severity;
24
+ dev-only, via vitest → vite → postcss). No override entry — postcss's own
25
+ range already permits the patched line.
26
+
27
+ ### Internal
28
+
29
+ - New tests pin the default runner's exemplars pre-flight wiring, and the rule
30
+ that a run whose vale output is unreadable masks nothing and prunes nothing
31
+ while the slots the run did observe still ratchet.
32
+
33
+ ## [0.12.1] - 2026-08-07
34
+
35
+ ### Added
36
+
37
+ - **A `prose` slot for writing-style linting**, powered by `vale`. Opt-in like
38
+ `format`, it checks markdown and TypeScript doc comments (via a JS format
39
+ mapping, so code and string literals stay untouched) for doubled words,
40
+ Latin abbreviations (`e.g.`/`i.e.`/`etc.`), hyphenated `-ly` adverbs,
41
+ sentence-initial "There is", weasel words (off by default — the one
42
+ subjective rule), and the lexical fingerprints of AI-generated prose:
43
+ overused vocabulary like `delve`/`tapestry`/`meticulous`, stock frames like
44
+ "it's not just X — it's Y", and the minted corporate dialect (`the ask`,
45
+ `learnings`, `leverage`/`utilize` as verbs). `init --add prose` scaffolds a
46
+ hermetic, repo-owned vale config and six house-style rule files under
47
+ `.vale/styles/Repo/` — no upstream package, no `vale sync` needed on the
48
+ default path. Findings ratchet through the shared baseline the same way
49
+ `lint`/`struct`/`spell` already do. `spell` keeps sole ownership of
50
+ spelling; the scaffold disables vale's own dictionary and vocabulary
51
+ checks to avoid a second, conflicting wordlist.
52
+ - **An `exemplars` config key on the `prose` slot**, for anchoring voice
53
+ rather than just policing vocabulary. Point it at a directory of
54
+ hand-written prose and the generated AGENTS.md stanza tells writing
55
+ sessions to read and imitate it before touching markdown or doc comments —
56
+ and never to edit or add to it, since a generated "improvement" would
57
+ replace the human original with a copy of the model's own register. The
58
+ check itself fails if the named directory is missing or empty, so a stale
59
+ config can't stay green; checkride never scores prose against the
60
+ exemplars, since judging voice deliberately stays a human call.
61
+
62
+ ### Fixed
63
+
64
+ - The README's slot table, its opt-in-slots sentence, the cheat sheet's
65
+ pipeline table, and the config schema's `checks` description had all
66
+ drifted out of sync with the slot registry over several releases (missing
67
+ the publish-ready bundle, missing `dupes`/`health`, undercounting opt-in
68
+ slots). Brought current and pinned by a new test that fails whenever a
69
+ slot is added without updating all four surfaces.
70
+
71
+ ### Internal
72
+
73
+ - checkride now dogfoods its own `prose` slot: 112 initial findings fixed by
74
+ hand (11 subject-first rewrites, 28 Latin substitutions, 21 `-ly`-hyphen
75
+ drops), landing at zero findings with no baseline grandfathering needed.
76
+
77
+ ## [0.12.0] - 2026-08-07
78
+
79
+ ### Added
80
+
81
+ - **`checkride recover` — restore a clobbered baseline from git history.** The
82
+ motivating incident: checkride landed in a large monorepo, other open PRs
83
+ merged the trunk back, and `checkride.baseline.json` came out of the merges
84
+ with many entries silently deleted (an agent resolving a conflict by removing
85
+ the "problem" lines, most likely). Missing entries fail closed, so the team
86
+ hit a wall of red with no tooling to diagnose or undo it — the remedy was
87
+ manual git archaeology, and the tempting wrong move, re-running
88
+ `checkride baseline`, would have grandfathered every genuinely-new finding
89
+ introduced since the damage.
90
+
91
+ `checkride recover` does the dig instead: it walks the baseline file's recent
92
+ commits (`--depth`, default 25), dedupes them into at most five distinct
93
+ snapshots plus one synthesized union, and lists them with `+restored/−absent`
94
+ deltas against the current file. `--pick <n|sha|union>` applies one by writing
95
+ the file directly — never `git checkout` — so the restore is an ordinary
96
+ working-tree edit to review and commit. The default write is the union of the
97
+ candidate and the current file: nothing is removed, and a stale resurrected
98
+ key is pruned by the ratchet on the next full green run. `--exact` writes the
99
+ snapshot verbatim (refused while the file has uncommitted changes), `--dry-run`
100
+ previews the per-slot delta, `--json` emits the machine shape. Shallow clones
101
+ list with a truncation note; a repo with no committed baseline history says so
102
+ at exit 0.
103
+
104
+ - **A mangled or plundered baseline now explains itself.** A committed
105
+ `checkride.baseline.json` that no longer parses (a botched merge, usually)
106
+ used to be indistinguishable from having no baseline: every grandfathered
107
+ finding reported red with no hint why. A run now prints one stderr line under
108
+ the summary naming the file as present-but-unparseable and pointing at
109
+ `checkride recover`; `checkride doctor` grows an advisory workspace row with
110
+ the same diagnosis (and the key count when the file is healthy). And when the
111
+ file parses but a merge silently dropped entries, a red run probes git history
112
+ (best-effort, bounded at 750 ms, silent on any failure) and prints one hint —
113
+ `N of M new finding(s) were grandfathered until <sha>` — naming
114
+ `checkride recover` as the remedy, so a wall of red arrives with its diagnosis
115
+ attached.
116
+
117
+ ### Changed
118
+
119
+ - **The baseline file is canonical on disk.** `writeBaseline` now sorts the
120
+ top-level slots (keys within a slot were already sorted), so two branches that
121
+ grandfather the same debt produce byte-identical files instead of
122
+ insertion-order diff churn — the README's "the file is canonical" claim is now
123
+ true on disk, not just in comparisons.
124
+
125
+ - **`DoctorEnv` gained a required `readText` probe** (it feeds doctor's new
126
+ baseline row). The type sits outside the promised programmatic surface, but a
127
+ TypeScript consumer injecting a custom `env` into `runDoctor` adds one field
128
+ on upgrade.
129
+
130
+ ### Contract
131
+
132
+ - New command `checkride recover` with `--pick <n|sha|union>`, `--exact`,
133
+ `--dry-run`, `--depth <n>`, and `--json` (docs/contract.md §CLI). List mode is
134
+ a reader: Markdown on stdout, exit 0. Apply mode writes
135
+ `checkride.baseline.json` atomically and never mutates git state. Exit codes
136
+ are 0 and 2 only — never 1. Additive; every existing command and flag is
137
+ unchanged.
138
+
139
+ ### Internal
140
+
141
+ - The website caught up with the product and can no longer drift: a new
142
+ `agents.html` covers the bundled plugin, the Claude Code / Cursor
143
+ asymmetries, and the "could not run" verdict; the command reference gained
144
+ `recover`, `hooks`, `gate`, `triage`, and `qa`; and `test/site.test.ts` +
145
+ cspell now hold `site/` under `pnpm check` (inline scripts must parse, the
146
+ command and slot tables must track `CLI_COMMANDS` and `SLOTS`, internal links
147
+ must resolve). Every baseline-adjacent doc (README, getting-started, CI,
148
+ cheatsheet, plugin skill) now routes a clobbered baseline to `recover`.
149
+
150
+ ## [0.11.2] - 2026-08-04
151
+
152
+ ### Changed
153
+
154
+ - **A Cursor stand-down now reaches the user, exactly once.** The promise since
155
+ 0.11.0 was that a stand-down is never silent: Claude Code gets a
156
+ `systemMessage`, Cursor gets stderr. Those were never equivalent, and the
157
+ Cursor half does not hold — Cursor documents no channel that shows hook stderr
158
+ to a user, mentioning only a Hooks output panel under **Customize** that
159
+ someone has to go and open. So on the one verdict whose entire purpose is to
160
+ name a fix only a human can apply (`pnpm install`, an `engines` pin), the human
161
+ saw nothing.
162
+
163
+ `followup_message` is the field that reaches the chat, and the reason it was
164
+ avoided is real: it *submits a new turn*, so a stand-down spending it every
165
+ turn rebuilds the unbounded loop 0.11.0 removed. It is therefore rationed to
166
+ one. The generated hook script already reads `loop_count` to bound `block`, and
167
+ that same guard now bounds `stand_down` — one nudge naming the fix, silence
168
+ after. The guard errs toward quiet: Cursor's `loop_count` counts every
169
+ auto-follow-up in a conversation rather than the consecutive ones, so an
170
+ over-count costs a silent stand-down and never a loop.
171
+
172
+ - **`CHECKRIDE_GATE_RETRY`, a new environment variable the generated gate script
173
+ sets.** checkride's own stand-down — a refusal it detects from inside, such as
174
+ an `engines` pin the hook's Node fails — needs the same one-shot bound and
175
+ cannot compute it, because the stop payload reaches the hook script and never
176
+ the child process. The script now exports `0` when the one message is unspent
177
+ and `1` when it is not, and `checkride gate --harness cursor` emits a followup
178
+ only on an explicit `0`.
179
+
180
+ **Absence is not `0`.** A hook script generated before this release exports
181
+ neither value and keeps the older stderr-only behavior. That asymmetry is the
182
+ point: it is what stops a refreshed checkride from looping under an
183
+ unrefreshed script, given the `loop_limit: null` the gate's own hook entry
184
+ asks for. Run `checkride agent-setup` to pick up the new script.
185
+
186
+ The variable is an internal contract between the two halves checkride
187
+ generates, not a knob — setting it by hand only suppresses or unlocks one
188
+ message.
189
+
190
+ ## [0.11.1] - 2026-08-04
191
+
192
+ ### Fixed
193
+
194
+ - **The Cursor stop gate no longer writes package-manager noise to the channel
195
+ Cursor parses as protocol.** The two generated gate scripts were asymmetric,
196
+ and accidentally so: the Claude Code script captured the run, so a failed
197
+ launch wrote into a variable it could then decline to print, while the Cursor
198
+ script ran the launcher uncaptured in order to let checkride's own JSON pass
199
+ straight through. A *failed* launch writes to that same stdout, and nothing
200
+ filtered it — so in a repo whose dependencies were never installed, two lines
201
+ of pnpm resolution error landed on stdout while the correct stand-down
202
+ explanation went to stderr.
203
+
204
+ The turn still ended as intended, because Cursor does not parse that text as a
205
+ hook body. The reason to fix it anyway is what happens if it ever does: a
206
+ bare-text followup would submit a new turn on every stand-down, rebuilding the
207
+ unbounded loop 0.11.0 removed, and doing it on the one cause an agent can do
208
+ nothing about. It also muddies the gate's own diagnostic — a maintainer
209
+ reading a Cursor transcript sees a dependency-resolution error on the protocol
210
+ channel and a different, correct explanation beside it on stderr.
211
+
212
+ Both scripts now capture, and each re-prints that body only on the two exit
213
+ statuses checkride answers with by design. Anything else is the launcher's
214
+ error rather than a verdict, and `block`/`stand_down` own stdout there alone.
215
+
216
+ ### Internal
217
+
218
+ - The marketing site under `site/` picks up Fathom analytics and Open Graph plus
219
+ Twitter Card metadata across all three pages. The published package is
220
+ unchanged.
221
+
222
+ ## [0.11.0] - 2026-08-04
223
+
224
+ ### Added
225
+
226
+ - **`gate.preflight` — a supported seam for wrapping the stop gate.** checkride
227
+ owns the generated hook scripts and overwrites them on every refresh, so a repo
228
+ with something to say before the gate had nowhere to say it: editing the script
229
+ lost the edit, and re-pointing the harness config lost it to the next
230
+ `hooks add`. A `"preflight"` under `gate` in checkride.config.json names a
231
+ repo-relative script the generated gate runs, from the repo root, before
232
+ checkride is started — which is what lets it answer for a repo where checkride
233
+ *cannot* start.
234
+
235
+ Its exit code is read in the gate's own vocabulary rather than a second
236
+ convention: `0` runs the gate, `2` blocks the turn, anything else stands the
237
+ gate down. Both non-zero branches use the script's output as the message and
238
+ never start checkride. A configured path that does not exist **blocks**, so a
239
+ typo cannot quietly disarm the gate.
240
+
241
+ It is a config key rather than a `hooks add --preflight` flag for one reason:
242
+ the path is re-read on every write, so a teammate running the bare command
243
+ restores the wiring instead of silently dropping it. A preflight narrows
244
+ nothing, so it never adds the "NOT the full check" clause to a verdict.
245
+
246
+ ### Contract
247
+
248
+ - **A could-not-run gate now blocks only when blocking could lead to a fix.**
249
+ Previously every could-not-run verdict blocked, in both harnesses, on the
250
+ reasoning that a gate which stops gating is a vacuous green. That reasoning
251
+ holds only while the block can *cause* the fix, and for a whole class of causes
252
+ it cannot: the harness re-prompts the same agent, which has no lever to pull.
253
+ The observed end of that road is an unbounded retry against an impossible
254
+ remedy, and then a contributor removing the gate.
255
+
256
+ Launch refusals are now classified by who can clear them. A **repo** refusal —
257
+ no `check` script, no package.json — blocks exactly as before, because an agent
258
+ holding the edit tools can fix it. (A missing `check` script stays blocking for
259
+ a second reason: it is the one refusal an agent could otherwise *create* to
260
+ escape the gate.) An **environment** refusal — an `engines` pin the hook's Node
261
+ fails, a corepack that will not verify the package manager, a package manager
262
+ absent from `PATH` — stands down: exit 0, no `decision`, `green: false`.
263
+
264
+ Two properties are promised about the stand-down and neither may be weakened:
265
+ it is never silent (Claude Code gets a `systemMessage`, Cursor gets stderr,
266
+ both stating that nothing was verified), and it is never reported as green.
267
+ `docs/contract.md` and `test/contract/gate-verdict.contract.test.ts` are
268
+ updated deliberately; a hook script that reads only the exit status will now
269
+ let a turn end on an environment refusal, which is the intended change.
270
+
271
+ ### Changed
272
+
273
+ - **`init` now pins markdownlint-cli2 0.23.2 for the `docs` slot**, up from
274
+ 0.23.1 — the version that resolves a vulnerable `js-yaml`
275
+ (GHSA-pm4m-ph32-ghv5) transitively into every repo it sets up. An existing
276
+ repo keeps whatever it installed; re-run `checkride init` or bump it yourself.
277
+
278
+ ### Fixed
279
+
280
+ - **An uninstalled repo no longer traps the agent in an unbreakable loop.** A
281
+ developer who pulled a branch that newly added checkride — or cloned fresh —
282
+ and started a session before `<pm> install` got a gate that blocked every turn
283
+ with `checkride gate is not resolvable in this repo. Install checkride (or
284
+ re-run checkride agent-setup)`. Every part of that was a dead end: the
285
+ suggested remedy needs the missing binary, and in a repo with a customized
286
+ AGENTS.md stanza `agent-setup` would refuse and exit 2 even if it could run.
287
+ The actual fix, `<pm> install`, was never named. Nothing bounded the retry.
288
+
289
+ The generated gate now distinguishes the two ways checkride can fail to answer.
290
+ Dependencies never installed → it names `<pm> install`, says checkride is a
291
+ devDependency, and stands down. Installed but not answering → it blocks, as
292
+ before, since there is no remedy to name. Yarn PnP is recognized, so a healthy
293
+ PnP repo is not misread as uninstalled.
294
+
295
+ - **Both generated gates bound every could-not-run cause.** The scripts now read
296
+ the Stop payload from stdin and consult Claude Code's `stop_hook_active` and
297
+ Cursor's `loop_count`. On the second consecutive could-not-run, whatever the
298
+ cause, the gate stands down instead of blocking again — the previous script
299
+ read no stdin at all, so `stop_hook_active` was discarded and
300
+ `"loop_limit": null` left the Cursor gate unbounded by design. A **red**
301
+ pipeline is deliberately still unbounded: that loop is the gate working.
302
+
303
+ - **The claude gate no longer forwards the launcher's error as a hook body.** On
304
+ a missing install pnpm writes `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL` to *stdout*
305
+ — ahead of a stray `undefined` on pnpm 11 — which the script captured into
306
+ `$body` and printed where Claude Code expects JSON. The status is now checked
307
+ before the body is forwarded, so a body travels only when checkride wrote it.
308
+
309
+ - **A refusal the repo can fix is no longer explained by the hook's Node.** The
310
+ `nodeClause` paragraph rode on every refusal, so "this repo has no `check`
311
+ script" was followed by an account of non-login shells and interpreter
312
+ alignment. It is now attached to environment refusals only.
313
+
314
+ - **A red run could be reported as "could not run" on Linux.** `isFresh` judged
315
+ the summary this run wrote by comparing its mtime against a `Date.now()`
316
+ reading taken just before the check spawned — and those are not the same
317
+ clock. Linux mtimes come from the kernel's coarse timer-tick clock, so a
318
+ summary written *after* `startedAt` routinely landed a few milliseconds behind
319
+ it: 934 of 2000 writes in a container, and 0 of 2000 on APFS. When it misfired
320
+ the gate read its own summary as a previous run's, believed an `engines`-pin
321
+ marker the check had merely printed, and blamed the environment for broken
322
+ code. `MTIME_TOLERANCE_MS` already guarded the same comparison in `triage/`;
323
+ the gate's own copy had dropped it. This is what turned the Linux CI legs red
324
+ on v0.10.2 and v0.10.3 while both macOS legs stayed green.
325
+
326
+ - **A green run that skipped most of its slots says so.** `✔ all checks passed`
327
+ is a true sentence about the checks that ran and a misleading one about the
328
+ repo, and the gap was invisible exactly where it matters: a repo configuring
329
+ almost nothing reported the same confident green as one configuring
330
+ everything. The line now reads `✔ all checks passed in 13ms — only 1 of 8
331
+ checks ran, 7 skipped` whenever any slot sat out. `--strict` is unchanged; its
332
+ floor is *zero* checks, which an always-available slot like `links` keeps a
333
+ repo off by itself.
334
+
335
+ ### Internal
336
+
337
+ - CI dumps a failing slot's captured streams into collapsible log groups and
338
+ keeps `.check/` as an artifact. A red run logged `✘ test` and nothing else,
339
+ and on a hosted runner the directory holding the reason died with the
340
+ workspace — so the Linux-only gate failure above had to be reproduced in a
341
+ container before it could be read at all.
342
+
343
+ - Dependency audit cleared and mostly retired. Two high advisories were open
344
+ (`fast-uri` GHSA-7p8r-x3mc-p8w7, `brace-expansion` GHSA-rgw5-rvv9-x895), both
345
+ already carrying an override whose range the advisory had since outgrown.
346
+ Re-resolving showed four of the five overrides were dead weight — the parents'
347
+ own ranges reach patched versions — so they were removed rather than widened,
348
+ leaving one. No runtime `dependencies` are declared, so none of this ever
349
+ reached the tarball.
350
+
351
+ ## [0.10.3] - 2026-08-01
352
+
353
+ ### Added
354
+
355
+ - **Both Cursor guards now cover the shell.** `protect` and `dirty` each take a
356
+ second `.cursor/hooks.json` entry — on `beforeShellExecution` and
357
+ `afterShellExecution` — whose matchers run against the command string rather
358
+ than a tool name. This closes the older of the two documented Cursor gaps:
359
+ `echo … > checkride.baseline.json` is a shell call, not a `Write`, so no
360
+ file-tool matcher could ever see it, and a turn that wrote only through the
361
+ shell used to skip the gate entirely.
362
+
363
+ The standing objection was that matching the shell means parsing arbitrary
364
+ command lines, and a guard that fires on a wrong parse is worse than one with
365
+ a known hole. That objection is answered rather than dropped. `protect`'s
366
+ matcher admits only commands already naming an accounting path — reads
367
+ included — and the script then denies solely on demonstrated write intent: a
368
+ `>`/`>>` redirect target, or a positional argument of `rm`, `mv`, `tee`,
369
+ `sed -i`, `dd of=` and kin. Anything it cannot read that way is allowed, so
370
+ `cat`, `jq`, `grep` and `cp .check/summary.json /tmp/x` keep working, as
371
+ triage requires. `dirty` decides from its matcher alone and biases the other
372
+ way, toward marking a turn dirty, because there the recoverable error is one
373
+ extra pipeline run.
374
+
375
+ Both entries share their sibling's name and script: one guard, one
376
+ `--remove-hook protect`, one `.cjs` branching on `hook_event_name`. The gate
377
+ takes no shell entry — it is not per-call. Claude Code's half of the gap stays
378
+ open; `permissions.deny` is a file-path list, and a `Bash` matcher there would
379
+ mean shipping a generated script that harness does not currently need.
380
+
381
+ ### Changed
382
+
383
+ - **The AGENTS.md stanza no longer promises the gate ran everything.** It said
384
+ the stop-gate hook "runs the full `pnpm check`" and told the agent to "let the
385
+ hook run the authoritative pipeline once at the end" — written before gate
386
+ profiles existed, and false in any repo that declares one. The cost of that is
387
+ specific: the stanza is what an agent reads *first*, so it pre-authorized
388
+ reading a profiled `✔ green` as done, and the `NOT the full check` clause
389
+ arrived after the agent had already been told the hook was the last word. The
390
+ disclosure and the contract disagreed, and the contract was the one being read
391
+ in advance.
392
+
393
+ The paragraph now sends the reader to the gate's own verdict — "read the
394
+ gate's verdict rather than assuming it covered everything" — and says plainly
395
+ that a narrowed green is not the "done" defined above. It names no profile
396
+ deliberately: a repo can add `gate` to `checkride.config.json` without
397
+ re-running `agent-setup`, so any stanza that spelled out a specific profile
398
+ would drift back into over-promising, while the verdict is generated per run
399
+ and cannot.
400
+
7
401
  ## [0.10.2] - 2026-07-31
8
402
 
9
403
  ### Added
@@ -1453,6 +1847,13 @@ The first real release. (`0.0.0` was a name-claim placeholder.)
1453
1847
  - Flags: `--only`, `--skip`, `--bail`, `--json`, `--changed`, `--all`,
1454
1848
  `--include`.
1455
1849
 
1850
+ [0.12.2]: https://www.npmjs.com/package/checkride/v/0.12.2
1851
+ [0.12.1]: https://www.npmjs.com/package/checkride/v/0.12.1
1852
+ [0.12.0]: https://www.npmjs.com/package/checkride/v/0.12.0
1853
+ [0.11.2]: https://www.npmjs.com/package/checkride/v/0.11.2
1854
+ [0.11.1]: https://www.npmjs.com/package/checkride/v/0.11.1
1855
+ [0.11.0]: https://www.npmjs.com/package/checkride/v/0.11.0
1856
+ [0.10.3]: https://www.npmjs.com/package/checkride/v/0.10.3
1456
1857
  [0.10.2]: https://www.npmjs.com/package/checkride/v/0.10.2
1457
1858
  [0.10.1]: https://www.npmjs.com/package/checkride/v/0.10.1
1458
1859
  [0.10.0]: https://www.npmjs.com/package/checkride/v/0.10.0
@@ -155,6 +155,12 @@ checkride init Set up a project (new or existing — auto-detected).
155
155
  checkride doctor Verify environment + every slot's status (read-only, exit 0/1).
156
156
  checkride fix Run every active adapter's fix command (oxlint --fix, ...).
157
157
  checkride baseline Record current diagnostics as a committed baseline.
158
+ checkride recover Rebuild checkride.baseline.json candidates from git history
159
+ and restore one (bare `recover` lists them with deltas).
160
+ --pick <n|sha|union> apply a candidate — additive union with the current file
161
+ --exact write the picked snapshot verbatim instead of the union
162
+ --dry-run preview the per-slot delta; write nothing
163
+ --depth <n> commits of file history to walk (default 25)
158
164
  checkride agent-setup Add the "check" alias, AGENTS.md stanza + agent hooks to a repo.
159
165
  --hook <a,b> write only these hooks (gate | dirty | protect)
160
166
  --no-hook skip the hooks (write only the stanza)
@@ -178,8 +184,8 @@ default run leaves stdout empty.
178
184
  ## The pipeline: slots and adapters
179
185
 
180
186
  A **slot** is a role in the pipeline (order matters — cheapest first). An
181
- **adapter** is a concrete tool that fills a slot. There is one blessed default
182
- per slot; alternates are wired so checkride can run them, but `init` only
187
+ **adapter** is a concrete tool that fills a slot. Each slot has one blessed
188
+ default; alternates are wired so checkride can run them, but `init` only
183
189
  generates config for the blessed default.
184
190
 
185
191
  | Slot | Role | Blessed default | Alternates |
@@ -195,10 +201,15 @@ generates config for the blessed default.
195
201
  | `docs` | Markdown lint | `markdownlint-cli2` | — |
196
202
  | `links` | Relative markdown links resolve | built-in | — |
197
203
  | `spell` | Spelling | `cspell` | — |
204
+ | `prose` | Writing style (opt-in) | `vale` | — |
198
205
  | `mutation` | Mutation testing (opt-in) | `stryker` | — |
199
206
  | `security` | Dependency audit (opt-in) | `pnpm audit` | — |
207
+ | `build` | Runs the consumer's build script (opt-in) | `<pm> run build` | — |
200
208
  | `publint` | Package publishing lint (opt-in) | `publint` | — |
201
209
  | `attw` | Type resolution across module systems (opt-in) | `attw --pack`| — |
210
+ | `pack` | Tarball ships the right files (opt-in) | built-in | — |
211
+ | `smoke` | Built package imports cleanly (opt-in) | built-in | — |
212
+ | `snippets` | Tagged doc fences type-check (opt-in) | built-in (`tsc`) | — |
202
213
 
203
214
  Most of these tools you already know. The one unfamiliar name, `fallow`, is a
204
215
  Rust-native codebase-analysis tool covering dead code, duplication, and
@@ -210,16 +221,17 @@ exists, and skips slots with no detected tool. The core has **no runtime
210
221
  dependency** on any checked tool — it spawns `<pm> exec <tool>`; the project
211
222
  owns the pinned tool versions.
212
223
 
213
- **Opt-in slots** (`format`, `mutation`, `security`, `publint`, `attw`) stay out of the
224
+ **Opt-in slots** (`format`, `dupes`, `health`, `prose`, `mutation`, `security`, `build`,
225
+ `publint`, `attw`, `pack`, `smoke`, `snippets`) stay out of the
214
226
  default run so adopting checkride — or bumping its version — never turns a repo red on a
215
227
  check it didn't ask for. Turn one on with `--include <slot>` (or `--all`), or by **naming
216
228
  it in `checks`**: an explicit entry like `"format": "prettier"` opts the slot into every
217
- run. `checkride fix` then runs its write form (e.g. `prettier --write`) alongside the
229
+ run. `checkride fix` then runs its write form (for example `prettier --write`) alongside the
218
230
  other fixers. `format` sits before `lint` so the tree is tidy before the linters look.
219
231
 
220
232
  To **configure a slot without opting it in** — say, give `attw` a `--profile` but keep it
221
233
  in the full sweep only — add `"optIn": true` to its entry: it stays out of the default run
222
- and runs solely under `--all`/`--include <slot>`. The same field demotes a normally-default
234
+ and runs solely under `--all`/`--include <slot>`. The same field demotes a normally default
223
235
  slot (or a heavy custom check like `test:integration`) to full-sweep-only; `"optIn": false`
224
236
  does the opposite, forcing a slot into the default run.
225
237
 
@@ -475,7 +487,13 @@ keys are sorted), so parallel branches usually merge cleanly; when they do
475
487
  conflict, resolve by keeping both sides' entries and running a full
476
488
  `checkride` — the ratchet prunes anything already fixed, so an over-generous
477
489
  union self-heals, while a dropped entry that still fails simply resurfaces as
478
- a red check. **Deliberate re-baseline:** `checkride baseline` re-records
490
+ a red check. When a merge went badly anyway — entries deleted wholesale, or a
491
+ file so mangled it no longer parses (a run says so on stderr and treats it as
492
+ no baseline at all) — the run diagnoses what it can: a red run whose "new"
493
+ findings were grandfathered until a recent commit prints a `hint:` line naming
494
+ that commit. `checkride recover` then rebuilds candidate states from the
495
+ file's git history and restores one additively; see `checkride recover --help`.
496
+ **Deliberate re-baseline:** `checkride baseline` re-records
479
497
  *everything* currently failing — including brand-new debt you might rather
480
498
  fix — so treat re-running it as a reviewed change: do it deliberately (say,
481
499
  after adopting a stricter rule set) and read the `checkride.baseline.json`
@@ -508,9 +526,9 @@ At runtime, checkride itself is workspace-agnostic: whatever the shape, it runs
508
526
  each tool **once, from the repo root** — one pipeline, one `.check/`. Workspace
509
527
  awareness comes from the tools' own configs, which is what the monorepo scaffold
510
528
  sets up: `tsc --build` follows the root tsconfig's project references, and
511
- vitest, oxlint, ast-grep, and the rest walk the whole tree. There is no
512
- per-package orchestration and no way to check a single package — narrow a run
513
- with `--only`/`--changed`, not per-directory.
529
+ vitest, oxlint, ast-grep, and the rest walk the whole tree. checkride itself
530
+ does no per-package orchestration and offers no way to check a single package —
531
+ narrow a run with `--only`/`--changed`, not per-directory.
514
532
 
515
533
  ## Conventions
516
534