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
@@ -11,18 +11,19 @@
11
11
  * Generation, not transformation: every file is written once; the project owns
12
12
  * it afterward.
13
13
  */
14
+ import { createHash } from 'node:crypto';
14
15
  import { existsSync, readFileSync } from 'node:fs';
15
16
  import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
16
17
  import { basename, dirname, join } from 'node:path';
17
18
  import { fileURLToPath } from 'node:url';
18
19
  import { ADAPTERS, SLOTS } from './adapters.js';
19
20
  import { planSnippets, selectDocFiles } from './snippets.js';
20
- import { CLAUDE_SETTINGS_FILE, writeStopHook } from './agent-setup/index.js';
21
+ import { detectHarnesses, writeCursorSkills, writeHooks } from './agent-setup/index.js';
21
22
  import { BASELINE_FILE, isFingerprintable } from './baseline/index.js';
22
23
  import { runBaseline } from './baseline-command.js';
23
- import { configSchemaUrl, resolveChecks } from './config.js';
24
- import { runChecks } from './orchestrator.js';
25
- import { detectPackageManager } from './pm/index.js';
24
+ import { configSchemaUrl, loadConfig, resolveChecks } from './config.js';
25
+ import { runChecks, selectChecks } from './orchestrator.js';
26
+ import { detectPackageManager, execCommand, runScript } from './pm/index.js';
26
27
  const NULL_OUT = { write: () => true };
27
28
  /** Run the adopted checks once; a failing (non-skipped) check marks its slot. */
