plumbbob 0.9.3 → 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 (320) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +92 -89
  3. package/dist/cli-core.js +25 -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 +55 -22
  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 +21 -20
  13. package/dist/lib/latch.js +19 -19
  14. package/dist/lib/orient.js +36 -17
  15. package/dist/lib/reentry.js +70 -0
  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 +27 -27
  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 +31 -30
  24. package/dist/verbs/finish.js +28 -41
  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 +297 -0
  29. package/dist/verbs/revert.js +13 -13
  30. package/dist/verbs/spike.js +25 -19
  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/hooks/post-edit.sh +4 -4
  36. package/hooks/pre-bash-commit.sh +3 -3
  37. package/node_modules/checkride/.claude-plugin/plugin.json +11 -0
  38. package/node_modules/checkride/CHANGELOG.md +1404 -0
  39. package/node_modules/checkride/LICENSE +201 -21
  40. package/node_modules/checkride/README.md +197 -38
  41. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts +15 -0
  42. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.d.ts.map +1 -0
  43. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js +17 -0
  44. package/node_modules/checkride/dist/__tests__/fixtures/released-stanzas.js.map +1 -0
  45. package/node_modules/checkride/dist/adapters.d.ts +30 -10
  46. package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
  47. package/node_modules/checkride/dist/adapters.js +62 -23
  48. package/node_modules/checkride/dist/adapters.js.map +1 -1
  49. package/node_modules/checkride/dist/agent-setup/claude.d.ts +88 -0
  50. package/node_modules/checkride/dist/agent-setup/claude.d.ts.map +1 -0
  51. package/node_modules/checkride/dist/agent-setup/claude.js +305 -0
  52. package/node_modules/checkride/dist/agent-setup/claude.js.map +1 -0
  53. package/node_modules/checkride/dist/agent-setup/command.d.ts +43 -0
  54. package/node_modules/checkride/dist/agent-setup/command.d.ts.map +1 -0
  55. package/node_modules/checkride/dist/agent-setup/command.js +77 -0
  56. package/node_modules/checkride/dist/agent-setup/command.js.map +1 -0
  57. package/node_modules/checkride/dist/agent-setup/cursor.d.ts +69 -0
  58. package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -0
  59. package/node_modules/checkride/dist/agent-setup/cursor.js +247 -0
  60. package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -0
  61. package/node_modules/checkride/dist/agent-setup/files.d.ts +50 -0
  62. package/node_modules/checkride/dist/agent-setup/files.d.ts.map +1 -0
  63. package/node_modules/checkride/dist/agent-setup/files.js +80 -0
  64. package/node_modules/checkride/dist/agent-setup/files.js.map +1 -0
  65. package/node_modules/checkride/dist/agent-setup/harness.d.ts +101 -0
  66. package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -0
  67. package/node_modules/checkride/dist/agent-setup/harness.js +93 -0
  68. package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -0
  69. package/node_modules/checkride/dist/agent-setup/hook.d.ts +45 -52
  70. package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -1
  71. package/node_modules/checkride/dist/agent-setup/hook.js +64 -95
  72. package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -1
  73. package/node_modules/checkride/dist/agent-setup/index.d.ts +10 -4
  74. package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -1
  75. package/node_modules/checkride/dist/agent-setup/index.js +9 -4
  76. package/node_modules/checkride/dist/agent-setup/index.js.map +1 -1
  77. package/node_modules/checkride/dist/agent-setup/scripts.d.ts +81 -0
  78. package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -0
  79. package/node_modules/checkride/dist/agent-setup/scripts.js +604 -0
  80. package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -0
  81. package/node_modules/checkride/dist/agent-setup/skills.d.ts +31 -0
  82. package/node_modules/checkride/dist/agent-setup/skills.d.ts.map +1 -0
  83. package/node_modules/checkride/dist/agent-setup/skills.js +56 -0
  84. package/node_modules/checkride/dist/agent-setup/skills.js.map +1 -0
  85. package/node_modules/checkride/dist/artifacts/configured.d.ts +30 -0
  86. package/node_modules/checkride/dist/artifacts/configured.d.ts.map +1 -0
  87. package/node_modules/checkride/dist/artifacts/configured.js +45 -0
  88. package/node_modules/checkride/dist/artifacts/configured.js.map +1 -0
  89. package/node_modules/checkride/dist/artifacts/format.d.ts +33 -0
  90. package/node_modules/checkride/dist/artifacts/format.d.ts.map +1 -0
  91. package/node_modules/checkride/dist/artifacts/format.js +56 -0
  92. package/node_modules/checkride/dist/artifacts/format.js.map +1 -0
  93. package/node_modules/checkride/dist/artifacts/freshness.d.ts +32 -0
  94. package/node_modules/checkride/dist/artifacts/freshness.d.ts.map +1 -0
  95. package/node_modules/checkride/dist/artifacts/freshness.js +37 -0
  96. package/node_modules/checkride/dist/artifacts/freshness.js.map +1 -0
  97. package/node_modules/checkride/dist/artifacts/index.d.ts +22 -0
  98. package/node_modules/checkride/dist/artifacts/index.d.ts.map +1 -0
  99. package/node_modules/checkride/dist/artifacts/index.js +19 -0
  100. package/node_modules/checkride/dist/artifacts/index.js.map +1 -0
  101. package/node_modules/checkride/dist/artifacts/json.d.ts +31 -0
  102. package/node_modules/checkride/dist/artifacts/json.d.ts.map +1 -0
  103. package/node_modules/checkride/dist/artifacts/json.js +47 -0
  104. package/node_modules/checkride/dist/artifacts/json.js.map +1 -0
  105. package/node_modules/checkride/dist/artifacts/raw.d.ts +60 -0
  106. package/node_modules/checkride/dist/artifacts/raw.d.ts.map +1 -0
  107. package/node_modules/checkride/dist/artifacts/raw.js +91 -0
  108. package/node_modules/checkride/dist/artifacts/raw.js.map +1 -0
  109. package/node_modules/checkride/dist/artifacts/summary.d.ts +69 -0
  110. package/node_modules/checkride/dist/artifacts/summary.d.ts.map +1 -0
  111. package/node_modules/checkride/dist/artifacts/summary.js +108 -0
  112. package/node_modules/checkride/dist/artifacts/summary.js.map +1 -0
  113. package/node_modules/checkride/dist/atomic.d.ts +16 -1
  114. package/node_modules/checkride/dist/atomic.d.ts.map +1 -1
  115. package/node_modules/checkride/dist/atomic.js +25 -3
  116. package/node_modules/checkride/dist/atomic.js.map +1 -1
  117. package/node_modules/checkride/dist/baseline/fallow.d.ts +9 -5
  118. package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -1
  119. package/node_modules/checkride/dist/baseline/fallow.js +163 -60
  120. package/node_modules/checkride/dist/baseline/fallow.js.map +1 -1
  121. package/node_modules/checkride/dist/baseline/fingerprint.d.ts +20 -11
  122. package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
  123. package/node_modules/checkride/dist/baseline/fingerprint.js +73 -23
  124. package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
  125. package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
  126. package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
  127. package/node_modules/checkride/dist/baseline/history.js +232 -0
  128. package/node_modules/checkride/dist/baseline/history.js.map +1 -0
  129. package/node_modules/checkride/dist/baseline/index.d.ts +7 -5
  130. package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
  131. package/node_modules/checkride/dist/baseline/index.js +5 -4
  132. package/node_modules/checkride/dist/baseline/index.js.map +1 -1
  133. package/node_modules/checkride/dist/baseline/store.d.ts +41 -10
  134. package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
  135. package/node_modules/checkride/dist/baseline/store.js +65 -24
  136. package/node_modules/checkride/dist/baseline/store.js.map +1 -1
  137. package/node_modules/checkride/dist/baseline-command.d.ts +5 -5
  138. package/node_modules/checkride/dist/baseline-command.js +7 -7
  139. package/node_modules/checkride/dist/baseline-command.js.map +1 -1
  140. package/node_modules/checkride/dist/cli.d.ts +21 -4
  141. package/node_modules/checkride/dist/cli.d.ts.map +1 -1
  142. package/node_modules/checkride/dist/cli.js +350 -36
  143. package/node_modules/checkride/dist/cli.js.map +1 -1
  144. package/node_modules/checkride/dist/config.d.ts +114 -9
  145. package/node_modules/checkride/dist/config.d.ts.map +1 -1
  146. package/node_modules/checkride/dist/config.js +142 -23
  147. package/node_modules/checkride/dist/config.js.map +1 -1
  148. package/node_modules/checkride/dist/digest/digest.d.ts +5 -5
  149. package/node_modules/checkride/dist/digest/digest.js +6 -6
  150. package/node_modules/checkride/dist/digest/index.d.ts +2 -2
  151. package/node_modules/checkride/dist/digest/index.js +2 -2
  152. package/node_modules/checkride/dist/doctor.d.ts +27 -6
  153. package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
  154. package/node_modules/checkride/dist/doctor.js +231 -27
  155. package/node_modules/checkride/dist/doctor.js.map +1 -1
  156. package/node_modules/checkride/dist/gate.d.ts +180 -0
  157. package/node_modules/checkride/dist/gate.d.ts.map +1 -0
  158. package/node_modules/checkride/dist/gate.js +617 -0
  159. package/node_modules/checkride/dist/gate.js.map +1 -0
  160. package/node_modules/checkride/dist/init.d.ts +79 -12
  161. package/node_modules/checkride/dist/init.d.ts.map +1 -1
  162. package/node_modules/checkride/dist/init.js +385 -81
  163. package/node_modules/checkride/dist/init.js.map +1 -1
  164. package/node_modules/checkride/dist/json.d.ts +21 -0
  165. package/node_modules/checkride/dist/json.d.ts.map +1 -0
  166. package/node_modules/checkride/dist/json.js +21 -0
  167. package/node_modules/checkride/dist/json.js.map +1 -0
  168. package/node_modules/checkride/dist/links.d.ts +31 -5
  169. package/node_modules/checkride/dist/links.d.ts.map +1 -1
  170. package/node_modules/checkride/dist/links.js +141 -32
  171. package/node_modules/checkride/dist/links.js.map +1 -1
  172. package/node_modules/checkride/dist/node-pin.d.ts +129 -0
  173. package/node_modules/checkride/dist/node-pin.d.ts.map +1 -0
  174. package/node_modules/checkride/dist/node-pin.js +211 -0
  175. package/node_modules/checkride/dist/node-pin.js.map +1 -0
  176. package/node_modules/checkride/dist/orchestrator.d.ts +53 -10
  177. package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
  178. package/node_modules/checkride/dist/orchestrator.js +312 -113
  179. package/node_modules/checkride/dist/orchestrator.js.map +1 -1
  180. package/node_modules/checkride/dist/pack.d.ts +8 -9
  181. package/node_modules/checkride/dist/pack.d.ts.map +1 -1
  182. package/node_modules/checkride/dist/pack.js +53 -17
  183. package/node_modules/checkride/dist/pack.js.map +1 -1
  184. package/node_modules/checkride/dist/pm/detect.d.ts +6 -1
  185. package/node_modules/checkride/dist/pm/detect.d.ts.map +1 -1
  186. package/node_modules/checkride/dist/pm/detect.js +6 -1
  187. package/node_modules/checkride/dist/pm/detect.js.map +1 -1
  188. package/node_modules/checkride/dist/pm/index.d.ts +7 -2
  189. package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
  190. package/node_modules/checkride/dist/pm/index.js +6 -2
  191. package/node_modules/checkride/dist/pm/index.js.map +1 -1
  192. package/node_modules/checkride/dist/pm/launch.d.ts +72 -0
  193. package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -0
  194. package/node_modules/checkride/dist/pm/launch.js +108 -0
  195. package/node_modules/checkride/dist/pm/launch.js.map +1 -0
  196. package/node_modules/checkride/dist/pm/tools.d.ts +73 -0
  197. package/node_modules/checkride/dist/pm/tools.d.ts.map +1 -0
  198. package/node_modules/checkride/dist/pm/tools.js +124 -0
  199. package/node_modules/checkride/dist/pm/tools.js.map +1 -0
  200. package/node_modules/checkride/dist/pm/translate.d.ts +35 -9
  201. package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -1
  202. package/node_modules/checkride/dist/pm/translate.js +78 -18
  203. package/node_modules/checkride/dist/pm/translate.js.map +1 -1
  204. package/node_modules/checkride/dist/proc.d.ts +30 -0
  205. package/node_modules/checkride/dist/proc.d.ts.map +1 -0
  206. package/node_modules/checkride/dist/proc.js +42 -0
  207. package/node_modules/checkride/dist/proc.js.map +1 -0
  208. package/node_modules/checkride/dist/qa/cli.d.ts +20 -0
  209. package/node_modules/checkride/dist/qa/cli.d.ts.map +1 -0
  210. package/node_modules/checkride/dist/qa/cli.js +24 -0
  211. package/node_modules/checkride/dist/qa/cli.js.map +1 -0
  212. package/node_modules/checkride/dist/qa/dead.d.ts +39 -0
  213. package/node_modules/checkride/dist/qa/dead.d.ts.map +1 -0
  214. package/node_modules/checkride/dist/qa/dead.js +88 -0
  215. package/node_modules/checkride/dist/qa/dead.js.map +1 -0
  216. package/node_modules/checkride/dist/qa/dupes.d.ts +45 -0
  217. package/node_modules/checkride/dist/qa/dupes.d.ts.map +1 -0
  218. package/node_modules/checkride/dist/qa/dupes.js +62 -0
  219. package/node_modules/checkride/dist/qa/dupes.js.map +1 -0
  220. package/node_modules/checkride/dist/qa/health.d.ts +85 -0
  221. package/node_modules/checkride/dist/qa/health.d.ts.map +1 -0
  222. package/node_modules/checkride/dist/qa/health.js +109 -0
  223. package/node_modules/checkride/dist/qa/health.js.map +1 -0
  224. package/node_modules/checkride/dist/qa/index.d.ts +23 -0
  225. package/node_modules/checkride/dist/qa/index.d.ts.map +1 -0
  226. package/node_modules/checkride/dist/qa/index.js +22 -0
  227. package/node_modules/checkride/dist/qa/index.js.map +1 -0
  228. package/node_modules/checkride/dist/qa/mutation.d.ts +80 -0
  229. package/node_modules/checkride/dist/qa/mutation.d.ts.map +1 -0
  230. package/node_modules/checkride/dist/qa/mutation.js +195 -0
  231. package/node_modules/checkride/dist/qa/mutation.js.map +1 -0
  232. package/node_modules/checkride/dist/qa/qa.d.ts +71 -0
  233. package/node_modules/checkride/dist/qa/qa.d.ts.map +1 -0
  234. package/node_modules/checkride/dist/qa/qa.js +158 -0
  235. package/node_modules/checkride/dist/qa/qa.js.map +1 -0
  236. package/node_modules/checkride/dist/qa/read.d.ts +40 -0
  237. package/node_modules/checkride/dist/qa/read.d.ts.map +1 -0
  238. package/node_modules/checkride/dist/qa/read.js +47 -0
  239. package/node_modules/checkride/dist/qa/read.js.map +1 -0
  240. package/node_modules/checkride/dist/qa/render.d.ts +26 -0
  241. package/node_modules/checkride/dist/qa/render.d.ts.map +1 -0
  242. package/node_modules/checkride/dist/qa/render.js +310 -0
  243. package/node_modules/checkride/dist/qa/render.js.map +1 -0
  244. package/node_modules/checkride/dist/recover.d.ts +48 -0
  245. package/node_modules/checkride/dist/recover.d.ts.map +1 -0
  246. package/node_modules/checkride/dist/recover.js +220 -0
  247. package/node_modules/checkride/dist/recover.js.map +1 -0
  248. package/node_modules/checkride/dist/security.d.ts +45 -0
  249. package/node_modules/checkride/dist/security.d.ts.map +1 -0
  250. package/node_modules/checkride/dist/security.js +86 -0
  251. package/node_modules/checkride/dist/security.js.map +1 -0
  252. package/node_modules/checkride/dist/smoke.d.ts +6 -6
  253. package/node_modules/checkride/dist/smoke.js +6 -6
  254. package/node_modules/checkride/dist/snippets.d.ts +27 -30
  255. package/node_modules/checkride/dist/snippets.d.ts.map +1 -1
  256. package/node_modules/checkride/dist/snippets.js +26 -30
  257. package/node_modules/checkride/dist/snippets.js.map +1 -1
  258. package/node_modules/checkride/dist/tool-json.d.ts +33 -0
  259. package/node_modules/checkride/dist/tool-json.d.ts.map +1 -0
  260. package/node_modules/checkride/dist/tool-json.js +58 -0
  261. package/node_modules/checkride/dist/tool-json.js.map +1 -0
  262. package/node_modules/checkride/dist/triage/cli.d.ts +21 -0
  263. package/node_modules/checkride/dist/triage/cli.d.ts.map +1 -0
  264. package/node_modules/checkride/dist/triage/cli.js +26 -0
  265. package/node_modules/checkride/dist/triage/cli.js.map +1 -0
  266. package/node_modules/checkride/dist/triage/doctor-fold.d.ts +51 -0
  267. package/node_modules/checkride/dist/triage/doctor-fold.d.ts.map +1 -0
  268. package/node_modules/checkride/dist/triage/doctor-fold.js +88 -0
  269. package/node_modules/checkride/dist/triage/doctor-fold.js.map +1 -0
  270. package/node_modules/checkride/dist/triage/env.d.ts +54 -0
  271. package/node_modules/checkride/dist/triage/env.d.ts.map +1 -0
  272. package/node_modules/checkride/dist/triage/env.js +81 -0
  273. package/node_modules/checkride/dist/triage/env.js.map +1 -0
  274. package/node_modules/checkride/dist/triage/gate.d.ts +61 -0
  275. package/node_modules/checkride/dist/triage/gate.d.ts.map +1 -0
  276. package/node_modules/checkride/dist/triage/gate.js +121 -0
  277. package/node_modules/checkride/dist/triage/gate.js.map +1 -0
  278. package/node_modules/checkride/dist/triage/index.d.ts +22 -0
  279. package/node_modules/checkride/dist/triage/index.d.ts.map +1 -0
  280. package/node_modules/checkride/dist/triage/index.js +20 -0
  281. package/node_modules/checkride/dist/triage/index.js.map +1 -0
  282. package/node_modules/checkride/dist/triage/render.d.ts +19 -0
  283. package/node_modules/checkride/dist/triage/render.d.ts.map +1 -0
  284. package/node_modules/checkride/dist/triage/render.js +419 -0
  285. package/node_modules/checkride/dist/triage/render.js.map +1 -0
  286. package/node_modules/checkride/dist/triage/triage.d.ts +83 -0
  287. package/node_modules/checkride/dist/triage/triage.d.ts.map +1 -0
  288. package/node_modules/checkride/dist/triage/triage.js +131 -0
  289. package/node_modules/checkride/dist/triage/triage.js.map +1 -0
  290. package/node_modules/checkride/package.json +18 -15
  291. package/node_modules/checkride/schema/checkride.config.schema.json +70 -5
  292. package/node_modules/checkride/schema/checkride.summary.schema.json +1 -1
  293. package/node_modules/checkride/skills/check/SKILL.md +265 -0
  294. package/node_modules/checkride/skills/qa/SKILL.md +231 -0
  295. package/node_modules/checkride/templates/shared/cspell.json +1 -0
  296. package/node_modules/checkride/templates/shared/oxlintrc.json +1 -1
  297. package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
  298. package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
  299. package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
  300. package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
  301. package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
  302. package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
  303. package/node_modules/checkride/templates/shared/vale.ini +34 -0
  304. package/package.json +10 -9
  305. package/skills/build/SKILL.md +85 -76
  306. package/skills/doctor/SKILL.md +11 -11
  307. package/skills/finish/SKILL.md +21 -18
  308. package/skills/harvest/SKILL.md +13 -11
  309. package/skills/park/SKILL.md +10 -10
  310. package/skills/plan/SKILL.md +69 -59
  311. package/skills/recover/SKILL.md +28 -0
  312. package/skills/refine/SKILL.md +21 -18
  313. package/skills/revert/SKILL.md +6 -6
  314. package/skills/spike/SKILL.md +9 -9
  315. package/skills/status/SKILL.md +5 -5
  316. package/skills/step/SKILL.md +20 -15
  317. package/skills/verify/SKILL.md +56 -46
  318. package/templates/build-log.md +7 -7
  319. package/templates/intent.md +52 -41
  320. package/templates/spike-report.md +10 -10
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Orchestrator — behavior-ported from the interim `scripts/check.mjs`.
2
+ * The check orchestrator.
3
3
  *
