plumbbob 0.9.2 → 0.9.5

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 (279) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -13
  3. package/dist/cli-core.js +298 -24
  4. package/dist/lib/check.js +46 -13
  5. package/dist/lib/intent.js +1 -1
  6. package/dist/lib/latch.js +3 -3
  7. package/dist/lib/reentry.js +70 -0
  8. package/dist/verbs/agent.js +5 -4
  9. package/dist/verbs/check.js +6 -2
  10. package/dist/verbs/doctor.js +4 -4
  11. package/dist/verbs/finish.js +1 -1
  12. package/dist/verbs/recover.js +297 -0
  13. package/dist/verbs/revert.js +2 -2
  14. package/dist/verbs/spike.js +8 -2
  15. package/hooks/post-edit.sh +4 -4
  16. package/hooks/pre-bash-commit.sh +3 -3
  17. package/node_modules/checkride/.claude-plugin/plugin.json +11 -0
  18. package/node_modules/checkride/CHANGELOG.md +1003 -0
  19. package/node_modules/checkride/LICENSE +201 -21
  20. package/node_modules/checkride/README.md +171 -30
  21. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts +15 -0
  22. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts.map +1 -0
  23. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js +17 -0
  24. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js.map +1 -0
  25. package/node_modules/checkride/dist/adapters.d.ts +18 -6
  26. package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
  27. package/node_modules/checkride/dist/adapters.js +42 -23
  28. package/node_modules/checkride/dist/adapters.js.map +1 -1
  29. package/node_modules/checkride/dist/agent-setup/claude.d.ts +88 -0
  30. package/node_modules/checkride/dist/agent-setup/claude.d.ts.map +1 -0
  31. package/node_modules/checkride/dist/agent-setup/claude.js +305 -0
  32. package/node_modules/checkride/dist/agent-setup/claude.js.map +1 -0
  33. package/node_modules/checkride/dist/agent-setup/command.d.ts +43 -0
  34. package/node_modules/checkride/dist/agent-setup/command.d.ts.map +1 -0
  35. package/node_modules/checkride/dist/agent-setup/command.js +77 -0
  36. package/node_modules/checkride/dist/agent-setup/command.js.map +1 -0
  37. package/node_modules/checkride/dist/agent-setup/cursor.d.ts +61 -0
  38. package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -0
  39. package/node_modules/checkride/dist/agent-setup/cursor.js +189 -0
  40. package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -0
  41. package/node_modules/checkride/dist/agent-setup/files.d.ts +50 -0
  42. package/node_modules/checkride/dist/agent-setup/files.d.ts.map +1 -0
  43. package/node_modules/checkride/dist/agent-setup/files.js +80 -0
  44. package/node_modules/checkride/dist/agent-setup/files.js.map +1 -0
  45. package/node_modules/checkride/dist/agent-setup/harness.d.ts +101 -0
  46. package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -0
  47. package/node_modules/checkride/dist/agent-setup/harness.js +84 -0
  48. package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -0
  49. package/node_modules/checkride/dist/agent-setup/hook.d.ts +45 -52
  50. package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -1
  51. package/node_modules/checkride/dist/agent-setup/hook.js +64 -95
  52. package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -1
  53. package/node_modules/checkride/dist/agent-setup/index.d.ts +10 -4
  54. package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -1
  55. package/node_modules/checkride/dist/agent-setup/index.js +9 -4
  56. package/node_modules/checkride/dist/agent-setup/index.js.map +1 -1
  57. package/node_modules/checkride/dist/agent-setup/scripts.d.ts +58 -0
  58. package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -0
  59. package/node_modules/checkride/dist/agent-setup/scripts.js +299 -0
  60. package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -0
  61. package/node_modules/checkride/dist/agent-setup/skills.d.ts +31 -0
  62. package/node_modules/checkride/dist/agent-setup/skills.d.ts.map +1 -0
  63. package/node_modules/checkride/dist/agent-setup/skills.js +56 -0
  64. package/node_modules/checkride/dist/agent-setup/skills.js.map +1 -0
  65. package/node_modules/checkride/dist/artifacts/configured.d.ts +30 -0
  66. package/node_modules/checkride/dist/artifacts/configured.d.ts.map +1 -0
  67. package/node_modules/checkride/dist/artifacts/configured.js +45 -0
  68. package/node_modules/checkride/dist/artifacts/configured.js.map +1 -0
  69. package/node_modules/checkride/dist/artifacts/format.d.ts +33 -0
  70. package/node_modules/checkride/dist/artifacts/format.d.ts.map +1 -0
  71. package/node_modules/checkride/dist/artifacts/format.js +56 -0
  72. package/node_modules/checkride/dist/artifacts/format.js.map +1 -0
  73. package/node_modules/checkride/dist/artifacts/freshness.d.ts +32 -0
  74. package/node_modules/checkride/dist/artifacts/freshness.d.ts.map +1 -0
  75. package/node_modules/checkride/dist/artifacts/freshness.js +37 -0
  76. package/node_modules/checkride/dist/artifacts/freshness.js.map +1 -0
  77. package/node_modules/checkride/dist/artifacts/index.d.ts +22 -0
  78. package/node_modules/checkride/dist/artifacts/index.d.ts.map +1 -0
  79. package/node_modules/checkride/dist/artifacts/index.js +19 -0
  80. package/node_modules/checkride/dist/artifacts/index.js.map +1 -0
  81. package/node_modules/checkride/dist/artifacts/json.d.ts +31 -0
  82. package/node_modules/checkride/dist/artifacts/json.d.ts.map +1 -0
  83. package/node_modules/checkride/dist/artifacts/json.js +47 -0
  84. package/node_modules/checkride/dist/artifacts/json.js.map +1 -0
  85. package/node_modules/checkride/dist/artifacts/raw.d.ts +60 -0
  86. package/node_modules/checkride/dist/artifacts/raw.d.ts.map +1 -0
  87. package/node_modules/checkride/dist/artifacts/raw.js +91 -0
  88. package/node_modules/checkride/dist/artifacts/raw.js.map +1 -0
  89. package/node_modules/checkride/dist/artifacts/summary.d.ts +69 -0
  90. package/node_modules/checkride/dist/artifacts/summary.d.ts.map +1 -0
  91. package/node_modules/checkride/dist/artifacts/summary.js +108 -0
  92. package/node_modules/checkride/dist/artifacts/summary.js.map +1 -0
  93. package/node_modules/checkride/dist/atomic.d.ts +16 -1
  94. package/node_modules/checkride/dist/atomic.d.ts.map +1 -1
  95. package/node_modules/checkride/dist/atomic.js +25 -3
  96. package/node_modules/checkride/dist/atomic.js.map +1 -1
  97. package/node_modules/checkride/dist/baseline/fallow.d.ts +9 -5
  98. package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -1
  99. package/node_modules/checkride/dist/baseline/fallow.js +161 -58
  100. package/node_modules/checkride/dist/baseline/fallow.js.map +1 -1
  101. package/node_modules/checkride/dist/baseline/fingerprint.d.ts +7 -7
  102. package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
  103. package/node_modules/checkride/dist/baseline/fingerprint.js +16 -19
  104. package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
  105. package/node_modules/checkride/dist/baseline/index.d.ts +4 -4
  106. package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
  107. package/node_modules/checkride/dist/baseline/index.js +4 -4
  108. package/node_modules/checkride/dist/baseline/index.js.map +1 -1
  109. package/node_modules/checkride/dist/baseline/store.d.ts +12 -4
  110. package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
  111. package/node_modules/checkride/dist/baseline/store.js +16 -9
  112. package/node_modules/checkride/dist/baseline/store.js.map +1 -1
  113. package/node_modules/checkride/dist/baseline-command.d.ts +5 -5
  114. package/node_modules/checkride/dist/baseline-command.js +7 -7
  115. package/node_modules/checkride/dist/baseline-command.js.map +1 -1
  116. package/node_modules/checkride/dist/cli.d.ts +17 -4
  117. package/node_modules/checkride/dist/cli.d.ts.map +1 -1
  118. package/node_modules/checkride/dist/cli.js +259 -18
  119. package/node_modules/checkride/dist/cli.js.map +1 -1
  120. package/node_modules/checkride/dist/config.d.ts +84 -8
  121. package/node_modules/checkride/dist/config.d.ts.map +1 -1
  122. package/node_modules/checkride/dist/config.js +122 -18
  123. package/node_modules/checkride/dist/config.js.map +1 -1
  124. package/node_modules/checkride/dist/digest/digest.d.ts +5 -5
  125. package/node_modules/checkride/dist/digest/digest.js +5 -5
  126. package/node_modules/checkride/dist/digest/index.d.ts +2 -2
  127. package/node_modules/checkride/dist/digest/index.js +2 -2
  128. package/node_modules/checkride/dist/doctor.d.ts +25 -6
  129. package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
  130. package/node_modules/checkride/dist/doctor.js +193 -27
  131. package/node_modules/checkride/dist/doctor.js.map +1 -1
  132. package/node_modules/checkride/dist/gate.d.ts +135 -0
  133. package/node_modules/checkride/dist/gate.d.ts.map +1 -0
  134. package/node_modules/checkride/dist/gate.js +490 -0
  135. package/node_modules/checkride/dist/gate.js.map +1 -0
  136. package/node_modules/checkride/dist/init.d.ts +72 -12
  137. package/node_modules/checkride/dist/init.d.ts.map +1 -1
  138. package/node_modules/checkride/dist/init.js +324 -81
  139. package/node_modules/checkride/dist/init.js.map +1 -1
  140. package/node_modules/checkride/dist/json.d.ts +21 -0
  141. package/node_modules/checkride/dist/json.d.ts.map +1 -0
  142. package/node_modules/checkride/dist/json.js +21 -0
  143. package/node_modules/checkride/dist/json.js.map +1 -0
  144. package/node_modules/checkride/dist/links.d.ts +31 -5
  145. package/node_modules/checkride/dist/links.d.ts.map +1 -1
  146. package/node_modules/checkride/dist/links.js +141 -32
  147. package/node_modules/checkride/dist/links.js.map +1 -1
  148. package/node_modules/checkride/dist/node-pin.d.ts +129 -0
  149. package/node_modules/checkride/dist/node-pin.d.ts.map +1 -0
  150. package/node_modules/checkride/dist/node-pin.js +211 -0
  151. package/node_modules/checkride/dist/node-pin.js.map +1 -0
  152. package/node_modules/checkride/dist/orchestrator.d.ts +40 -10
  153. package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
  154. package/node_modules/checkride/dist/orchestrator.js +196 -102
  155. package/node_modules/checkride/dist/orchestrator.js.map +1 -1
  156. package/node_modules/checkride/dist/pack.d.ts +8 -9
  157. package/node_modules/checkride/dist/pack.d.ts.map +1 -1
  158. package/node_modules/checkride/dist/pack.js +53 -17
  159. package/node_modules/checkride/dist/pack.js.map +1 -1
  160. package/node_modules/checkride/dist/pm/detect.d.ts +6 -1
  161. package/node_modules/checkride/dist/pm/detect.d.ts.map +1 -1
  162. package/node_modules/checkride/dist/pm/detect.js +6 -1
  163. package/node_modules/checkride/dist/pm/detect.js.map +1 -1
  164. package/node_modules/checkride/dist/pm/index.d.ts +6 -2
  165. package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
  166. package/node_modules/checkride/dist/pm/index.js +6 -2
  167. package/node_modules/checkride/dist/pm/index.js.map +1 -1
  168. package/node_modules/checkride/dist/pm/launch.d.ts +51 -0
  169. package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -0
  170. package/node_modules/checkride/dist/pm/launch.js +92 -0
  171. package/node_modules/checkride/dist/pm/launch.js.map +1 -0
  172. package/node_modules/checkride/dist/pm/tools.d.ts +73 -0
  173. package/node_modules/checkride/dist/pm/tools.d.ts.map +1 -0
  174. package/node_modules/checkride/dist/pm/tools.js +124 -0
  175. package/node_modules/checkride/dist/pm/tools.js.map +1 -0
  176. package/node_modules/checkride/dist/pm/translate.d.ts +35 -9
  177. package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -1
  178. package/node_modules/checkride/dist/pm/translate.js +78 -18
  179. package/node_modules/checkride/dist/pm/translate.js.map +1 -1
  180. package/node_modules/checkride/dist/proc.d.ts +30 -0
  181. package/node_modules/checkride/dist/proc.d.ts.map +1 -0
  182. package/node_modules/checkride/dist/proc.js +42 -0
  183. package/node_modules/checkride/dist/proc.js.map +1 -0
  184. package/node_modules/checkride/dist/qa/cli.d.ts +20 -0
  185. package/node_modules/checkride/dist/qa/cli.d.ts.map +1 -0
  186. package/node_modules/checkride/dist/qa/cli.js +24 -0
  187. package/node_modules/checkride/dist/qa/cli.js.map +1 -0
  188. package/node_modules/checkride/dist/qa/dead.d.ts +39 -0
  189. package/node_modules/checkride/dist/qa/dead.d.ts.map +1 -0
  190. package/node_modules/checkride/dist/qa/dead.js +88 -0
  191. package/node_modules/checkride/dist/qa/dead.js.map +1 -0
  192. package/node_modules/checkride/dist/qa/dupes.d.ts +45 -0
  193. package/node_modules/checkride/dist/qa/dupes.d.ts.map +1 -0
  194. package/node_modules/checkride/dist/qa/dupes.js +62 -0
  195. package/node_modules/checkride/dist/qa/dupes.js.map +1 -0
  196. package/node_modules/checkride/dist/qa/health.d.ts +85 -0
  197. package/node_modules/checkride/dist/qa/health.d.ts.map +1 -0
  198. package/node_modules/checkride/dist/qa/health.js +109 -0
  199. package/node_modules/checkride/dist/qa/health.js.map +1 -0
  200. package/node_modules/checkride/dist/qa/index.d.ts +23 -0
  201. package/node_modules/checkride/dist/qa/index.d.ts.map +1 -0
  202. package/node_modules/checkride/dist/qa/index.js +22 -0
  203. package/node_modules/checkride/dist/qa/index.js.map +1 -0
  204. package/node_modules/checkride/dist/qa/mutation.d.ts +80 -0
  205. package/node_modules/checkride/dist/qa/mutation.d.ts.map +1 -0
  206. package/node_modules/checkride/dist/qa/mutation.js +195 -0
  207. package/node_modules/checkride/dist/qa/mutation.js.map +1 -0
  208. package/node_modules/checkride/dist/qa/qa.d.ts +71 -0
  209. package/node_modules/checkride/dist/qa/qa.d.ts.map +1 -0
  210. package/node_modules/checkride/dist/qa/qa.js +158 -0
  211. package/node_modules/checkride/dist/qa/qa.js.map +1 -0
  212. package/node_modules/checkride/dist/qa/read.d.ts +40 -0
  213. package/node_modules/checkride/dist/qa/read.d.ts.map +1 -0
  214. package/node_modules/checkride/dist/qa/read.js +47 -0
  215. package/node_modules/checkride/dist/qa/read.js.map +1 -0
  216. package/node_modules/checkride/dist/qa/render.d.ts +26 -0
  217. package/node_modules/checkride/dist/qa/render.d.ts.map +1 -0
  218. package/node_modules/checkride/dist/qa/render.js +310 -0
  219. package/node_modules/checkride/dist/qa/render.js.map +1 -0
  220. package/node_modules/checkride/dist/security.d.ts +45 -0
  221. package/node_modules/checkride/dist/security.d.ts.map +1 -0
  222. package/node_modules/checkride/dist/security.js +86 -0
  223. package/node_modules/checkride/dist/security.js.map +1 -0
  224. package/node_modules/checkride/dist/smoke.d.ts +5 -5
  225. package/node_modules/checkride/dist/smoke.js +5 -5
  226. package/node_modules/checkride/dist/snippets.d.ts +26 -29
  227. package/node_modules/checkride/dist/snippets.d.ts.map +1 -1
  228. package/node_modules/checkride/dist/snippets.js +26 -30
  229. package/node_modules/checkride/dist/snippets.js.map +1 -1
  230. package/node_modules/checkride/dist/tool-json.d.ts +33 -0
  231. package/node_modules/checkride/dist/tool-json.d.ts.map +1 -0
  232. package/node_modules/checkride/dist/tool-json.js +58 -0
  233. package/node_modules/checkride/dist/tool-json.js.map +1 -0
  234. package/node_modules/checkride/dist/triage/cli.d.ts +21 -0
  235. package/node_modules/checkride/dist/triage/cli.d.ts.map +1 -0
  236. package/node_modules/checkride/dist/triage/cli.js +26 -0
  237. package/node_modules/checkride/dist/triage/cli.js.map +1 -0
  238. package/node_modules/checkride/dist/triage/doctor-fold.d.ts +51 -0
  239. package/node_modules/checkride/dist/triage/doctor-fold.d.ts.map +1 -0
  240. package/node_modules/checkride/dist/triage/doctor-fold.js +88 -0
  241. package/node_modules/checkride/dist/triage/doctor-fold.js.map +1 -0
  242. package/node_modules/checkride/dist/triage/env.d.ts +54 -0
  243. package/node_modules/checkride/dist/triage/env.d.ts.map +1 -0
  244. package/node_modules/checkride/dist/triage/env.js +81 -0
  245. package/node_modules/checkride/dist/triage/env.js.map +1 -0
  246. package/node_modules/checkride/dist/triage/gate.d.ts +61 -0
  247. package/node_modules/checkride/dist/triage/gate.d.ts.map +1 -0
  248. package/node_modules/checkride/dist/triage/gate.js +121 -0
  249. package/node_modules/checkride/dist/triage/gate.js.map +1 -0
  250. package/node_modules/checkride/dist/triage/index.d.ts +22 -0
  251. package/node_modules/checkride/dist/triage/index.d.ts.map +1 -0
  252. package/node_modules/checkride/dist/triage/index.js +20 -0
  253. package/node_modules/checkride/dist/triage/index.js.map +1 -0
  254. package/node_modules/checkride/dist/triage/render.d.ts +19 -0
  255. package/node_modules/checkride/dist/triage/render.d.ts.map +1 -0
  256. package/node_modules/checkride/dist/triage/render.js +419 -0
  257. package/node_modules/checkride/dist/triage/render.js.map +1 -0
  258. package/node_modules/checkride/dist/triage/triage.d.ts +83 -0
  259. package/node_modules/checkride/dist/triage/triage.d.ts.map +1 -0
  260. package/node_modules/checkride/dist/triage/triage.js +131 -0
  261. package/node_modules/checkride/dist/triage/triage.js.map +1 -0
  262. package/node_modules/checkride/package.json +17 -15
  263. package/node_modules/checkride/schema/checkride.config.schema.json +62 -5
  264. package/node_modules/checkride/schema/checkride.summary.schema.json +1 -1
  265. package/node_modules/checkride/skills/check/SKILL.md +257 -0
  266. package/node_modules/checkride/skills/qa/SKILL.md +231 -0
  267. package/node_modules/checkride/templates/shared/cspell.json +1 -0
  268. package/node_modules/checkride/templates/shared/oxlintrc.json +1 -1
  269. package/package.json +2 -2
  270. package/skills/build/SKILL.md +23 -19
  271. package/skills/finish/SKILL.md +3 -1
  272. package/skills/harvest/SKILL.md +2 -1
  273. package/skills/plan/SKILL.md +8 -7
  274. package/skills/recover/SKILL.md +28 -0
  275. package/skills/refine/SKILL.md +5 -4
  276. package/skills/spike/SKILL.md +1 -1
  277. package/skills/step/SKILL.md +4 -4
  278. package/skills/verify/SKILL.md +19 -14
  279. package/templates/intent.md +8 -7