28
29
  async function defaultProbe(slots, cwd) {
@@ -33,22 +34,158 @@ async function defaultProbe(slots, cwd) {
33
34
  }
34
35
  const HERE = dirname(fileURLToPath(import.meta.url));
35
36
  const TEMPLATES_DIR = join(HERE, '..', 'templates');
36
- const STANZA_BEGIN = '<!-- checkride:begin -->';
37
+ const STANZA_BEGIN_PREFIX = '<!-- checkride:begin';
37
38
  const STANZA_END = '<!-- checkride:end -->';
39
+ /** The block, capturing its stamp (absent on stanzas written before v0.10.1) and its body. */
40
+ const STANZA_RE = /<!-- checkride:begin(?: hash=([0-9a-z]+))? -->([\s\S]*?)<!-- checkride:end -->/;
41
+ const STANZA_BEGIN_RE = /<!-- checkride:begin(?: hash=[0-9a-z]+)? -->/;
38
42
  // ----------------------------------------------------------------------------
39
43
  // AGENTS.md stanza (gate: idempotent)
40
44
  // ----------------------------------------------------------------------------
41
- function escapeRegex(text) {
42
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
+ /**
46
+ * Compare two stanza bodies the way a reader would: line endings and trailing
47
+ * whitespace are not edits. Deliberately minimal — one checkride version stamps
48
+ * a block and a later one verifies that stamp, so every rule here has to keep
49
+ * holding across versions or a pristine stanza starts reading as an edited one.
50
+ */
51
+ function normalizeStanza(text) {
52
+ return text.replace(/\r\n/g, '\n').replace(/[ \t]+$/gm, '').trim();
53
+ }
54
+ /**
55
+ * The stamp written into the begin marker: a digest of the body checkride
56
+ * generated. A later run recomputes it from what is on disk — a match means the
57
+ * block is still checkride's own output and is safe to refresh; a mismatch means
58
+ * someone edited it and refreshing would destroy their work.
59
+ *
60
+ * The `v1` prefix earns its place twice: it dates the normalization rules above
61
+ * (change them and bump it, so old stamps read as unverifiable rather than
62
+ * edited), and it guarantees the token contains a digit. cspell ignores words
63
+ * with digits, so a digest that happened to come up all `a`-`f` cannot fail the
64
+ * `spell` check of the repo checkride wrote it into.
65
+ */
66
+ function stanzaStamp(body) {
67
+ return `v1${createHash('sha256').update(normalizeStanza(body)).digest('hex').slice(0, 16)}`;
68
+ }
69
+ /**
70
+ * A stanza body without its trailing generated `Active checks in this repo: …`
71
+ * line — the part that is pure wording, identical in every repo running a given
72
+ * checkride version.
73
+ *
74
+ * The active-checks line is what makes two stanzas from the same release differ,
75
+ * so a registry of released wordings has to compare everything but that. If the
76
+ * line is missing, or something follows it inside the markers, nothing is
77
+ * stripped and the digest simply fails to match — which is the safe answer.
78
+ */
79
+ function stanzaPreamble(body) {
80
+ return normalizeStanza(normalizeStanza(body).replace(/\nActive checks in this repo:[^\n]*$/, ''));
81
+ }
82
+ /**
83
+ * The wording of every stanza checkride released before it stamped them, keyed
84
+ * by {@link stanzaPreamble} digest. Four distinct texts across v0.1.1–v0.10.0.
85
+ *
86
+ * This is what makes the stamp's arrival a non-event for repos that never
87
+ * touched their stanza. Without it, every upgrading repo looked `unstamped` —
88
+ * an older version's wording is not distinguishable from a customization by
89
+ * comparison alone — so the first run after upgrading refused and wrote
90
+ * *nothing*, hooks and config included. That is how the 0.10.1 gate-timeout fix
91
+ * failed to reach the repos that most needed it: the guard protecting AGENTS.md
92
+ * prose was also withholding a fix for a gate that had silently stopped gating.
93
+ *
94
+ * Recognizing the wording collapses that to the case it was always meant to
95
+ * cover. A pre-stamp stanza whose preamble is one of these is checkride's own
96
+ * output, untouched, and refreshing it loses nothing; one that has been edited
97
+ * still fails to match and is still refused.
98
+ *
99
+ * The list is closed. Every release from 0.10.1 stamps what it writes, so no
100
+ * future version can add an unstamped wording — nothing here needs maintaining.
101
+ */
102
+ const LEGACY_STANZA_PREAMBLES = new Set([
103
+ 'v16da2ec6a25e24085', // v0.1.1–v0.1.6
104
+ 'v190c2d48a280ff92c', // v0.2.0–v0.7.0
105
+ 'v1d4b4eaeeccb5faa2', // v0.8.0–v0.9.6
106
+ 'v17a3a1a85e3c3123e', // v0.10.0 (0.10.1 shares the wording but stamps it)
107
+ ]);
108
+ /** Classify the stanza in `content` against the `body` checkride would write. */
109
+ export function inspectStanza(content, body) {
110
+ const match = STANZA_RE.exec(content);
111
+ if (!match) {
112
+ // A begin marker with no end is a stanza someone edited badly, not an absent
113
+ // one: appending a second block would be the same data loss in another form.
114
+ return content.includes(STANZA_BEGIN_PREFIX) ? 'edited' : 'absent';
115
+ }
116
+ const [, stamp, found = ''] = match;
117
+ if (stamp === undefined) {
118
+ if (normalizeStanza(found) === normalizeStanza(body))
119
+ return 'pristine';
120
+ return LEGACY_STANZA_PREAMBLES.has(stanzaStamp(stanzaPreamble(found))) ? 'pristine' : 'unstamped';
121
+ }
122
+ return stamp === stanzaStamp(found) ? 'pristine' : 'edited';
123
+ }
124
+ /**
125
+ * The `struct` slot's section: where the boundary rules live, not what they say.
126
+ *
127
+ * checkride ships opinionated ast-grep defaults (`no-class.yml` and friends),
128
+ * but it does not own the repo's architecture, and the stanza is the one place
129
+ * a repo cannot talk back — editing it blocks the next refresh. So this points
130
+ * at the rule files and stops. A repo that allows classes, default exports, or
131
+ * bundler-style extensionless imports deletes those rules and this stays true.
132
+ */
133
+ function structSection() {
134
+ return [
135
+ '### Module boundaries',
136
+ '',
137
+ 'The `struct` check runs whatever ast-grep rules `sgconfig.yml` points at',
138
+ "(`rules/` by default). Those files are this repo's boundary convention —",
139
+ 'read them rather than assuming one.',
140
+ '',
141
+ ];
142
+ }
143
+ /**
144
+ * The `prose` slot's voice section, present only when the repo names an
145
+ * `exemplars` directory in its config. The instruction runs one way — read and
146
+ * imitate, never edit — because the exemplars are the repo's hand-written voice
147
+ * reference, the fresh human signal that keeps generated prose from converging
148
+ * on the model's own register. A generated "improvement" to one would replace
149
+ * the original with a copy, which is the drift the directory exists to prevent.
150
+ */
151
+ function proseSection(exemplars) {
152
+ return [
153
+ '### Prose voice',
154
+ '',
155
+ 'Before writing or editing prose (markdown, doc comments), read the exemplars in',
156
+ `\`${exemplars}\` and imitate their voice — sentence rhythm, word choice, register.`,
157
+ 'They are hand-written and human-owned: never edit, rewrite, or add to them, and',
158
+ 'never generate new ones. When your draft and an exemplar disagree about how a',
159
+ 'sentence should sound, the exemplar wins.',
160
+ '',
161
+ ];
43
162
  }
44
- /** The agent-facing contract block, parameterized by the active checks. */
45
- export function buildStanza(activeSlots) {
163
+ /**
164
+ * The agent-facing contract block, parameterized by the active checks and the
165
+ * repo's package manager.
166
+ *
167
+ * `pm` is not cosmetic. The stanza is a list of commands an agent is told to
168
+ * run, and `pnpm check` in an npm repo is an instruction that cannot succeed —
169
+ * the agent either invents a substitute or reports the pipeline as broken.
170
+ * Everything here routes through {@link runScript}/{@link execCommand} for that
171
+ * reason; nothing spells a launcher literally.
172
+ *
173
+ * The gate profile is deliberately *not* a parameter. A repo can add `gate` to
174
+ * checkride.config.json without re-running `agent-setup`, so a stanza naming a
175
+ * specific profile would go stale in exactly the direction that costs something
176
+ * — telling an agent the hook covered everything when it no longer does. The
177
+ * paragraph points at the gate's verdict instead, which is generated per run and
178
+ * states the narrowing itself (see `gate.ts`'s `profileClause`).
179
+ */
180
+ export function buildStanza(activeSlots, pm = 'pnpm', proseExemplars = null) {
181
+ const check = runScript(pm, 'check');
182
+ const triage = execCommand(pm, ['checkride', 'triage']);
46
183
  return [
47
184
  '## Checkride: the definition of done',
48
185
  '',
49
- '`pnpm check` is the single source of truth for "done". Exit 0 means the work is',
186
+ `\`${check}\` is the single source of truth for "done". Exit 0 means the work is`,
50
187
  'complete; any other exit code means it is not. Never claim a task is finished while',
51
- '`pnpm check` is red.',
188
+ `\`${check}\` is red.`,
52
189
  '',
53
190
  'When it fails:',
54
191
  '',
@@ -56,13 +193,19 @@ export function buildStanza(activeSlots) {
56
193
  "2. Read that check's raw output (`.check/<slot>.json` or `.check/<slot>.stdout.txt`).",
57
194
  '3. Fix the root cause, then re-run.',
58
195
  '',
59
- 'Tight feedback loops: `pnpm check --bail`, `pnpm check --only types,lint`, and',
60
- '`pnpm check --changed`.',
196
+ `\`${triage}\` runs this procedure in full and reads \`.check/\` for you`,
197
+ '(`/checkride:check` and `/checkride-check` are the same thing as a skill).',
61
198
  '',
62
- 'If a Claude Code Stop hook is configured (`.claude/settings.json`), it runs the full',
63
- '`pnpm check` as the final gate — so while iterating, prefer the narrow commands above',
64
- 'and let the hook run the authoritative pipeline once at the end rather than running',
65
- 'the full check yourself every loop.',
199
+ `Tight feedback loops: \`${check} --bail\`, \`${check} --only types,lint\`, and`,
200
+ `\`${check} --changed\`.`,
201
+ '',
202
+ 'If a stop-gate hook is configured (`.claude/settings.json` or `.cursor/hooks.json`),',
203
+ 'it runs the check when a turn ends — so while iterating, prefer the narrow commands',
204
+ "above rather than running the full check yourself every loop. Read the gate's verdict",
205
+ 'rather than assuming it covered everything: a repo can narrow the gate with `gate` in',
206
+ '`checkride.config.json`, and a narrowed one prints `NOT the full check` in every',
207
+ `verdict. That green is not the "done" defined above; run \`${check}\` in full before you`,
208
+ 'claim the work is finished.',
66
209
  '',
67
210
  '### Baseline',
68
211
  '',
@@ -72,27 +215,29 @@ export function buildStanza(activeSlots) {
72
215
  'the ratchet, so the baseline only ever shrinks. Never add to the baseline to make a',
73
216
  'check pass; fix the finding.',
74
217
  '',
75
- '### Module boundaries',
76
- '',
77
- 'A module is a unit of encapsulation. A single file is a module; promote it to a',
78
- 'folder with a barrel `index.ts` when it grows internals worth hiding. A folder',
79
- "module's `index.ts` is its only public surface — re-exports only, no logic. Import",
80
- "siblings through `'../<sibling>/index.js'`, never their internals.",
81
- '',
82
- 'Named exports only; no classes; `.js` extensions on relative imports.',
83
- '',
218
+ ...(activeSlots.includes('struct') ? structSection() : []),
219
+ ...(proseExemplars !== null ? proseSection(proseExemplars) : []),
84
220
  `Active checks in this repo: ${activeSlots.join(', ')}.`,
85
221
  ].join('\n');
86
222
  }
87
223
  /**
88
224
  * Insert or refresh the checkride stanza in an AGENTS.md body. Idempotent:
89
- * applying twice yields identical output.
225
+ * applying twice yields identical output. It rewrites the marked region without
226
+ * looking at what was there — callers gate on {@link inspectStanza} first.
90
227
  */
91
228
  export function applyStanza(content, body) {
92
- const block = `${STANZA_BEGIN}\n\n${body}\n\n${STANZA_END}`;
93
- const re = new RegExp(`${escapeRegex(STANZA_BEGIN)}[\\s\\S]*?${escapeRegex(STANZA_END)}`);
94
- if (re.test(content)) {
95
- return content.replace(re, block);
229
+ // A replacer function, not the string: a `$&` in the stanza would otherwise be
230
+ // read as a backreference.
231
+ const block = `${STANZA_BEGIN_PREFIX} hash=${stanzaStamp(body)} -->\n\n${body}\n\n${STANZA_END}`;
232
+ if (STANZA_RE.test(content)) {
233
+ return content.replace(STANZA_RE, () => block);
234
+ }
235
+ if (STANZA_BEGIN_RE.test(content)) {
236
+ // A begin marker with no end: replace the marker in place rather than
237
+ // appending a second block, which would leave two begins and make the next
238
+ // refresh swallow everything between them. What followed the orphaned marker
239
+ // stays where it is — now outside the markers, so nothing rewrites it again.
240
+ return content.replace(STANZA_BEGIN_RE, () => block);
96
241
  }
97
242
  if (content.trim().length === 0) {
98
243
  return `${block}\n`;
@@ -312,18 +457,57 @@ async function writePackage(w, dir, pkgName, value) {
312
457
  await put(w, join(dir, 'src', 'index.test.ts'), smokeTest(value));
313
458
  }
314
459
  /**
315
- * Write/refresh the Claude Code Stop hook unless opted out (`hook === false`),
316
- * using the repo's detected package manager (b7). Records the file when it
317
- * changed, else a no-op note in `skipped` when one is provided.
460
+ * Write/refresh the agent hooks unless opted out (`hook === false`), remove any
461
+ * named for removal, for the selected harnesses (default: detected) and the
462
+ * repo's detected package manager. Records each file when it changed, else a
463
+ * no-op note in `skipped` when one is provided.
464
+ *
465
+ * `--no-hook` and `--remove-hook` compose rather than conflict: the first says
466
+ * "write nothing", the second "take this one out", and together they mean
467
+ * "remove the gate and leave the rest of the setup untouched" — which is the
468
+ * whole point of being able to drop a hook after the fact.
469
+ */
470
+ async function writeHook(w, options, skipped) {
471
+ const remove = options.removeHooks ?? [];
472
+ if (options.hook === false && remove.length === 0)
473
+ return;
474
+ const hooks = options.hook === false ? [] : options.hooks;
475
+ const result = await writeHooks(w.cwd, {
476
+ dryRun: w.dryRun,
477
+ ...(hooks ? { hooks } : {}),
478
+ ...(remove.length > 0 ? { remove } : {}),
479
+ ...(options.harnesses ? { harnesses: options.harnesses } : {}),
480
+ });
481
+ record(w, result.files, skipped);
482
+ }
483
+ /**
484
+ * Write the bundled skills for every selected harness that cannot install them
485
+ * as a plugin. Only Cursor needs this today; Claude Code gets the same two
486
+ * skills from `.claude-plugin/`, installed once for every repo.
487
+ *
488
+ * Unlike the hooks this is not covered by `--no-hook`: that flag turns off the
489
+ * mechanical gate, and a skill gates nothing.
318
490
  */
319
- async function writeHook(w, hook, skipped) {
320
- if (hook === false)
491
+ async function writeSkills(w, options, skipped) {
492
+ const harnesses = options.harnesses ?? detectHarnesses(w.cwd);
493
+ if (!harnesses.includes('cursor'))
321
494
  return;
322
- const result = await writeStopHook(w.cwd, { dryRun: w.dryRun });
323
- if (result.changed)
324
- w.written.push(result.path);
325
- else
326
- skipped?.push(`${CLAUDE_SETTINGS_FILE} (Stop hook unchanged)`);
495
+ record(w, await writeCursorSkills(w.cwd, { dryRun: w.dryRun }), skipped);
496
+ }
497
+ /**
498
+ * Fold a writer's file list into the run's written/removed/skipped report. A
499
+ * deleted file is a change, but not a *write* — counting it as one would have
500
+ * `--remove-hook gate` report that it wrote a file it actually took away.
501
+ */
502
+ function record(w, files, skipped) {
503
+ for (const f of files) {
504
+ if (!f.changed)
505
+ skipped?.push(`${f.path} (${f.removed === true ? 'absent' : 'unchanged'})`);
506
+ else if (f.removed === true)
507
+ w.removed.push(f.path);
508
+ else
509
+ w.written.push(f.path);
510
+ }
327
511
  }
328
512
  // ----------------------------------------------------------------------------
329
513
  // New-project mode
@@ -361,7 +545,7 @@ async function writeNewScaffold(w, s) {
361
545
  * into `.claude/settings.json` idempotently rather than overwriting.
362
546
  */
363
547
  async function planCollisions(s, cwd) {
364
- const plan = { cwd, dryRun: true, written: [] };
548
+ const plan = { cwd, dryRun: true, written: [], removed: [] };
365
549
  await writeNewScaffold(plan, s);
366
550
  return plan.written.filter((rel) => existsSync(join(cwd, rel)));
367
551
  }
@@ -388,7 +572,7 @@ function resolveNewScaffold(options, cwd) {
388
572
  };
389
573
  }
390
574
  /**
391
- * Overwrite protection (D4): unless `force`, refuse rather than clobber any file
575
+ * Overwrite protection: unless `force`, refuse rather than clobber any file
392
576
  * the scaffold would write. Planning on a dry writer first means a collision
393
577
  * bails before a single real byte is written — the refusal writes nothing.
394
578
  */
@@ -412,14 +596,24 @@ function reportNew(stdout, shape, w, cwd) {
412
596
  async function initNew(options, cwd) {
413
597
  const scaffold = resolveNewScaffold(options, cwd);
414
598
  await assertNoCollisions(scaffold, cwd, options.force ?? false);
415
- const w = { cwd, dryRun: options.dryRun ?? false, written: [] };
599
+ const w = { cwd, dryRun: options.dryRun ?? false, written: [], removed: [] };
416
600
  await writeNewScaffold(w, scaffold);
417
- // Claude Code Stop hook (opt-out; step 12). A fresh project has no PM lockfile
601
+ // Claude Code Stop hook (opt-out). A fresh project has no PM lockfile
418
602
  // yet, so it resolves to the `pnpm` default — matching the generated scripts.
419
- await writeHook(w, options.hook);
603
+ await writeHook(w, options);
604
+ await writeSkills(w, options);
420
605
  if (options.stdout)
421
606
  reportNew(options.stdout, scaffold.shape, w, cwd);
422
- return { mode: 'new', shape: scaffold.shape, written: w.written, skipped: [], disabled: [], grandfathered: [], exitCode: 0 };
607
+ return {
608
+ mode: 'new',
609
+ shape: scaffold.shape,
610
+ written: w.written,
611
+ removed: w.removed,
612
+ skipped: [],
613
+ disabled: [],
614
+ grandfathered: [],
615
+ exitCode: 0,
616
+ };
423
617
  }
424
618
  // ----------------------------------------------------------------------------
425
619
  // Existing-project mode
@@ -433,9 +627,9 @@ async function readIfExists(path) {
433
627
  }
434
628
  }
435
629
  // ----------------------------------------------------------------------------
436
- // Publish-ready bundle (step 9): the library path scaffolds the opt-in artifact
630
+ // Publish-ready bundle: the library path scaffolds the opt-in artifact
437
631
  // checks into `checkride.config.json` so a buildable library orders itself out
438
- // of the box (build → pack/smoke/attw/publint/snippets, D4).
632
+ // of the box (build → pack/smoke/attw/publint/snippets).
439
633
  // ----------------------------------------------------------------------------
440
634
  /**
441
635
  * The publish-ready bundle, in wave order. Each slot's value is the adapter
@@ -446,7 +640,7 @@ async function readIfExists(path) {
446
640
  const PUBLISH_BUNDLE_SLOTS = ['build', 'publint', 'attw', 'pack', 'smoke', 'snippets'];
447
641
  /** `--add` values that expand to the whole bundle. */
448
642
  const PUBLISH_BUNDLE_ALIASES = new Set(['publish', 'bundle']);
449
- /** Printed (and recorded in `skipped`) when snippets is requested but no tagged fence exists (Q12). */
643
+ /** Printed (and recorded in `skipped`) when snippets is requested but no tagged fence exists. */
450
644
  const SNIPPETS_POINTER = 'snippets-dist: no tagged fences found — add a `<!-- snippet: check -->` marker above a ' +
451
645
  'ts/typescript fence in README.md or docs/*.md to enable the doc-snippet typecheck';
452
646
  /**
@@ -465,9 +659,9 @@ function readInitManifest(cwd) {
465
659
  }
466
660
  /**
467
661
  * True when README.md or a non-recursive docs/*.md carries at least one tagged
468
- * snippet fence — the signal that gates scaffolding `snippets-dist` (Q12).
469
- * Reuses step 7's pure discovery/plan primitives so the detection matches what
470
- * the slot itself checks.
662
+ * snippet fence — the signal that gates scaffolding `snippets-dist`.
663
+ * Reuses the snippets module's pure discovery/plan primitives so the detection
664
+ * matches what the slot itself checks.
471
665
  */
472
666
  async function hasTaggedSnippets(cwd) {
473
667
  let docsEntries = [];
@@ -488,7 +682,7 @@ async function hasTaggedSnippets(cwd) {
488
682
  }
489
683
  /**
490
684
  * Fold the requested publish slots into config entries, gating `build` on a
491
- * build script and `snippets` (→ `snippets-dist`) on a tagged fence (Q12) — the
685
+ * build script and `snippets` (→ `snippets-dist`) on a tagged fence — the
492
686
  * two slots that would otherwise stand down as a skip (`build`) or hard-error on
493
687
  * zero snippets (`snippets`).
494
688
  */
@@ -540,16 +734,80 @@ function splitAdd(add, manifest) {
540
734
  }
541
735
  return { publishRequested, fileAdds };
542
736
  }
737
+ /**
738
+ * The stanza's active-check list: the selection a default `checkride` run
739
+ * makes, resolved config-aware from disk. `inventory()` is the wrong input
740
+ * here — it has detection semantics and never reads checkride.config.json, so
741
+ * it under-reports the gate (opt-in slots the config opts in, custom checks)
742
+ * to the exact audience the stanza exists to inform. Mirrors `doctor`'s
743
+ * `defaultActive` derivation.
744
+ */
745
+ function activeCheckSlots(cwd, slots, adapters) {
746
+ const resolved = resolveChecks({ slots, adapters, config: loadConfig(cwd), cwd });
747
+ return selectChecks(resolved, {})
748
+ .filter((r) => r.adapter !== null)
749
+ .map((r) => r.slot);
750
+ }
751
+ /**
752
+ * The voice-exemplars directory the repo's config names on its `prose` entry,
753
+ * or `null`. Read fresh at every stanza build rather than threaded from a
754
+ * caller, so the stanza and the config it describes cannot drift apart within
755
+ * one run — `initExisting` writes config between its stanza guard and its
756
+ * stanza write.
757
+ */
758
+ function proseExemplarsDir(cwd) {
759
+ const entry = loadConfig(cwd)?.checks?.['prose'];
760
+ return entry && typeof entry === 'object' && 'use' in entry && typeof entry.exemplars === 'string'
761
+ ? entry.exemplars
762
+ : null;
763
+ }
764
+ const STANZA_REFUSALS = {
765
+ edited: 'it has been edited since checkride wrote it',
766
+ unstamped: 'it predates the stanza stamp and matches no wording checkride has released, so it has been changed since it was written',
767
+ };
768
+ /**
769
+ * Refuse to refresh a stanza that is not checkride's own output, so a repo's
770
+ * edge-case additions survive the next `init`/`agent-setup` instead of being
771
+ * silently overwritten. `opts.force` overrides.
772
+ *
773
+ * Called before either entry point writes anything, so a refusal leaves the repo
774
+ * exactly as it found it (the same rule `assertNoCollisions` follows in new
775
+ * mode). In existing mode that puts it ahead of the config write, so `body` here
776
+ * reflects the config as it is on disk now rather than as this run will leave
777
+ * it — which no longer costs anything: a stamped stanza is checked against its
778
+ * own stamp, and an unstamped one against a registry of released wordings that
779
+ * ignores the active-checks line entirely. Neither reads `body`'s slot list.
780
+ *
781
+ * Hook management deliberately does not come through here — see the `hooks`
782
+ * command, which never touches AGENTS.md and so has nothing to refuse.
783
+ */
784
+ async function assertStanzaUnedited(cwd, body, opts) {
785
+ if (opts.force === true)
786
+ return;
787
+ const existing = await readIfExists(join(cwd, 'AGENTS.md'));
788
+ if (existing === null)
789
+ return;
790
+ const state = inspectStanza(existing, body);
791
+ if (state === 'absent' || state === 'pristine')
792
+ return;
793
+ const remedy = state === 'edited'
794
+ ? 'Move your additions outside the markers — checkride never rewrites what is outside them — or re-run with --force to discard them and refresh.'
795
+ : 'If you have not edited it, re-run with --force: the refreshed stanza carries a stamp, and later runs tell an edit from a refresh on their own.';
796
+ throw new Error(`refusing to overwrite the checkride stanza in AGENTS.md: ${STANZA_REFUSALS[state]}.\n ${remedy}`);
797
+ }
543
798
  /**
544
799
  * Write (or refresh) the AGENTS.md stanza for `slots`, idempotently: it writes
545
800
  * only when the applied stanza differs from the current file, records the outcome
546
801
  * on `w`/`skipped`, and honours dry-run. Shared by `initExisting` and
547
802
  * `runAgentSetup` (both create-or-refresh the same stanza).
803
+ *
804
+ * It rewrites the marked region unconditionally; whether that is allowed is
805
+ * {@link assertStanzaUnedited}'s call, made before either caller writes anything.
548
806
  */
549
- async function writeAgentsStanza(w, cwd, slots, skipped) {
807
+ async function writeAgentsStanza(w, cwd, slots, pm, skipped) {
550
808
  const agentsPath = join(cwd, 'AGENTS.md');
551
809
  const existing = await readIfExists(agentsPath);
552
- const nextAgents = applyStanza(existing ?? '', buildStanza(slots));
810
+ const nextAgents = applyStanza(existing ?? '', buildStanza(slots, pm, proseExemplarsDir(cwd)));
553
811
  if (nextAgents !== existing) {
554
812
  if (!w.dryRun)
555
813
  await writeFile(agentsPath, nextAgents);
@@ -566,12 +824,19 @@ const ADD_CONFIGS = {
566
824
  types: [['shared/tsconfig.base.json', 'tsconfig.base.json'], ['flat/tsconfig.json', 'tsconfig.json']],
567
825
  format: [['shared/prettierrc.json', '.prettierrc.json']],
568
826
  lint: [['shared/oxlintrc.json', '.oxlintrc.json']],
827
+ // `struct` scaffolds the boundary rule and stops. New-mode `init` writes all
828
+ // four (`writeSharedStatic`), and that is a different bargain: there checkride
829
+ // is creating the package, so its house style has nobody to override. Here the
830
+ // repo already exists and has already decided. `no-class`, `no-default-export`
831
+ // and `require-js-extension` are style and module-system opinions — a repo
832
+ // with classes, default exports, or bundler resolution goes red on adoption
833
+ // for writing the code it always wrote, which reads as checkride being broken.
834
+ // `no-deep-sibling-import` is the one that makes `struct` the slot it is, and
835
+ // failing it means what it says. The other three stay one copy away, in
836
+ // `docs/deep-modules.md`.
569
837
  struct: [
570
838
  ['shared/sgconfig.yml', 'sgconfig.yml'],
571
- ['shared/rules/no-class.yml', 'rules/no-class.yml'],
572
- ['shared/rules/no-default-export.yml', 'rules/no-default-export.yml'],
573
839
  ['shared/rules/no-deep-sibling-import.yml', 'rules/no-deep-sibling-import.yml'],
574
- ['shared/rules/require-js-extension.yml', 'rules/require-js-extension.yml'],
575
840
  ],
576
841
  dead: [['flat/fallow.toml', 'fallow.toml']],
577
842
  // dupes/health are the same fallow tool + config as `dead`, just other slots.
@@ -580,6 +845,23 @@ const ADD_CONFIGS = {
580
845
  test: [['shared/vitest.config.ts.template', 'vitest.config.ts']],
581
846
  docs: [['shared/markdownlint-cli2.jsonc', '.markdownlint-cli2.jsonc']],
582
847
  spell: [['shared/cspell.json', 'cspell.json']],
848
+ // `prose` is the one slot whose config alone scaffolds nothing usable: vale's
849
+ // .vale.ini is a pointer at a StylesPath, and an empty StylesPath lints
850
+ // nothing. The rules are the deliverable, so they come with it. They are the
851
+ // repo's from the moment they land — plain YAML, no `vale sync`, no upstream
852
+ // to track — which is also why `struct`'s restraint does not apply here:
853
+ // ast-grep's extra rules assert what the repo's code already is, while these
854
+ // assert nothing about code at all, and a prose rule the author disagrees
855
+ // with is deleted in one line.
856
+ prose: [
857
+ ['shared/vale.ini', '.vale.ini'],
858
+ ['shared/styles/Repo/Drift.yml', '.vale/styles/Repo/Drift.yml'],
859
+ ['shared/styles/Repo/Latin.yml', '.vale/styles/Repo/Latin.yml'],
860
+ ['shared/styles/Repo/LyHyphen.yml', '.vale/styles/Repo/LyHyphen.yml'],
861
+ ['shared/styles/Repo/Minted.yml', '.vale/styles/Repo/Minted.yml'],
862
+ ['shared/styles/Repo/ThereIs.yml', '.vale/styles/Repo/ThereIs.yml'],
863
+ ['shared/styles/Repo/Weasel.yml', '.vale/styles/Repo/Weasel.yml'],
864
+ ],
583
865
  };
584
866
  /** `--add <slots>`: scaffold blessed-default configs for the named empty slots. */
585
867
  async function addConfigs(w, add, skipped) {
@@ -625,7 +907,7 @@ async function addCheckAlias(w, skipped) {
625
907
  w.written.push('package.json (added check script)');
626
908
  }
627
909
  /**
628
- * Classify each failing adopted slot. `--baseline` (step 6 / c10) grandfathers a
910
+ * Classify each failing adopted slot. `--baseline` grandfathers a
629
911
  * failing *fingerprintable* slot (kept enabled; the baseline masks its debt and
630
912
  * the ratchet carries the cleanup); a failing slot with no extractor can't be
631
913
  * grandfathered, so it still falls back to a `false` disable. Without
@@ -652,7 +934,7 @@ async function captureBaselineIfNeeded(options, cwd, grandfathered, dryRun) {
652
934
  }
653
935
  /**
654
936
  * Write checkride.config.json for the adopted tools (disabled failures as
655
- * `false`) plus the publish-ready bundle (step 9); never clobber an existing one.
937
+ * `false`) plus the publish-ready bundle; never clobber an existing one.
656
938
  */
657
939
  async function writeExistingConfig(w, cwd, adopted, disabledSet, bundleChecks, skipped) {
658
940
  if (existsSync(join(cwd, 'checkride.config.json'))) {
@@ -667,7 +949,7 @@ async function writeExistingConfig(w, cwd, adopted, disabledSet, bundleChecks, s
667
949
  checks[i.slot] = i.adapter;
668
950
  }
669
951
  // The opt-in publish bundle follows the adopted default slots; explicit
670
- // entries opt these slots into the run (D4 — the library orders itself).
952
+ // entries opt these slots into the run (the library orders itself).
671
953
  for (const [slot, adapter] of Object.entries(bundleChecks))
672
954
  checks[slot] = adapter;
673
955
  const config = { $schema: configSchemaUrl(productVersion()), checks };
@@ -708,7 +990,7 @@ function reportExisting(stdout, adopted, w, grandfathered, disabled) {
708
990
  }
709
991
  }
710
992
  /**
711
- * Plan the publish-ready bundle (step 9) for existing-repo init. Only meaningful
993
+ * Plan the publish-ready bundle for existing-repo init. Only meaningful
712
994
  * when a config will actually be written: `writeExistingConfig` never clobbers an
713
995
  * existing `checkride.config.json`, so an existing config means the bundle has
714
996
  * nowhere to land. The bundle is scaffolded *enabled* — not probed/disabled like
@@ -733,8 +1015,12 @@ async function initExisting(options, cwd) {
733
1015
  const adapters = options.adapters ?? ADAPTERS;
734
1016
  const slots = options.slots ?? SLOTS;
735
1017
  const manifest = readInitManifest(cwd);
736
- const w = { cwd, dryRun: options.dryRun ?? false, written: [] };
1018
+ const w = { cwd, dryRun: options.dryRun ?? false, written: [], removed: [] };
737
1019
  const skipped = [];
1020
+ const pm = detectPackageManager({ cwd });
1021
+ // Before the first write: a stanza this repo has customized stops the run
1022
+ // rather than being clobbered by the refresh below.
1023
+ await assertStanzaUnedited(cwd, buildStanza(activeCheckSlots(cwd, slots, adapters), pm, proseExemplarsDir(cwd)), options);
738
1024
  // Route --add: publish slots (and the library path) opt into the bundle; the
739
1025
  // rest scaffold blessed configs before inventory, so they're detected this run.
740
1026
  const { publishRequested, fileAdds } = splitAdd(options.add ?? [], manifest);
@@ -746,24 +1032,35 @@ async function initExisting(options, cwd) {
746
1032
  const failing = new Set(await probe(adopted.map((i) => i.slot), cwd));
747
1033
  const { grandfathered, disabled } = resolveAdoptionPlan(adopted, failing, options.baseline ?? false);
748
1034
  await captureBaselineIfNeeded(options, cwd, grandfathered, w.dryRun);
749
- // Publish-ready bundle (step 9): planned only when a config will be written.
1035
+ // Publish-ready bundle: planned only when a config will be written.
750
1036
  const bundle = await planInitBundle(publishRequested, manifest, cwd);
751
1037
  if (bundle.snippetsPointer)
752
1038
  skipped.push(bundle.snippetsPointer);
753
1039
  await writeExistingConfig(w, cwd, adopted, new Set(disabled), bundle.checks, skipped);
754
1040
  await ensureGitignore(w, cwd, skipped);
755
- // package.json: add the `check: checkride` alias (decision 8) if missing.
1041
+ // package.json: add the `check: checkride` alias if missing.
756
1042
  await addCheckAlias(w, skipped);
757
- // AGENTS.md stanza (create or refresh, idempotent).
758
- await writeAgentsStanza(w, cwd, adopted.map((i) => i.slot), skipped);
1043
+ // AGENTS.md stanza (create or refresh, idempotent), derived from the config
1044
+ // written above so it reports the gate as configured, not as detected.
1045
+ await writeAgentsStanza(w, cwd, activeCheckSlots(cwd, slots, adapters), pm, skipped);
759
1046
  await writeClaudePointer(w, cwd, skipped);
760
- // Claude Code Stop hook (opt-out; step 12), using the repo's detected PM (b7).
761
- await writeHook(w, options.hook, skipped);
1047
+ // Claude Code Stop hook (opt-out), using the repo's detected PM.
1048
+ await writeHook(w, options, skipped);
1049
+ await writeSkills(w, options, skipped);
762
1050
  if (options.stdout) {
763
1051
  reportExisting(options.stdout, adopted, w, grandfathered, disabled);
764
1052
  reportInitBundle(options.stdout, bundle);
765
1053
  }
766
- return { mode: 'existing', shape: null, written: w.written, skipped, disabled, grandfathered, exitCode: 0 };
1054
+ return {
1055
+ mode: 'existing',
1056
+ shape: null,
1057
+ written: w.written,
1058
+ removed: w.removed,
1059
+ skipped,
1060
+ disabled,
1061
+ grandfathered,
1062
+ exitCode: 0,
1063
+ };
767
1064
  }
768
1065
  // ----------------------------------------------------------------------------
769
1066
  // Entry
@@ -780,25 +1077,32 @@ export async function runInit(options) {
780
1077
  /**
781
1078
  * `checkride agent-setup` — wire the agent contract into an *existing* repo
782
1079
  * without a full `init`: the `check` alias the hook resolves to, the AGENTS.md
783
- * stanza (the human-readable contract), and the Claude Code Stop hook (the
784
- * mechanical gate). Every write is additive and idempotent — a second run is a
785
- * no-op — and the hook is opt-out (`hook: false`).
1080
+ * stanza (the human-readable contract), the stop-gate hooks for every detected
1081
+ * harness (the mechanical gate), and the skills a harness cannot get from the
1082
+ * bundled plugin. Every write is additive and idempotent — a second run is a
1083
+ * no-op — and the hooks are opt-out (`hook: false`).
786
1084
  */
787
1085
  export async function runAgentSetup(options) {
788
1086
  const cwd = options.cwd ?? process.cwd();
789
1087
  const slots = options.slots ?? SLOTS;
790
1088
  const adapters = options.adapters ?? ADAPTERS;
791
- const w = { cwd, dryRun: options.dryRun ?? false, written: [] };
1089
+ const w = { cwd, dryRun: options.dryRun ?? false, written: [], removed: [] };
792
1090
  const skipped = [];
1091
+ const stanzaSlots = activeCheckSlots(cwd, slots, adapters);
1092
+ const pm = detectPackageManager({ cwd });
1093
+ // Before the first write: a stanza this repo has customized stops the run
1094
+ // rather than being clobbered by the refresh below.
1095
+ await assertStanzaUnedited(cwd, buildStanza(stanzaSlots, pm, proseExemplarsDir(cwd)), options);
793
1096
  // The `check` alias the hook's `<pm> run check` resolves to (never clobbers).
794
1097
  await addCheckAlias(w, skipped);
795
- // AGENTS.md stanza for the currently-adopted slots (create or refresh).
796
- const adopted = inventory({ cwd, slots, adapters }).filter((i) => i.status === 'adopted');
797
- await writeAgentsStanza(w, cwd, adopted.map((i) => i.slot), skipped);
798
- await writeHook(w, options.hook, skipped);
1098
+ // AGENTS.md stanza for the checks the default run selects (create or refresh).
1099
+ await writeAgentsStanza(w, cwd, stanzaSlots, pm, skipped);
1100
+ await writeHook(w, options, skipped);
1101
+ await writeSkills(w, options, skipped);
799
1102
  if (options.stdout) {
800
- options.stdout.write(`checkride agent-setup: wrote ${w.written.length} file(s)${w.dryRun ? ' [dry run]' : ''}.\n`);
1103
+ const removed = w.removed.length > 0 ? `, removed ${w.removed.length}` : '';
1104
+ options.stdout.write(`checkride agent-setup: wrote ${w.written.length} file(s)${removed}${w.dryRun ? ' [dry run]' : ''}.\n`);
801
1105
  }
802
- return { written: w.written, skipped, exitCode: 0 };
1106
+ return { written: w.written, removed: w.removed, skipped, exitCode: 0 };
803
1107
  }
804
1108
  //# sourceMappingURL=init.js.map