4
4
  * Resolves slots to adapters, selects which to run (flags), spawns each command
5
5
  * (or runs a built-in), captures raw output to `.check/`, and writes the
@@ -7,22 +7,26 @@
7
7
  * diagnostics — agents read the per-tool JSON directly.
8
8
  */
9
9
  import { spawn } from 'node:child_process';
10
+ import { readdirSync } from 'node:fs';
10
11
  import { mkdir, rm } from 'node:fs/promises';
11
12
  import { cpus } from 'node:os';
12
13
  import { join } from 'node:path';
13
14
  import { performance } from 'node:perf_hooks';
14
15
  import { ADAPTERS, SCHEMA_VERSION, SLOTS } from './adapters.js';
15
16
  import { writeFileAtomic } from './atomic.js';
16
- import { applyBaseline, BASELINE_FILE, baselinesEqual, countBaselineKeys, fallowVerdict, fingerprint, loadBaseline, ratchet, writeBaseline, } from './baseline/index.js';
17
+ import { applyBaseline, BASELINE_FILE, baselinesEqual, countBaselineKeys, fallowVerdict, fingerprint, historicalHint, ratchet, readBaselineStatus, writeBaseline, } from './baseline/index.js';
17
18
  import { loadConfig, resolveChecks } from './config.js';