@@ -0,0 +1,70 @@
1
+ // The gate's re-entrancy marker: which repos have a check run in flight,
2
+ // recorded on the environment so every process in the tree can see it.
3
+ //
4
+ // The failure it exists to stop is not slowness, it is unbounded recursion. A
5
+ // repo whose `check` setting is its own full pipeline (`pnpm check`) has a gate
6
+ // that runs its test suite; anything in that suite which gates THAT SAME REPO —
7
+ // a test driving `checkpoint` against the process cwd — spawns a second full
8
+ // pipeline, whose suite spawns a third. Each generation forks wider than the
9
+ // last, and nothing in the loop is aware there is a loop. The only thing that
10
+ // ended the first observed occurrence was checkride's 600s per-check cap
11
+ // killing one generation from the outside.
12
+ //
13
+ // It is scoped BY ROOT, and that is the whole design. A flat "a gate is
14
+ // running" flag is wrong in the one case that matters most: plumbbob's own
15
+ // suite legitimately gates dozens of fixture repos, and when the suite itself
16
+ // is running under this repo's gate it inherits the marker — so a flat flag
17
+ // refuses every one of them and turns a green suite red. Recursion is a repo
18
+ // re-entering ITS OWN gate; a different root is ordinary nested work and must
19
+ // pass through untouched.
20
+ //
21
+ // The marker rides the environment rather than a module-level variable because
22
+ // the recursion crosses process boundaries (spawned shells, checkride's tool
23
+ // subprocesses, vitest's worker forks), and a variable in one heap is invisible
24
+ // to all of them. Node builtins only.
25
+ import { resolve } from 'node:path';
26
+ /** The environment key. Namespaced so nothing else can set it by accident. */
27
+ const MARKER = 'PLUMBBOB_GATE_ROOTS';
28
+ /** The separator. A newline cannot appear in a path, unlike `:` on a weird one. */
29
+ const SEP = '\n';
30
+ /**
31
+ * The roots with a gate in flight, as absolute paths.
32
+ */
33
+ function activeRoots(env) {
34
+ const raw = env[MARKER] ?? '';
35
+ return raw.split(SEP).filter((r) => r.length > 0);
36
+ }
37
+ /**
38
+ * True when `root` already has a check gate running in this process tree.
39
+ *
40
+ * Compared as resolved absolute paths, so the same repo reached by a relative
41
+ * path, a symlink-free alias, or a trailing slash still counts as itself.
42
+ * `env` is injectable so the guard is testable without mutating the real one.
43
+ */
44
+ export function gateIsRunningFor(root, env = process.env) {
45
+ return activeRoots(env).includes(resolve(root));
46
+ }
47
+ /**
48
+ * Run `body` with `root` recorded as gated, restoring the prior value after.
49
+ *
50
+ * The marker is set on `process.env` itself, not on a copy handed to one child:
51
+ * the in-process checkride path spawns its own tool subprocesses, and they
52
+ * inherit the parent's environment at spawn time. `finally` restores rather
53
+ * than deletes, so nesting a second root inside the first cannot un-mark the
54
+ * first when the inner call returns.
55
+ */
56
+ export async function withGateMarker(root, body) {
57
+ const prior = process.env[MARKER];
58
+ process.env[MARKER] = [...activeRoots(process.env), resolve(root)].join(SEP);
59
+ try {
60
+ return await body();
61
+ }
62
+ finally {
63
+ if (prior === undefined) {
64
+ delete process.env[MARKER];
65
+ }
66
+ else {
67
+ process.env[MARKER] = prior;
68
+ }
69
+ }
70
+ }
@@ -103,12 +103,12 @@ async function run(cwd, args) {
103
103
  async function runOne(root, slug, step, spec) {
104
104
  const resolution = resolveAgent(root, spec.name, spec.flagPath !== undefined ? { flagPath: spec.flagPath } : {});
105
105
  if (!resolution.ok) {
106
- return degrade(spec.ambient, `plumbbob: ${resolution.error}`, `plumbbob: bound agent "${spec.name}" did not resolve — ${resolution.error} Skipping (D54 — the loop works without it).`);
106
+ return degrade(spec.ambient, `plumbbob: ${resolution.error}`, `plumbbob: bound agent "${spec.name}" did not resolve — ${resolution.error} Skipping — D54 (bindings-degrade-soft): the loop works without it.`);
107
107
  }
108
108
  const { manifest, dir } = resolution.agent;
109
109
  const resolved = resolveMode(spec.mode, manifest);
110
110
  if (!resolved.ok) {
111
- return degrade(spec.ambient, resolved.error, `plumbbob: bound agent "${spec.name}" — ${resolved.error.replace(/^plumbbob:\s*/, '')} Skipping (D54).`);
111
+ return degrade(spec.ambient, resolved.error, `plumbbob: bound agent "${spec.name}" — ${resolved.error.replace(/^plumbbob:\s*/, '')} Skipping — D54 (bindings-degrade-soft).`);
112
112
  }
113
113
  const mode = resolved.mode;
114
114
  let intent;
@@ -215,8 +215,9 @@ function degrade(ambient, hard, soft) {
215
215
  * agent name and the value flags.
216
216
  *
217
217
  * A value flag missing its value, or `--step` given a non-number, is a loud
218
- * error (returned as a string) rather than a silent default. Unknown `--flags`
219
- * are ignored — the point is the named agent.
218
+ * error (returned as a string) rather than a silent default. An undeclared
219
+ * `--flag` never arrives here — `run` screens argv against the verb spec in
220
+ * cli-core.ts and refuses before dispatch.
220
221
  */
221
222
  function parseRunArgs(args) {
222
223
  const positionals = [];
@@ -32,8 +32,12 @@ export async function check(cwd, args = []) {
32
32
  /**
33
33
  * Map argv onto checkride's flags: bare booleans plus comma-separated slot lists.
34
34
  *
35
- * Unknown args are ignored rather than refused — running the gate is the point,
36
- * not flag hygiene.
35
+ * Only the declared flags are read here; an undeclared one never reaches this
36
+ * function, because `run` screens argv against the verb spec in cli-core.ts and
37
+ * refuses before dispatch. This used to tolerate unknown args on the grounds
38
+ * that running the gate mattered more than flag hygiene — but the same tolerance
39
+ * on the mutating verbs let `checkpoint --help` commit, so the screen is now
40
+ * central and uniform.
37
41
  */
38
42
  function parseFlags(args) {
39
43
  return {
@@ -423,7 +423,7 @@ function agentReport(cwd) {
423
423
  const listings = listAgents(root);
424
424
  if (listings.length === 0)
425
425
  return { lines: [], failed: 0 };
426
- const lines = ['', 'plumbbob doctor — agents (D48)'];
426
+ const lines = ['', 'plumbbob doctor — agents — D48 (doctor-validates-agents)'];
427
427
  let failed = 0;
428
428
  for (const listing of listings) {
429
429
  const problem = agentProblem(listing, root);
@@ -451,7 +451,7 @@ async function gateReport(cwd, env) {
451
451
  const root = findRepoRoot(cwd);
452
452
  if (root === null)
453
453
  return { lines: [], failed: 0 };
454
- const lines = ['', 'plumbbob doctor — check gate (D32)'];
454
+ const lines = ['', 'plumbbob doctor — check gate — D32 (checkride-gate)'];
455
455
  const command = resolveString(root, 'check', '');
456
456
  if (command.length > 0) {
457
457
  lines.push(` ✓ gate: '${command}' — the "check" setting overrides checkride`);
@@ -523,13 +523,13 @@ function latchReport(cwd) {
523
523
  const line = turn === null
524
524
  ? ' ○ latch: dormant — guidance only (no turn ledger yet; it ticks on your first prompt when the UserPromptSubmit hook is wired — re-run after one to confirm)'
525
525
  : ` ✓ latch: live (turn ${turn})`;
526
- const lines = ['', 'plumbbob doctor — approval latch (D64)', line];
526
+ const lines = ['', 'plumbbob doctor — approval latch — D64 (approval-latch)', line];
527
527
  // The latch ignores a settings `auto`: a model can write that file, and a grant
528
528
  // the model can forge is no grant — self-approval comes only from the human's
529
529
  // typed `/build --auto`. Surface a set value so a human relying on it isn't
530
530
  // silently changed — informational, never a problem.
531
531
  if (resolveBoolean(root, 'auto', false)) {
532
- lines.push(' ○ auto: set in settings but not a grant since D67 — self-approve per run with `/plumbbob:build --auto`');
532
+ lines.push(' ○ auto: set in settings but not a grant since D67 (auto-not-a-grant) — self-approve per run with `/plumbbob:build --auto`');
533
533
  }
534
534
  return { lines, failed: 0 };
535
535
  }
@@ -35,7 +35,7 @@ export function finish(cwd, args = []) {
35
35
  }
36
36
  else {
37
37
  process.stderr.write('plumbbob: note — no report.md found; finishing without one ' +
38
- '(/plumbbob:finish normally writes the report first). No gate (D9).\n');
38
+ '(/plumbbob:finish normally writes the report first). No gate — D9 (finish-no-gate).\n');
39
39
  }
40
40
  // The final commit: stage the report just written plus the build folder's tail —
41
41
  // a step's commit sweeps its own bookkeeping along with the work, so the last
@@ -0,0 +1,297 @@
1
+ // `plumbbob recover` — read the control plane and say, plainly, whether it is
2
+ // telling the truth. Nothing else reads these files as a set: `doctor` checks
3
+ // the install and the gate, `status` reports what the markers claim without
4
+ // questioning them. So a session that crashed, lost its context window, or was
5
+ // switched away mid-step can sit in a state no verb detects — the worst being a
6
+ // cursor pointing at a build that no longer exists, which renders a perfectly
7
+ // plausible empty dashboard instead of an error.
8
+ //
9
+ // Diagnosis is free and repair is asked for by name (`--fix`), the same posture
10
+ // `doctor --migrate` takes. What `--fix` may touch is deliberately narrow: only
11
+ // the untracked control files this tool owns and can rewrite from what it
12
+ // already knows. It never touches a tracked artifact (intent, build-log,
13
+ // checkpoints, reports), never touches git, and never advances the loop — a
14
+ // recovery verb that could land a step would be a second, quieter checkpoint.
15
+ //
16
+ // Spike leftovers are reported and never removed: those worktrees live outside
17
+ // the repo root and may hold work nobody copied out, so naming them is help and
18
+ // deleting them is a gamble the human should take by hand.
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import { findRepoRoot } from "../lib/git.js";
21
+ import { parseSteps } from "../lib/orient.js";
22
+ import { activeBuild, checkpointsPath, clearHandoff, clearTick, grantPath, handoffPath, hasSession, intentPath, inSpike, listBuilds, readTurn, setActiveBuild, setGrant, stepPath, tickPath, } from "../lib/sidecar.js";
23
+ import { spikeBranches, spikeWorktrees } from "./spike.js";
24
+ /**
25
+ * Report every inconsistency in the control plane, and with `--fix` repair the
26
+ * ones that can be repaired without judgment.
27
+ *
28
+ * Exits 0 when the control plane is consistent (or when every problem found was
29
+ * fixed), 1 while any problem is still standing — so a scripted caller can tell
30
+ * "clean" from "you still have to look at this".
31
+ */
32
+ export function recover(cwd, args) {
33
+ const root = findRepoRoot(cwd);
34
+ if (root === null) {
35
+ process.stderr.write('plumbbob: not a git repository — recover reads a repo\'s control plane.\n');
36
+ return 1;
37
+ }
38
+ const fix = args.includes('--fix');
39
+ const findings = [...sessionFindings(root), ...spikeOrphanFindings(root)];
40
+ const lines = ['', 'plumbbob recover — control plane'];
41
+ let standing = 0;
42
+ for (const finding of findings) {
43
+ if (finding.ok) {
44
+ lines.push(` ✓ ${finding.name}: ${finding.detail}`);
45
+ continue;
46
+ }
47
+ if (fix && finding.repair !== undefined) {
48
+ finding.repair();
49
+ lines.push(` ✓ ${finding.name}: ${finding.repaired ?? finding.detail}`);
50
+ continue;
51
+ }
52
+ standing += 1;
53
+ lines.push(` ✗ ${finding.name}: ${finding.detail}`);
54
+ if (finding.hint !== undefined)
55
+ lines.push(` → ${finding.hint}`);
56
+ }
57
+ const fixable = findings.filter((f) => !f.ok && f.repair !== undefined).length;
58
+ lines.push('', summary(standing, fixable, fix), '');
59
+ process.stdout.write(lines.join('\n'));
60
+ return standing > 0 ? 1 : 0;
61
+ }
62
+ /**
63
+ * The closing line: what is still wrong, and whether `--fix` would help.
64
+ */
65
+ function summary(standing, fixable, fix) {
66
+ if (standing === 0) {
67
+ return fix ? 'control plane consistent — everything fixable was fixed.' : 'control plane consistent — nothing to recover.';
68
+ }
69
+ const problems = `${standing} problem${standing === 1 ? '' : 's'} still standing`;
70
+ if (fix || fixable === 0)
71
+ return `${problems} — each needs a call only you can make (see the → lines).`;
72
+ return `${problems}; ${fixable} of them recover --fix can repair. Nothing was changed.`;
73
+ }
74
+ /**
75
+ * Everything that depends on there being a session: the cursor, the phase
76
+ * markers, and the per-build control files.
77
+ *
78
+ * With no session there is no control plane to be wrong about — only the spike
79
+ * leftovers, which outlive `finish` and are checked separately.
80
+ */
81
+ function sessionFindings(root) {
82
+ if (!hasSession(root)) {
83
+ return [{ name: 'session', ok: true, detail: 'none open — nothing in flight to reconcile' }];
84
+ }
85
+ const slug = activeBuild(root);
86
+ const findings = [];
87
+ // The cursor first: everything below reads through it, so a cursor pointing
88
+ // nowhere makes the rest meaningless rather than merely wrong.
89
+ const resolvable = slug !== null && existsSync(intentPath(root, slug));
90
+ if (!resolvable) {
91
+ return [cursorFinding(root, slug)];
92
+ }
93
+ findings.push({ name: 'cursor', ok: true, detail: `build "${slug}" resolves` });
94
+ const stepInFlight = existsSync(stepPath(root, slug));
95
+ findings.push(...phaseFindings(root, slug, stepInFlight));
96
+ findings.push(...leftoverFindings(root, slug, stepInFlight));
97
+ return findings;
98
+ }
99
+ /**
100
+ * The cursor names a build that is not there.
101
+ *
102
+ * This is the quiet one: `status` takes the cursor at its word, so every read
103
+ * comes back empty and the dashboard renders as a fresh, untouched build rather
104
+ * than an error. Repairable only when exactly one real build remains — with
105
+ * several, which one you meant is a judgment call.
106
+ */
107
+ function cursorFinding(root, slug) {
108
+ const named = slug === null ? 'nothing' : `"${slug}"`;
109
+ const builds = listBuilds(root).filter((b) => existsSync(intentPath(root, b)));
110
+ const detail = `points at ${named}, which has no intent.md — status will render an empty dashboard rather than refuse`;
111
+ if (builds.length === 1) {
112
+ const only = builds[0];
113
+ return {
114
+ name: 'cursor',
115
+ ok: false,
116
+ detail,
117
+ hint: `fix: plumbbob recover --fix (re-points the cursor at "${only}", the only build left) — or plumbbob use <slug>`,
118
+ repair: () => setActiveBuild(root, only),
119
+ repaired: `re-pointed at "${only}"`,
120
+ };
121
+ }
122
+ const hint = builds.length === 0
123
+ ? 'no build folder survives here — plumbbob finish closes the session, or start a new one'
124
+ : `pick the one you meant: plumbbob use <slug>. Builds: ${builds.join(', ')}`;
125
+ return { name: 'cursor', ok: false, detail, hint };
126
+ }
127
+ /**
128
+ * Contradictions in what phase the build claims to be in.
129
+ *
130
+ * Phase is derived from the markers rather than stored, so two markers at once
131
+ * is not an impossible state — it is an unreadable one, and `status` resolves
132
+ * it by showing the spike and hiding the step entirely.
133
+ */
134
+ function phaseFindings(root, slug, stepInFlight) {
135
+ const findings = [];
136
+ const spiking = inSpike(root, slug);
137
+ if (spiking && stepInFlight) {
138
+ findings.push({
139
+ name: 'phase',
140
+ ok: false,
141
+ detail: `both a spike and step ${readMarker(stepPath(root, slug))} are marked in flight — status shows the spike and hides the step`,
142
+ hint: 'close the spike first (plumbbob spike done), which returns you to the step still in flight',
143
+ });
144
+ }
145
+ else {
146
+ findings.push({
147
+ name: 'phase',
148
+ ok: true,
149
+ detail: spiking ? 'in a spike' : stepInFlight ? `step ${readMarker(stepPath(root, slug))} in flight` : 'at the boundary',
150
+ });
151
+ }
152
+ // A STEP the plan no longer contains: `refine` can rewrite `## Steps` under a
153
+ // step that is already in flight, and nothing reconciles the marker with it.
154
+ const marker = Number(readMarker(stepPath(root, slug)));
155
+ if (stepInFlight && Number.isInteger(marker) && marker > 0) {
156
+ const planned = parseSteps(readOr(intentPath(root, slug))).map((s) => s.n);
157
+ if (planned.length > 0 && !planned.includes(marker)) {
158
+ findings.push({
159
+ name: 'step marker',
160
+ ok: false,
161
+ detail: `step ${marker} is in flight but the plan has no step ${marker} (planned: ${planned.join(', ')})`,
162
+ hint: 'the plan was rewritten under the step — sharpen it with /plumbbob:step, or plumbbob revert to drop the step',
163
+ });
164
+ }
165
+ }
166
+ return findings;
167
+ }
168
+ /**
169
+ * Control files left behind by a verb that did not get to clean them up.
170
+ *
171
+ * Each of these fails quietly rather than loudly, which is why they are worth a
172
+ * verb: an orphaned handoff ledger feeds a finished step's agent output into
173
+ * the next one, and a stale TICK arms the approval latch against a span that
174
+ * already closed.
175
+ */
176
+ function leftoverFindings(root, slug, stepInFlight) {
177
+ const findings = [];
178
+ // The handoff ledger is step-scoped and cleared by `checkpoint` alone — a
179
+ // revert or an abandoned step leaves it for the next step to inherit.
180
+ const handoffs = handoffCount(root, slug);
181
+ if (!stepInFlight && handoffs > 0) {
182
+ findings.push({
183
+ name: 'handoff ledger',
184
+ ok: false,
185
+ detail: `${handoffs} agent envelope${handoffs === 1 ? '' : 's'} left over at the boundary — the next step would thread them into its context`,
186
+ hint: 'fix: plumbbob recover --fix (clears the step-scoped ledger)',
187
+ repair: () => clearHandoff(root, slug),
188
+ repaired: `cleared ${handoffs} leftover envelope${handoffs === 1 ? '' : 's'}`,
189
+ });
190
+ }
191
+ else {
192
+ findings.push({ name: 'handoff ledger', ok: true, detail: stepInFlight ? 'in scope for the step in flight' : 'empty' });
193
+ }
194
+ // TICK is stamped when a step is entered and when `start` opens the plan
195
+ // pause, and cleared by the checkpoint that lands either one. At the boundary
196
+ // with the plan already committed, neither pause is open, so a TICK left here
197
+ // is measuring a span that closed.
198
+ const planLanded = readOr(checkpointsPath(root, slug)).split('\n').some((l) => l.startsWith('plan '));
199
+ if (!stepInFlight && planLanded && existsSync(tickPath(root, slug))) {
200
+ findings.push({
201
+ name: 'latch tick',
202
+ ok: false,
203
+ detail: 'a TICK stamp survives at the boundary — the approval latch is armed against a pause that already closed',
204
+ hint: 'fix: plumbbob recover --fix (clears the stale stamp; the latch re-arms on the next build)',
205
+ repair: () => clearTick(root, slug),
206
+ repaired: 'cleared the stale stamp',
207
+ });
208
+ }
209
+ else {
210
+ findings.push({ name: 'latch tick', ok: true, detail: stepInFlight ? 'stamped for the step in flight' : 'clear' });
211
+ }
212
+ // GRANT is rewritten on every turn tick, so it self-clears — but only while
213
+ // the hook runs. Without a TURN ledger nothing will ever clear it, and the
214
+ // latch reads a standing grant as self-approval.
215
+ if (existsSync(grantPath(root)) && readTurn(root) === null) {
216
+ findings.push({
217
+ name: 'self-approval grant',
218
+ ok: false,
219
+ detail: 'a GRANT survives with no turn ledger to clear it — the latch would honor it as self-approval',
220
+ hint: 'fix: plumbbob recover --fix (drops the grant; type /plumbbob:build --auto to mint a fresh one)',
221
+ repair: () => setGrant(root, null),
222
+ repaired: 'dropped the stranded grant',
223
+ });
224
+ }
225
+ return findings;
226
+ }
227
+ /**
228
+ * Spike worktrees and branches with no open spike to own them.
229
+ *
230
+ * `spike done` is the only cleanup and it refuses without the marker, so a
231
+ * spike interrupted by `finish`, a cursor switch, or a half-failed open strands
232
+ * these where no verb can reach them. Reported with the exact commands and
233
+ * never removed: the directories sit outside the repo and may hold the only
234
+ * copy of what the spike learned.
235
+ */
236
+ function spikeOrphanFindings(root) {
237
+ if (inSpike(root, activeBuild(root))) {
238
+ return [{ name: 'spike leftovers', ok: true, detail: 'a spike is open — plumbbob spike done owns its cleanup' }];
239
+ }
240
+ const worktrees = spikeWorktrees(root);
241
+ const branches = spikeBranches(root);
242
+ if (worktrees.length === 0 && branches.length === 0) {
243
+ return [{ name: 'spike leftovers', ok: true, detail: 'none' }];
244
+ }
245
+ const parts = [
246
+ worktrees.length > 0 ? `${worktrees.length} worktree${worktrees.length === 1 ? '' : 's'} (${worktrees.join(', ')})` : '',
247
+ branches.length > 0 ? `${branches.length} branch${branches.length === 1 ? '' : 'es'} (${branches.join(', ')})` : '',
248
+ ].filter((p) => p.length > 0);
249
+ const commands = [
250
+ ...worktrees.map((path) => `git worktree remove --force ${path}`),
251
+ ...(branches.length > 0 ? [`git branch -D ${branches.join(' ')}`] : []),
252
+ ];
253
+ return [
254
+ {
255
+ name: 'spike leftovers',
256
+ ok: false,
257
+ detail: `${parts.join(' and ')} survive with no spike open — plumbbob spike done cannot reach them`,
258
+ hint: `remove by hand once you have salvaged anything you want: ${commands.join(' && ')}`,
259
+ },
260
+ ];
261
+ }
262
+ /**
263
+ * How many agent envelopes the step-scoped handoff ledger holds; 0 when it is
264
+ * absent or unreadable.
265
+ */
266
+ function handoffCount(root, slug) {
267
+ const raw = readOr(handoffPath(root, slug));
268
+ if (raw.trim().length === 0)
269
+ return 0;
270
+ try {
271
+ const parsed = JSON.parse(raw);
272
+ return Array.isArray(parsed) ? parsed.length : 1;
273
+ }
274
+ catch {
275
+ return 1;
276
+ }
277
+ }
278
+ /**
279
+ * A control marker's trimmed content, or '?' when it cannot be read — a marker
280
+ * exists to be reported on, so an unreadable one must not throw mid-report.
281
+ */
282
+ function readMarker(path) {
283
+ const raw = readOr(path).trim();
284
+ return raw.length > 0 ? raw : '?';
285
+ }
286
+ /**
287
+ * File content, or '' when absent — recover reads state that may be missing by
288
+ * definition, so every read degrades rather than throws.
289
+ */
290
+ function readOr(path) {
291
+ try {
292
+ return readFileSync(path, 'utf8');
293
+ }
294
+ catch {
295
+ return '';
296
+ }
297
+ }
@@ -95,8 +95,8 @@ export function revert(cwd, args) {
95
95
  *
96
96
  * The match is by name alone, and since the skills dropped their `pb-` prefix
97
97
  * those names are ordinary words — a user's own `.claude/skills/plan/` is
98
- * carried across the reset too. That errs toward keeping work (C4), so it is
99
- * the safe direction to be wrong in. Only paths that currently exist are returned.
98
+ * carried across the reset too. That errs toward keeping work — never destroy —
99
+ * so it is the safe direction to be wrong in. Only paths that currently exist are returned.
100
100
  */
101
101
  function plumbbobOwnedPaths(root) {
102
102
  const paths = [sidecarDir(root)];
@@ -201,8 +201,12 @@ function spikeReportsMissingVerdict(root, buildSlug) {
201
201
  * Worktree paths whose checked-out branch is under spike/ — parsed from the
202
202
  * porcelain output (blank-line-separated `worktree <path>` / `branch <ref>`
203
203
  * blocks).
204
+ *
205
+ * Exported for `recover`, which reports the ones this verb can no longer reach
206
+ * (a spike interrupted before `spike done` strands them). Sharing the reader
207
+ * keeps one definition of what counts as a spike worktree.
204
208
  */
205
- function spikeWorktrees(root) {
209
+ export function spikeWorktrees(root) {
206
210
  const out = git(root, ['worktree', 'list', '--porcelain']);
207
211
  const paths = [];
208
212
  let current = null;
@@ -218,8 +222,10 @@ function spikeWorktrees(root) {
218
222
  }
219
223
  /**
220
224
  * List the local `spike/<slug>` branch names.
225
+ *
226
+ * Exported alongside spikeWorktrees, for the same reason.
221
227
  */
222
- function spikeBranches(root) {
228
+ export function spikeBranches(root) {
223
229
  const out = git(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/spike/']);
224
230
  return out.length === 0 ? [] : out.split('\n').filter((b) => b.length > 0);
225
231
  }
@@ -1,12 +1,12 @@
1
1
  #!/bin/sh
2
- # post-edit.sh — light feedback (D25). PostToolUse, non-blocking, ALWAYS exits 0.
2
+ # post-edit.sh — light feedback — D25 (light-then-heavy). PostToolUse, non-blocking, ALWAYS exits 0.
3
3
  # Runs file-scoped oxlint + ast-grep on the changed file and injects any failures
4
- # into the model's context via additionalContext (verified API, D25). No-ops when
4
+ # into the model's context via additionalContext (verified API — D25 (light-then-heavy)). No-ops when
5
5
  # the tools are absent or there is no session. tsc is deferred to the heavy tier
6
- # (D25): it has no true single-file mode and would tax every keystroke.
6
+ # — D25 (light-then-heavy): it has no true single-file mode and would tax every keystroke.
7
7
 
8
8
  # The repo root is the nearest ancestor whose .plumbbob/STATE is NON-EMPTY — the
9
- # per-worktree signal that a tracked build is live here (D28). STATE's existence
9
+ # per-worktree signal that a tracked build is live here — D28 (state-cursor). STATE's existence
10
10
  # means "a session is active"; its content is the active-build cursor, so a non-empty
11
11
  # STATE is exactly "a tracked build is live" (the `--local` layout leaves STATE empty,
12
12
  # and this file-scoped check is a tracked-build affordance). `-s` tests both existence
@@ -1,9 +1,9 @@
1
1
  #!/bin/sh
2
- # pre-bash-commit.sh — the git-commit ask-hook (D66). PreToolUse on Bash: when a
2
+ # pre-bash-commit.sh — the git-commit ask-hook — D66 (oob-commits-surfaced). PreToolUse on Bash: when a
3
3
  # step is in flight and the model reaches for a raw `git commit`, turn that commit
4
4
  # into a permission *question*, never a wall. checkpoint owns the landing (the latch
5
5
  # lives there); this only nudges the human to route through it. ALWAYS exits 0 and
6
- # never `deny`s — the human decides, C5 stays intact (D66). `plumbbob checkpoint`'s
6
+ # never `deny`s — the human decides, C5 (additive-git) stays intact — D66 (oob-commits-surfaced). `plumbbob checkpoint`'s
7
7
  # own internal `git commit` spawn never passes through hooks, so nothing self-trips.
8
8
 
9
9
  # The repo root is the nearest ancestor with an active session — `.plumbbob/STATE`,
@@ -27,7 +27,7 @@ root=$(find_root) || exit 0 # no active session here: nothing to guard
27
27
 
28
28
  # The in-flight signal is STEP (cleared when the step checkpoints): inside the
29
29
  # active build's folder when STATE names one, else the flat sidecar STEP of the
30
- # `--local` layout. STATE's content IS the cursor (D28) — read the first line, trim
30
+ # `--local` layout. STATE's content IS the cursor — D28 (state-cursor) — read the first line, trim
31
31
  # whitespace, and treat the legacy `active` sentinel (pre-STATE-cursor) as no cursor.
32
32
  slug=$(head -n1 "$root/.plumbbob/STATE" 2>/dev/null | tr -d '[:space:]')
33
33
  [ "$slug" = "active" ] && slug=""
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "checkride",
3
+ "displayName": "checkride",
4
+ "version": "0.10.2",
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
+ "author": { "name": "Rob McLarty", "email": "hello@robmclarty.com", "url": "https://robmclarty.com" },
7
+ "homepage": "https://robmclarty.github.io/checkride/",
8
+ "repository": "https://github.com/robmclarty/checkride",
9
+ "license": "Apache-2.0",
10
+ "keywords": ["claude-code", "claude", "agent", "checkride", "ci", "triage"]
11
+ }