18
19
  import { writeDigest } from './digest/index.js';
19
20
  import { checkLinks } from './links.js';
20
21
  import { checkPack } from './pack.js';
21
- import { detectPackageManager, isAvailableUnder, translateExec } from './pm/index.js';
22
+ import { detectPackageManager, execTool, execUsesGlobalCache, installCommand, isAvailableUnder, resolveSlotTool, translateExec, } from './pm/index.js';
23
+ import { KILL_GRACE_SECONDS, killGroup } from './proc.js';
24
+ import { checkSecurity } from './security.js';
22
25
  import { checkSmoke } from './smoke.js';
23
26
  import { checkSnippets } from './snippets.js';
27
+ import { parseToolJson } from './tool-json.js';
24
28
  /**
25
- * Port of the interim `select_checks`: only/skip/opt-in selection by slot name.
29
+ * Only/skip/opt-in selection by slot name.
26
30
  * An opt-in slot runs when `--all`/`--include` names it, or when it was
27
31
  * explicitly configured in `checks` (`r.explicit`) — naming a slot is opting in.
28
32
  */
@@ -42,17 +46,30 @@ export function selectChecks(resolved, flags) {
42
46
  });
43
47
  }
44
48
  /**
45
- * Reject unknown slot names in `--only`/`--skip`/`--include`. A typo like
46
- * `--only lints` used to slip through `selectChecks` as a filter that matched
49
+ * Reject bad slot selections in `--only`/`--skip`/`--include`. A typo like
50
+ * `--only lints` would otherwise slip through `selectChecks` as a filter that matched
47
51
  * nothing, silently disabling the gate — the worst kind of vacuous green in a
48
52
  * definition-of-done check. It is a usage error instead (thrown here, surfaced
49
53
  * as exit 2 at the CLI). The valid set is every resolved slot: the catalogue
50
54
  * slots plus any config custom-check names.
55
+ *
56
+ * A *present but empty* list is rejected for the same reason. `[]` is truthy,
57
+ * so `selectChecks` reads it as "match nothing" rather than "no filter", and an
58
+ * empty `only` selects zero checks. The CLI already rejects `--only ,` when it
59
+ * parses (see `parseList`); this covers the programmatic API, where a caller can
60
+ * hand `runChecks` an array it computed. `selectChecks` itself is left alone —
61
+ * it is exported public surface, and the error belongs before it runs.
51
62
  */
52
63
  function validateSelection(resolved, flags) {
53
64
  const valid = new Set(resolved.map((r) => r.slot));
54
65
  for (const flag of ['only', 'skip', 'include']) {
55
- const unknown = (flags[flag] ?? []).filter((name) => !valid.has(name));
66
+ const names = flags[flag];
67
+ if (names === undefined || names === null)
68
+ continue;
69
+ if (names.length === 0) {
70
+ throw new Error(`--${flag} was given an empty list, which selects nothing. Valid slots: ${[...valid].join(', ')}.`);
71
+ }
72
+ const unknown = names.filter((name) => !valid.has(name));
56
73
  if (unknown.length === 0)
57
74
  continue;
58
75
  const named = unknown.map((n) => `'${n}'`).join(', ');
@@ -76,33 +93,18 @@ export function runtimeArgs(adapter, changed) {
76
93
  export const DEFAULT_TIMEOUT_SECONDS = 600;
77
94
  /**
78
95
  * Default pool width for a wave (equal-`order` group): `min(4, max(1, cores −
79
- * 1))` (D5). Heavy checks (test, mutation, build) parallelize internally, so
96
+ * reserve))`. Heavy checks (test, mutation, build) parallelize internally, so
80
97
  * oversubscribing every core is worse than a conservative cap; one reserved
81
- * core keeps the machine responsive. Override with `--concurrency`.
98
+ * core keeps the machine responsive — for the human at it. A hosted CI runner
99
+ * has no such human, and a standard GitHub-hosted runner reports 2 CPUs, so
100
+ * reserving one there collapsed the pool to 1 and wave scheduling silently
101
+ * degenerated to fully sequential — on exactly the machine class the docs say
102
+ * to gate on. Every CI provider sets `CI`, so no core is reserved there.
103
+ * Override with `--concurrency`; `--bail` stays fail-fast sequential.
82
104
  */
83
- function defaultConcurrency() {
84
- return Math.min(4, Math.max(1, cpus().length - 1));
85
- }
86
- /** Grace between SIGTERM and SIGKILL when a timed-out check won't die politely. */
87
- const KILL_GRACE_SECONDS = 5;
88
- /**
89
- * Signal a spawned check's whole process group. The check is spawned
90
- * `detached`, so it leads its own group and a negated-pid `kill` reaches every
91
- * descendant — a wrapper's grandchildren included — not just the direct child.
92
- * That is the difference between a timed-out `sh -c 'slow-tool …'` leaving its
93
- * real worker orphaned and alive versus taking the whole tree down. ESRCH is
94
- * swallowed: the group racing to exit on its own before we signal is success,
95
- * not an error (and `pid` is absent only when the spawn itself failed).
96
- */
97
- function killGroup(pid, signal) {
98
- if (pid === undefined)
99
- return;
100
- try {
101
- process.kill(-pid, signal);
102
- }
103
- catch {
104
- // Group already gone (raced with a clean exit) — nothing left to kill.
105
- }
105
+ export function defaultConcurrency(env = process.env, cores = cpus().length) {
106
+ const reserve = env['CI'] ? 0 : 1;
107
+ return Math.min(4, Math.max(1, cores - reserve));
106
108
  }
107
109
  /**
108
110
  * Process groups of checks currently in flight: pid → a promise that settles
@@ -221,51 +223,164 @@ function spawnCheck(command, args, cwd, timeoutSec) {
221
223
  });
222
224
  });
223
225
  }
226
+ /**
227
+ * Dispatch a built-in check, or `null` when the adapter spawns a real tool.
228
+ * Every built-in that runs a subprocess goes through `spawnCheck`, so its
229
+ * child registers in `liveChecks` and inherits the timeout + reaping like any
230
+ * other check. `security` alone runs the adapter's own args — its
231
+ * `--audit-level` doubles as the threshold the evaluator enforces, since
232
+ * pnpm's JSON-mode exit code ignores the level. The snippets pair shares one
233
+ * execution path, differing only in mode.
234
+ */
235
+ function runBuiltin(adapter, ctx, timeout) {
236
+ const base = { cwd: ctx.cwd, spawn: spawnCheck, timeoutSec: timeout };
237
+ switch (adapter.builtin) {
238
+ case 'pack':
239
+ return checkPack({ ...base, pm: ctx.pm });
240
+ case 'smoke':
241
+ return checkSmoke(base);
242
+ case 'security':
243
+ return checkSecurity({ ...base, command: adapter.command, args: runtimeArgs(adapter, ctx.changed) });
244
+ case 'snippets':
245
+ return checkSnippets({ ...base, mode: 'src', pm: ctx.pm });
246
+ case 'snippets-dist':
247
+ return checkSnippets({ ...base, mode: 'dist', pm: ctx.pm });
248
+ default:
249
+ return null;
250
+ }
251
+ }
252
+ /**
253
+ * Refuse a slot whose tool this repo never declared, rather than letting the
254
+ * launcher's per-user cache decide the verdict.
255
+ *
256
+ * `--no-install` stops `npx`/`bunx` fetching a missing tool, but both still run
257
+ * a copy cached from some earlier, unrelated invocation — so the same commit
258
+ * passes on a machine that happens to hold one and fails on a clean checkout,
259
+ * which is the CI runner. A gate whose result depends on that is not reporting
260
+ * on the code. Resolving the binary in the local tree first moves the failure
261
+ * to every machine equally, and to the one place it is cheap to fix.
262
+ *
263
+ * Scoped to the launchers that *have* such a cache ({@link
264
+ * execUsesGlobalCache}): `pnpm exec` and `yarn` resolve from the project tree
265
+ * already, and pre-flighting Yarn PnP — which has no `node_modules/.bin` — would
266
+ * report every tool missing. `null` means the check may spawn.
267
+ */
268
+ export function missingToolOutcome(slot, adapter, args, ctx) {
269
+ if (!execUsesGlobalCache(ctx.pm))
270
+ return null;
271
+ const tool = execTool(adapter.command, args);
272
+ if (!tool || resolveSlotTool(ctx.cwd, tool))
273
+ return null;
274
+ const stderr = [
275
+ `checkride: the \`${slot}\` slot needs \`${tool}\`, which is not installed in this project.`,
276
+ '',
277
+ ` looked for: node_modules/.bin/${tool} (from ${ctx.cwd} up to the repo root)`,
278
+ '',
279
+ 'checkride never fetches a tool mid-run, and a launcher cache can still supply',
280
+ "one this repo never declared — so a tool that isn't a dependency here would",
281
+ 'pass on your machine and fail on a clean checkout. Declare it instead:',
282
+ '',
283
+ ` ${installCommand(ctx.pm, tool)}`,
284
+ '',
285
+ ].join('\n');
286
+ // Exit 1, not -1. Nothing spawned, so there is no real status to report — but
287
+ // -1 is reserved for a spawn failure or timeout, which `triage` discounts as
288
+ // "a harness problem, not a finding" (see `docs/plugin.md`). An undeclared
289
+ // tool is the opposite: the finding the run exists to surface, and the one
290
+ // the reader has to act on. Reporting it as -1 would tell them to ignore it.
291
+ return { ok: false, exit_code: 1, stdout: '', stderr };
292
+ }
293
+ /**
294
+ * Fail the `prose` check when its configured `exemplars` directory is missing
295
+ * or holds no files, rather than letting the tool pass while the anchor texts
296
+ * rot away.
297
+ *
298
+ * The exemplars are the repo's hand-written voice reference: the AGENTS.md
299
+ * stanza tells writing sessions to imitate them, so a config that names a
300
+ * directory that no longer exists (renamed, deleted, never created) would leave
301
+ * that instruction aimed at nothing while the check stayed green. Presence is
302
+ * the whole assertion — checkride never scores prose against the exemplars;
303
+ * that read belongs to a human. `null` means the check may spawn.
304
+ */
305
+ export function missingExemplarsOutcome(adapter, cwd) {
306
+ const dir = adapter.exemplars;
307
+ if (dir === undefined)
308
+ return null;
309
+ let state = 'does not exist';
310
+ try {
311
+ const files = readdirSync(join(cwd, dir), { withFileTypes: true }).filter((e) => e.isFile() && !e.name.startsWith('.'));
312
+ state = files.length > 0 ? null : 'has no files in it';
313
+ }
314
+ catch (error) {
315
+ // Unreadable reads as missing. A file sitting at the path is named for what
316
+ // it is — "does not exist" would send the reader hunting a typo that isn't
317
+ // there.
318
+ if (error instanceof Error && 'code' in error && error.code === 'ENOTDIR')
319
+ state = 'is a file, not a directory';
320
+ }
321
+ if (state === null)
322
+ return null;
323
+ const stderr = [
324
+ `checkride: the \`prose\` slot names \`${dir}\` as its voice exemplars, and that path ${state}.`,
325
+ '',
326
+ 'Exemplars are hand-written prose the agent contract points writing sessions at',
327
+ 'as the voice to imitate, so a config naming an empty directory aims that',
328
+ 'instruction at nothing. Write a few short samples of prose in your own voice',
329
+ 'there (or fix the `exemplars` path in checkride.config.json), then re-run.',
330
+ '',
331
+ ].join('\n');
332
+ // Exit 1 for the same reason as missingToolOutcome above: nothing spawned,
333
+ // but this is the finding the run exists to surface, not a harness problem.
334
+ return { ok: false, exit_code: 1, stdout: '', stderr };
335
+ }
224
336
  const defaultRunner = (resolved, ctx) => {
225
337
  const adapter = resolved.adapter;
226
338
  if (!adapter)
227
339
  return Promise.resolve({ ok: true, exit_code: 0, stdout: '', stderr: '' });
228
- if (adapter.builtin === 'links')
229
- return checkLinks(ctx.cwd);
230
- const timeout = adapter.timeout ?? ctx.timeout ?? DEFAULT_TIMEOUT_SECONDS;
231
- // The pack built-in spawns its own subprocess (the PM's pack dry-run) through
232
- // `spawnCheck`, so its child registers in `liveChecks` and inherits the
233
- // timeout + reaping like every other check (D16/C6).
234
- if (adapter.builtin === 'pack')
235
- return checkPack({ cwd: ctx.cwd, pm: ctx.pm, spawn: spawnCheck, timeoutSec: timeout });
236
- // The smoke built-in spawns a `node` probe of the built package through
237
- // `spawnCheck`, so that child registers in `liveChecks` and inherits the
238
- // timeout + reaping like every other check (D9/D16/C6).
239
- if (adapter.builtin === 'smoke')
240
- return checkSmoke({ cwd: ctx.cwd, spawn: spawnCheck, timeoutSec: timeout });
241
- // The snippets built-ins spawn `<pm> exec tsc` through `spawnCheck` (D16/C6);
242
- // the two adapters share one execution path, differing only in mode (D12).
243
- if (adapter.builtin === 'snippets') {
244
- return checkSnippets({ cwd: ctx.cwd, mode: 'src', pm: ctx.pm, spawn: spawnCheck, timeoutSec: timeout });
245
- }
246
- if (adapter.builtin === 'snippets-dist') {
247
- return checkSnippets({ cwd: ctx.cwd, mode: 'dist', pm: ctx.pm, spawn: spawnCheck, timeoutSec: timeout });
340
+ if (adapter.builtin === 'links') {
341
+ return checkLinks(ctx.cwd, { exclude: adapter.exclude, allowlist: adapter.allowlist });
248
342
  }
249
- const { command, args } = translateExec(adapter.command, runtimeArgs(adapter, ctx.changed), ctx.pm);
343
+ const timeout = adapter.timeout ?? ctx.timeout ?? DEFAULT_TIMEOUT_SECONDS;
344
+ const builtin = runBuiltin(adapter, ctx, timeout);
345
+ if (builtin)
346
+ return builtin;
347
+ const missingExemplars = missingExemplarsOutcome(adapter, ctx.cwd);
348
+ if (missingExemplars)
349
+ return Promise.resolve(missingExemplars);
350
+ const declared = runtimeArgs(adapter, ctx.changed);
351
+ const missing = missingToolOutcome(resolved.slot, adapter, declared, ctx);
352
+ if (missing)
353
+ return Promise.resolve(missing);
354
+ const { command, args } = translateExec(adapter.command, declared, ctx.pm);
250
355
  return spawnCheck(command, args, ctx.cwd, timeout);
251
356
  };
252
- /** Persist raw output atomically: JSON to `.check/<outputFile>`, else stdout/stderr text. */
357
+ /**
358
+ * Persist raw output atomically: JSON to `.check/<outputFile>`, else
359
+ * stdout/stderr text.
360
+ *
361
+ * Returns the JSON file it wrote, or `null` when it fell through to text — the
362
+ * summary's `output_file` is that return value, never the adapter's
363
+ * *declaration*. A slot that declares an `outputFile` does not always produce
364
+ * one (the tool printed a warning first, crashed, or emitted plain text this
365
+ * run), and naming a file that was never written sends every consumer to an
366
+ * ENOENT while the real bytes sit in `<slot>.stdout.txt` beside it.
367
+ */
253
368
  async function persistOutput(cwd, adapter, outcome) {
254
369
  const dir = join(cwd, '.check');
255
370
  if (adapter.outputFile && outcome.stdout.trim()) {
256
- try {
257
- JSON.parse(outcome.stdout);
258
- await writeFileAtomic(join(dir, adapter.outputFile), outcome.stdout);
259
- return;
260
- }
261
- catch {
262
- // Not JSON after all; fall through to raw text.
371
+ // Tolerates a launcher preamble ahead of the JSON; writes the tool's own
372
+ // bytes from the first JSON character on, so the artifact actually parses.
373
+ const parsed = parseToolJson(outcome.stdout);
374
+ if (parsed) {
375
+ await writeFileAtomic(join(dir, adapter.outputFile), parsed.text);
376
+ return adapter.outputFile;
263
377
  }
264
378
  }
265
379
  if (outcome.stdout.trim())
266
380
  await writeFileAtomic(join(dir, `${adapter.slot}.stdout.txt`), outcome.stdout);
267
381
  if (outcome.stderr.trim())
268
382
  await writeFileAtomic(join(dir, `${adapter.slot}.stderr.txt`), outcome.stderr);
383
+ return null;
269
384
  }
270
385
  /**
271
386
  * Remove a slot's prior `.check/` artifacts before it re-runs, so a later clean
@@ -273,7 +388,7 @@ async function persistOutput(cwd, adapter, outcome) {
273
388
  * the previous run's output lingering as authoritative. Covers everything
274
389
  * `persistOutput` writes (`<slot>.stdout.txt`, `<slot>.stderr.txt`, the adapter's
275
390
  * JSON `outputFile`) plus the conventional `<slot>.json` a tool may write itself
276
- * (e.g. vitest's `--outputFile=.check/test.json`, where `outputFile` is null).
391
+ * (for example vitest's `--outputFile=.check/test.json`, where `outputFile` is null).
277
392
  * Runs *before* the check so this run's own tool-written artifacts survive.
278
393
  */
279
394
  async function clearSlotOutputs(cwd, adapter) {
@@ -290,9 +405,18 @@ async function clearSlotOutputs(cwd, adapter) {
290
405
  function writeLine(out, line) {
291
406
  out.write(`${line}\n`);
292
407
  }
293
- function formatStatusLine(check) {
408
+ /**
409
+ * Width of the slot-name column, measured from the names this run will print
410
+ * (never below the historical 8). A fixed 8 was sized for catalogue slots, and
411
+ * a config custom check — `typecheck-tests` is 15 — pushed the duration and
412
+ * description columns right on its row alone.
413
+ */
414
+ function nameWidth(selected) {
415
+ return selected.reduce((n, r) => Math.max(n, r.slot.length), 8);
416
+ }
417
+ function formatStatusLine(check, width) {
294
418
  const mark = check.ok ? '✔' : '✘';
295
- const name = check.name.padEnd(8);
419
+ const name = check.name.padEnd(width);
296
420
  const duration = `${check.duration_ms}ms`.padStart(8);
297
421
  return ` ${mark} ${name} ${duration} ${check.description}`;
298
422
  }
@@ -306,14 +430,16 @@ function skippedEntry(resolved) {
306
430
  reason: resolved.skip ?? 'skipped',
307
431
  exit_code: null,
308
432
  duration_ms: 0,
309
- output_file: resolved.adapter?.outputFile ?? null,
433
+ // A skipped check ran nothing and wrote nothing; naming its adapter's
434
+ // declared output file would point at whatever an earlier run left there.
435
+ output_file: null,
310
436
  };
311
437
  }
312
438
  /**
313
- * `total_duration_ms` is the wall-clock span of the whole execution phase (D7),
439
+ * `total_duration_ms` is the wall-clock span of the whole execution phase,
314
440
  * not the sum of per-check durations — under concurrency those diverge, and
315
- * wall-clock is what the field honestly means. It equals the old sum whenever
316
- * execution is sequential (one check in flight at a time).
441
+ * wall-clock is what the field honestly means. It equals the per-check sum
442
+ * whenever execution is sequential (one check in flight at a time).
317
443
  */
318
444
  function buildSummary(checks, totalDurationMs) {
319
445
  return {
@@ -335,8 +461,8 @@ function enableHint(slot, adapters) {
335
461
  }
336
462
  /**
337
463
  * The first-party vacuous-green signal: "green because nothing ran" must be
338
- * unmissable, not something only a consumer that hand-rolled the check (as
339
- * plumbbob's gate did) can distinguish from "green because everything passed".
464
+ * unmissable, not something only a consumer that hand-rolls its own check can
465
+ * distinguish from "green because everything passed".
340
466
  * Names why each slot sat out and what would enable it.
341
467
  */
342
468
  function warnVacuous(stderr, checks, adapters) {
@@ -354,9 +480,8 @@ function warnVacuous(stderr, checks, adapters) {
354
480
  }
355
481
  /**
356
482
  * Apply the defaults every command shares: cwd, the slot/adapter catalogues, the
357
- * config (loaded from `cwd` unless injected), and the two streams. Extracted so
358
- * `runChecks`/`runFix`/`runDoctor` resolve this identical block one way (it was
359
- * copy-pasted across all three).
483
+ * config (loaded from `cwd` unless injected), and the two streams, so
484
+ * `runChecks`/`runFix`/`runDoctor` resolve this identical block one way.
360
485
  */
361
486
  export function resolveCommonOptions(options) {
362
487
  const cwd = options.cwd ?? process.cwd();
@@ -369,18 +494,36 @@ export function resolveCommonOptions(options) {
369
494
  stderr: options.stderr ?? process.stderr,
370
495
  };
371
496
  }
372
- /** Apply every run default: the common options plus runner, flags, PM, and baseline. */
497
+ /**
498
+ * Apply every run default: the common options plus runner, flags, PM, and
499
+ * baseline. `nameWidth` is a placeholder here — the selection it measures is
500
+ * not known until after `selectChecks`, so `runChecks` narrows it then.
501
+ */
502
+ /**
503
+ * The baseline a run masks with: the option override, or the committed file.
504
+ * An injected baseline bypasses the file, so its state carries no file
505
+ * diagnosis — it is classified by presence alone.
506
+ */
507
+ function resolveBaselineRead(options, cwd) {
508
+ if (options.baseline !== undefined) {
509
+ return { baseline: options.baseline, state: options.baseline === null ? 'absent' : 'ok' };
510
+ }
511
+ return readBaselineStatus(cwd);
512
+ }
373
513
  function resolveRunContext(options) {
374
514
  const common = resolveCommonOptions(options);
515
+ const read = resolveBaselineRead(options, common.cwd);
375
516
  return {
376
517
  ...common,
518
+ nameWidth: 8,
377
519
  runner: options.runner ?? defaultRunner,
378
520
  json: options.json ?? false,
379
521
  bail: options.bail ?? false,
380
522
  changed: options.changed ?? false,
381
523
  timeout: common.config?.timeout,
382
524
  pm: options.pm ?? detectPackageManager({ cwd: common.cwd }),
383
- baseline: options.baseline !== undefined ? options.baseline : loadBaseline(common.cwd),
525
+ baseline: read.baseline,
526
+ baselineState: read.state,
384
527
  concurrency: Math.max(1, Math.floor(options.concurrency ?? defaultConcurrency())),
385
528
  };
386
529
  }
@@ -403,15 +546,15 @@ function maskOutcome(adapter, outcome, baseline, slot) {
403
546
  return { ok: outcome.ok, baselined: 0, newKeys: [], reason: null, observed: null };
404
547
  }
405
548
  /** Build (and print) the skipped-entry row for a slot that won't run this pass. */
406
- function handleSkip(r, unavailable, pm, json, stderr) {
407
- const entry = skippedEntry(unavailable ? { ...r, skip: `'${r.adapter?.command} ${r.adapter?.args[0]}' is unavailable under ${pm}` } : r);
408
- if (!json)
409
- writeLine(stderr, ` ○ ${entry.name.padEnd(8)} skip ${entry.reason ?? ''}`);
549
+ function handleSkip(r, unavailable, ctx) {
550
+ const entry = skippedEntry(unavailable ? { ...r, skip: `'${r.adapter?.command} ${r.adapter?.args[0]}' is unavailable under ${ctx.pm}` } : r);
551
+ if (!ctx.json)
552
+ writeLine(ctx.stderr, ` ○ ${entry.name.padEnd(ctx.nameWidth)} skip ${entry.reason ?? ''}`);
410
553
  return entry;
411
554
  }
412
555
  /** Print one check's status line, its baselined count, and any new (non-grandfathered) findings. */
413
- function reportCheckResult(stderr, entry, mask) {
414
- writeLine(stderr, formatStatusLine(entry));
556
+ function reportCheckResult(stderr, entry, mask, width) {
557
+ writeLine(stderr, formatStatusLine(entry, width));
415
558
  if (mask.baselined > 0)
416
559
  writeLine(stderr, ` ${mask.baselined} baselined (grandfathered)`);
417
560
  if (!entry.ok && mask.reason)
@@ -428,12 +571,12 @@ async function runOneCheck(r, adapter, ctx) {
428
571
  writeLine(ctx.stderr, ` ▸ ${r.slot} ${adapter.description}`);
429
572
  // Wipe this slot's stale `.check/` artifacts before it runs, so a leaner re-run
430
573
  // can't leave last run's output behind as authoritative — and so any artifact
431
- // the tool writes during *this* run (e.g. `test.json`) survives.
574
+ // the tool writes during *this* run (for example `test.json`) survives.
432
575
  await clearSlotOutputs(ctx.cwd, adapter);
433
576
  const start = performance.now();
434
577
  const outcome = await ctx.runner(r, { cwd: ctx.cwd, changed: ctx.changed, pm: ctx.pm, ...(ctx.timeout !== undefined ? { timeout: ctx.timeout } : {}) });
435
578
  const duration_ms = Math.round(performance.now() - start);
436
- await persistOutput(ctx.cwd, adapter, outcome);
579
+ const outputFile = await persistOutput(ctx.cwd, adapter, outcome);
437
580
  // Masking is always on (even under a partial run); only the ratchet is gated.
438
581
  // The raw `.check/<slot>.json` is persisted untouched — masking changes the
439
582
  // pass/fail verdict, never the authoritative output.
@@ -445,7 +588,7 @@ async function runOneCheck(r, adapter, ctx) {
445
588
  ok: mask.ok,
446
589
  exit_code: outcome.exit_code,
447
590
  duration_ms,
448
- output_file: adapter.outputFile,
591
+ output_file: outputFile,
449
592
  ...(mask.baselined > 0 ? { baselined: mask.baselined } : {}),
450
593
  };
451
594
  return { entry, run: { slot: r.slot, adapter, outcome }, mask };
@@ -455,19 +598,19 @@ async function runOneCheck(r, adapter, ctx) {
455
598
  * shared by the sequential (`--bail`) and wave paths. A skipped/unavailable slot
456
599
  * records a skip row and runs nothing; an active one runs, prints its status
457
600
  * line, and reports its fingerprint for the ratchet. It mutates no shared state,
458
- * so N of these can be in flight at once (C6).
601
+ * so N of these can be in flight at once.
459
602
  */
460
603
  async function runSelectedCheck(r, ctx) {
461
- // Skip when unresolved, or when the adapter can't run under this PM — e.g.
462
- // `pnpm audit` (the `security` slot) is unavailable off pnpm (b5).
604
+ // Skip when unresolved, or when the adapter can't run under this PM — for
605
+ // example `pnpm audit` (the `security` slot) is unavailable off pnpm.
463
606
  const unavailable = Boolean(r.adapter && !isAvailableUnder(r.adapter.command, r.adapter.args, ctx.pm));
464
607
  if (r.skip || !r.adapter || unavailable) {
465
- return { entry: handleSkip(r, unavailable, ctx.pm, ctx.json, ctx.stderr), run: null, observed: null };
608
+ return { entry: handleSkip(r, unavailable, ctx), run: null, observed: null, newKeys: [] };
466
609
  }
467
610
  const { entry, run, mask } = await runOneCheck(r, r.adapter, ctx);
468
611
  if (!ctx.json)
469
- reportCheckResult(ctx.stderr, entry, mask);
470
- return { entry, run, observed: mask.observed !== null ? { slot: r.slot, fp: mask.observed } : null };
612
+ reportCheckResult(ctx.stderr, entry, mask, ctx.nameWidth);
613
+ return { entry, run, observed: mask.observed !== null ? { slot: r.slot, fp: mask.observed } : null, newKeys: mask.newKeys };
471
614
  }
472
615
  /** Fold one result into the report accumulators, in call order. */
473
616
  function collect(res, acc) {
@@ -476,15 +619,16 @@ function collect(res, acc) {
476
619
  acc.runs.push(res.run);
477
620
  if (res.observed)
478
621
  acc.observed.set(res.observed.slot, res.observed.fp);
622
+ acc.newKeys.push(...res.newKeys);
479
623
  }
480
624
  /** A fresh, empty {@link Execution}. */
481
625
  function emptyExecution(brokeEarly = false) {
482
- return { checks: [], runs: [], observed: new Map(), brokeEarly };
626
+ return { checks: [], runs: [], observed: new Map(), newKeys: [], brokeEarly };
483
627
  }
484
628
  /**
485
- * Scheduling coordinates for a check on D1's line: the group `rank` (firsts 0, the
486
- * numeric line 1, singles 2, lasts 3) and its position on the numeric line
487
- * (`'any'`/`'middle'` sit at 0 — v1's conservative placement). This must agree
629
+ * Scheduling coordinates for a check on the order line: the group `rank` (firsts 0,
630
+ * the numeric line 1, singles 2, lasts 3) and its position on the numeric line
631
+ * (`'any'`/`'middle'` sit at 0 — the conservative placement). This must agree
488
632
  * with `config.ts`'s group sort, which already put `selected` in exactly this
489
633
  * sequence; here it only re-derives the wave *boundaries* the sort collapsed.
490
634
  */
@@ -547,16 +691,16 @@ async function runPool(items, width, worker) {
547
691
  await Promise.all(workers);
548
692
  }
549
693
  /**
550
- * `--bail`: the flat sequential path (D6). Checks run one at a time in D1's group
551
- * order and the run stops at the first *executed* failure — fail-fast is
694
+ * `--bail`: the flat sequential path. Checks run one at a time in group order
695
+ * and the run stops at the first *executed* failure — fail-fast is
552
696
  * incompatible with already-launched concurrent work, so `--concurrency` is moot
553
697
  * here (the caller notes it if one was passed). For the default set (all `'any'`)
554
- * this is exactly today's cheapest-first order.
698
+ * this is exactly the cheapest-first order.
555
699
  */
556
700
  async function executeBail(selected, ctx) {
557
701
  const acc = emptyExecution();
558
702
  for (const r of selected) {
559
- // oxlint-disable-next-line no-await-in-loop -- --bail is fail-fast: checks run one at a time so the run can stop at the first failure (D6).
703
+ // oxlint-disable-next-line no-await-in-loop -- --bail is fail-fast: checks run one at a time so the run can stop at the first failure.
560
704
  const res = await runSelectedCheck(r, ctx);
561
705
  collect(res, acc);
562
706
  if (res.run && !res.entry.ok)
@@ -565,17 +709,17 @@ async function executeBail(selected, ctx) {
565
709
  return acc;
566
710
  }
567
711
  /**
568
- * The wave scheduler (D1): run each wave's members concurrently through the pool,
712
+ * The wave scheduler: run each wave's members concurrently through the pool,
569
713
  * with a barrier between waves. Results are placed by their `selected` index and
570
714
  * assembled in that order afterwards, so the report is deterministic regardless
571
- * of which check finishes first (D7). Every selected check runs — there is no
715
+ * of which check finishes first. Every selected check runs — there is no
572
716
  * early stop off the `--bail` path.
573
717
  */
574
718
  async function executeWaves(selected, ctx) {
575
719
  const indexed = selected.map((r, i) => ({ r, i }));
576
720
  const results = Array.from({ length: selected.length });
577
721
  for (const wave of partitionWaves(indexed)) {
578
- // oxlint-disable-next-line no-await-in-loop -- the between-values barrier (D1): a wave runs to completion before the next distinct order value begins.
722
+ // oxlint-disable-next-line no-await-in-loop -- the between-values barrier: a wave runs to completion before the next distinct order value begins.
579
723
  await runPool(wave, ctx.concurrency, async ({ r, i }) => { results[i] = await runSelectedCheck(r, ctx); });
580
724
  }
581
725
  const acc = emptyExecution();
@@ -595,12 +739,12 @@ function executeChecks(selected, ctx) {
595
739
  /**
596
740
  * Whether this run saw less than the full pipeline — an `--only`/`--skip`/
597
741
  * `--changed` filter or an early `--bail` break. The ratchet is gated off for
598
- * these: an unobserved diagnostic must not be mistaken for a fixed one (a1).
742
+ * these: an unobserved diagnostic must not be mistaken for a fixed one.
599
743
  */
600
744
  function isPartialRun(options, changed, brokeEarly) {
601
745
  return (options.only ?? null) !== null || (options.skip?.length ?? 0) > 0 || changed || brokeEarly;
602
746
  }
603
- /** On a fully-observed run, prune grandfathered diagnostics now fixed (shrink-only, atomic). */
747
+ /** On a fully observed run, prune grandfathered diagnostics now fixed (shrink-only, atomic). */
604
748
  async function maybeRatchet(cwd, baseline, observed, restricted, json, stderr) {
605
749
  if (!baseline || restricted)
606
750
  return;
@@ -612,6 +756,26 @@ async function maybeRatchet(cwd, baseline, observed, restricted, json, stderr) {
612
756
  writeLine(stderr, `\nbaseline: ratcheted ${BASELINE_FILE} to ${countBaselineKeys(pruned)} grandfathered diagnostic(s)`);
613
757
  }
614
758
  }
759
+ /**
760
+ * How much of the selection actually ran, said only when it was not all of it.
761
+ *
762
+ * `✔ all checks passed` is a true sentence about the checks that ran and a
763
+ * misleading one about the repo, and the gap is invisible at exactly the moment
764
+ * it matters most: a repo that configures almost nothing reports the same
765
+ * confident green as one that configures everything. `--strict` does not catch
766
+ * this, and cannot — its floor is *zero* checks, which a slot like `links` that
767
+ * needs no tool keeps a repo off by itself.
768
+ *
769
+ * Only skipped slots are counted here. The per-slot `○ … skip <reason>` lines
770
+ * are already on screen above; this is the one line someone scrolled past them
771
+ * to read.
772
+ */
773
+ function coverage(summary, checks) {
774
+ const skipped = checks.length - summary.checks_run;
775
+ if (skipped <= 0)
776
+ return '';
777
+ return ` — only ${summary.checks_run} of ${checks.length} checks ran, ${skipped} skipped`;
778
+ }
615
779
  /** Print the human run summary: the vacuous-green warning, the status line, and artifact paths. */
616
780
  function reportSummary(stderr, summary, checks, adapters, digestWritten) {
617
781
  if (summary.checks_run === 0)
@@ -620,7 +784,8 @@ function reportSummary(stderr, summary, checks, adapters, digestWritten) {
620
784
  const status = summary.checks_run === 0
621
785
  ? '⚠ no checks ran'
622
786
  : summary.ok ? '✔ all checks passed' : '✘ one or more checks failed';
623
- writeLine(stderr, `${status} in ${summary.total_duration_ms}ms`);
787
+ const ran = summary.checks_run === 0 ? '' : coverage(summary, checks);
788
+ writeLine(stderr, `${status} in ${summary.total_duration_ms}ms${ran}`);
624
789
  writeLine(stderr, 'report: .check/summary.json');
625
790
  if (digestWritten)
626
791
  writeLine(stderr, 'digest: .check/digest.md');
@@ -639,34 +804,60 @@ function computeExitCode(summary, strict, json, stderr) {
639
804
  }
640
805
  return summary.ok ? 0 : 1;
641
806
  }
807
+ /**
808
+ * On a red run, the best-effort recover hint: when the run's new-not-in-baseline
809
+ * findings were grandfathered in a recent committed baseline, one stderr line
810
+ * says so. The difference between "a wall of new debt" and "a merge dropped
811
+ * baseline entries" is a diagnosis the run output alone cannot make — and this
812
+ * is the moment someone is actually reading it. Strictly bounded and silent on
813
+ * any failure (see {@link historicalHint}); a hint must never slow or break a run.
814
+ */
815
+ async function maybeRecoverHint(ctx, ok, newKeys) {
816
+ if (ok || newKeys.length === 0)
817
+ return;
818
+ const hint = await historicalHint(ctx.cwd, newKeys);
819
+ if (hint === null)
820
+ return;
821
+ writeLine(ctx.stderr, `hint: ${hint.matched} of ${hint.total} new finding(s) were grandfathered until ${hint.shortSha} — a merge may have dropped baseline entries; see \`checkride recover\`\n`);
822
+ }
642
823
  /** Run the selected checks against `cwd`, persist output, write the summary. */
643
824
  export async function runChecks(options) {
644
- const ctx = resolveRunContext(options);
645
- const resolved = resolveChecks({ slots: ctx.slots, adapters: ctx.adapters, config: ctx.config, cwd: ctx.cwd });
825
+ // Clear the interrupt latch. On the CLI path this is a no-op — the process
826
+ // re-raises the signal and dies — but `runChecks` is exported, and a
827
+ // long-lived programmatic consumer that took one SIGINT would otherwise have
828
+ // every later run silently spawn nothing (`spawnCheck` starts nothing while
829
+ // latched). A new call necessarily follows the previous one's return, so
830
+ // there is no in-flight run for this to un-latch.
831
+ interrupted = false;
832
+ const base = resolveRunContext(options);
833
+ const resolved = resolveChecks({ slots: base.slots, adapters: base.adapters, config: base.config, cwd: base.cwd });
646
834
  // A usage error before any side effect: no `.check/` dir, no run, exit 2.
647
835
  validateSelection(resolved, options);
648
- await mkdir(join(ctx.cwd, '.check'), { recursive: true });
836
+ await mkdir(join(base.cwd, '.check'), { recursive: true });
649
837
  const selected = selectChecks(resolved, options);
838
+ // The status-line column is sized once the selection is known, so a long
839
+ // custom-check name widens the column instead of overflowing it.
840
+ const ctx = { ...base, nameWidth: nameWidth(selected) };
650
841
  if (!ctx.json)
651
842
  writeLine(ctx.stderr, `\nRunning ${selected.length} check(s)...\n`);
652
- // `--bail` is fail-fast sequential (D6); a `--concurrency > 1` passed alongside
843
+ // `--bail` is fail-fast sequential; a `--concurrency > 1` passed alongside
653
844
  // is safe but moot, so say so once — not a usage error, the run just goes slow.
654
845
  if (ctx.bail && (options.concurrency ?? 0) > 1 && !ctx.json) {
655
846
  writeLine(ctx.stderr, '--concurrency ignored under --bail (fail-fast runs sequentially).');
656
847
  }
657
- // `total_duration_ms` is wall-clock of the execution phase (D7), so measure
848
+ // `total_duration_ms` is wall-clock of the execution phase, so measure
658
849
  // around it rather than summing per-check durations (which diverge under
659
850
  // concurrency). The report array is assembled in `selected` order, not
660
851
  // completion order, so it stays byte-reproducible regardless of interleaving.
661
852
  const startedAt = performance.now();
662
- const { checks, runs, observed, brokeEarly } = await executeChecks(selected, ctx);
853
+ const { checks, runs, observed, newKeys, brokeEarly } = await executeChecks(selected, ctx);
663
854
  const totalDurationMs = Math.round(performance.now() - startedAt);
664
855
  await maybeRatchet(ctx.cwd, ctx.baseline, observed, isPartialRun(options, ctx.changed, brokeEarly), ctx.json, ctx.stderr);
665
856
  const summary = buildSummary(checks, totalDurationMs);
666
857
  await writeFileAtomic(join(ctx.cwd, '.check', 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`);
667
858
  // `--digest`: write (or, on green, clear) the token-bounded failure excerpt.
668
859
  // A file beside summary.json, never a stdout stream, so the machine-output
669
- // split holds (C5). Raw `.check/<slot>.json` files are already persisted and
860
+ // split holds. Raw `.check/<slot>.json` files are already persisted and
670
861
  // untouched — the digest only reads them.
671
862
  const digestWritten = (options.digest ?? false) ? await writeDigest(ctx.cwd, runs, checks) : false;
672
863
  if (ctx.json) {
@@ -674,6 +865,13 @@ export async function runChecks(options) {
674
865
  }
675
866
  else {
676
867
  reportSummary(ctx.stderr, summary, checks, ctx.adapters, digestWritten);
868
+ // A mangled committed baseline (a botched merge, usually) masks nothing, so
869
+ // every grandfathered finding above reported red — say so where the person
870
+ // reading the wall of red will actually look, right under the summary.
871
+ if (ctx.baselineState === 'unparseable') {
872
+ writeLine(ctx.stderr, `baseline: ${BASELINE_FILE} is present but unparseable — possibly a botched merge; grandfathered findings report red. See \`checkride recover\`.\n`);
873
+ }
874
+ await maybeRecoverHint(ctx, summary.ok, newKeys);
677
875
  }
678
876
  const exitCode = computeExitCode(summary, options.strict ?? false, ctx.json, ctx.stderr);
679
877
  return { ok: summary.ok, summary, exitCode, runs };
@@ -712,13 +910,14 @@ export async function runFix(options) {
712
910
  return { ok: true, exitCode: 0, ran: [] };
713
911
  }
714
912
  const ran = [];
913
+ const width = nameWidth(fixable);
715
914
  let ok = true;
716
915
  for (const r of fixable) {
717
916
  const adapter = r.adapter;
718
917
  if (!adapter)
719
918
  continue;
720
- writeLine(stderr, ` ▸ fix ${r.slot.padEnd(8)} (${adapter.name})`);
721
- // oxlint-disable-next-line no-await-in-loop -- fixers mutate the working tree; running them sequentially prevents two (e.g. oxlint --fix and prettier --write) racing on the same files.
919
+ writeLine(stderr, ` ▸ fix ${r.slot.padEnd(width)} (${adapter.name})`);
920
+ // oxlint-disable-next-line no-await-in-loop -- fixers mutate the working tree; running them sequentially prevents two (for example oxlint --fix and prettier --write) racing on the same files.
722
921
  const outcome = await fixRunner(adapter, { cwd, pm });
723
922
  ran.push(adapter.name);
724
923
  writeLine(stderr, outcome.ok ? ` ✔ ${r.slot}` : ` ✘ ${r.slot} (exit ${outcome.exit_code})`);