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
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The Cursor hook writer — `.cursor/hooks.json` plus the scripts its entries
3
+ * invoke.
4
+ *
5
+ * Same contract as the Claude writer (`./claude.ts`): sentinel-keyed identity,
6
+ * in-place refresh, every unrelated key preserved, re-applying is a no-op. The
7
+ * schema is what differs, and it differs enough that sharing the merge would
8
+ * cost more than it saved:
9
+ *
10
+ * - Cursor requires a top-level `"version": 1`.
11
+ * - An event maps to a flat list of `{command, matcher?, timeout?}` entries —
12
+ * there is no group wrapper and no `type` discriminator, so a hook is one
13
+ * object where Claude Code needs two levels.
14
+ * - The events are lowercase and differently cut: `stop`, `afterFileEdit`
15
+ * (purpose-built, no matcher) and `preToolUse` (matched against Cursor's own
16
+ * tool names — `Shell`, `Read`, `Write`, `Grep`, `Delete`, `Task`, `MCP:*`).
17
+ * - Cursor cuts the shell out as its own pair of events, `beforeShellExecution`
18
+ * and `afterShellExecution`, whose matchers run against the **command string**
19
+ * rather than a tool name. Both guards take a second entry there to cover what
20
+ * the file tools never see; the gate does not, since it is not per-call.
21
+ *
22
+ * A hook name therefore maps to one *or more* entries, on different events. They
23
+ * share a name because they are one guard — `--remove-hook protect` takes out
24
+ * both — and share a script, since the script branches on `hook_event_name`.
25
+ *
26
+ * The gate entry carries three fields the Claude writer has no equivalent for —
27
+ * `timeout`, `loop_limit` and `failClosed` — because Cursor's defaults for all
28
+ * three end a turn that checkride would keep blocking. See {@link hookSpecs}.
29
+ */
30
+ import { CURSOR_HOOKS_FILE } from '../gate.js';
31
+ import { GATE_TIMEOUT_SECONDS, HOOK_NAMES, writeHarnessHooks } from './harness.js';
32
+ /**
33
+ * Project-shared Cursor hook config (committed), relative to the repo root.
34
+ * Declared in `../gate.js`, which reads it to detect a native Cursor gate, and
35
+ * re-exported here so this module stays the one place a caller looks for it.
36
+ */
37
+ export { CURSOR_HOOKS_FILE };
38
+ const GATE_SCRIPT_FILE = '.cursor/hooks/checkride-gate.sh';
39
+ const DIRTY_SCRIPT_FILE = '.cursor/hooks/checkride-dirty.sh';
40
+ const PROTECT_SCRIPT_FILE = '.cursor/hooks/checkride-protect.cjs';
41
+ const HARNESS = 'cursor';
42
+ /** The schema version Cursor's loader expects at the top of the file. */
43
+ const SCHEMA_VERSION = 1;
44
+ /**
45
+ * `sh` (not the exec bit) runs the script so a clone that lost file modes still
46
+ * gates; `CURSOR_PROJECT_DIR` anchors it when the session cwd is a subdirectory.
47
+ *
48
+ * **Unverified against a live Cursor.** Cursor documents `command` as "a shell
49
+ * string, an absolute path, or a relative path", but every documented example is
50
+ * a bare path, and it states separately that project hooks already run from the
51
+ * project root. If Cursor ever spawns these without a shell, `${…}` stays
52
+ * literal, the spawn fails, and Cursor's fail-open default ends the turn with no
53
+ * signal at all — the one failure mode `failClosed` cannot cover, because the
54
+ * hook it would guard never started. `failClosed` is set regardless; if that
55
+ * turns out not to catch it, drop both the interpreter and the variable and
56
+ * write the bare `.cursor/hooks/checkride-gate.sh` the docs show, accepting the
57
+ * dependence on the exec bit. See docs/cursor.md.
58
+ */
59
+ const run = (interpreter, file) => `${interpreter} "\${CURSOR_PROJECT_DIR:-.}/${file}"`;
60
+ /**
61
+ * Cursor's file-mutating tool names. Cursor has no separate `Edit` tool — `Write`
62
+ * covers both creating and modifying — and `Delete` can remove an accounting
63
+ * file just as effectively as a write can overwrite it.
64
+ */
65
+ const EDIT_TOOLS = 'Write|Delete';
66
+ /**
67
+ * Commands `protect` bothers to parse: the ones that name an accounting path at
68
+ * all. `beforeShellExecution` matches its `matcher` against the **full command
69
+ * string**, and that is the whole reason a shell guard is affordable here.
70
+ *
71
+ * checkride's standing objection to matching a shell tool is that a guard firing
72
+ * on a wrong parse is worse than a known hole. Filtering first turns the blast
73
+ * radius from "every command the agent runs" into "commands that already mention
74
+ * `checkride.baseline.json` or `.check`" — and inside that set the script still
75
+ * denies only on demonstrated write intent. Reads land here too and are allowed;
76
+ * they have to be, because triage reads these files.
77
+ */
78
+ const ACCOUNTING_PATHS = 'checkride\\.baseline\\.json|\\.check\\b';
79
+ /**
80
+ * Commands `dirty` reads as having written a file: a `>`/`>>` redirect, or one of
81
+ * the mutating verbs. The negative lookahead on `&` is load-bearing — without it
82
+ * the `2>&1` on the end of half the agent's commands marks every turn dirty and
83
+ * the gate stops skipping anything.
84
+ *
85
+ * Heuristic on purpose, and biased toward matching: a false positive costs one
86
+ * pipeline run on a turn that changed nothing, while a false negative is the
87
+ * status quo — a shell-written file that no gate ever sees. Only the first of
88
+ * those is recoverable by running the gate.
89
+ */
90
+ const SHELL_WRITES = '>>?\\s*[^&\\s>]' +
91
+ '|(^|[\\s;&|(])(rm|mv|cp|tee|sed|truncate|dd|patch|install|shred|ln)(\\s|$)' +
92
+ '|(^|[\\s;&|(])git\\s+(apply|checkout|restore|stash|clean|reset|revert|switch|merge|rebase|pull|cherry-pick)(\\s|$)';
93
+ /**
94
+ * Cursor's defaults are tuned for observers; the gate is not one, so all three
95
+ * of its non-command fields are set against the default rather than with it:
96
+ *
97
+ * - `timeout` — a full pipeline run is minutes, and the platform default is not.
98
+ * - `loop_limit: null` — the default of **5** stops the auto-followup after five
99
+ * turns, at which point a red repo finishes anyway. Claude Code re-blocks
100
+ * indefinitely; without this the two harnesses are not the same gate, and the
101
+ * Cursor one degrades into five nudges.
102
+ * - `failClosed: true` — Cursor's default is fail-*open*: a hook that crashes,
103
+ * times out, or emits unparseable JSON lets the turn end silently. That is the
104
+ * vacuous green the whole contract exists to prevent (docs/contract.md).
105
+ *
106
+ * The two guards keep Cursor's fail-open default on purpose — see `dirty` and
107
+ * `protect` in `./scripts.ts` for why each must never block an edit.
108
+ */
109
+ function hookSpecs() {
110
+ return [
111
+ {
112
+ name: 'gate',
113
+ event: 'stop',
114
+ timeout: GATE_TIMEOUT_SECONDS,
115
+ loopLimit: null,
116
+ failClosed: true,
117
+ sentinels: [GATE_SCRIPT_FILE],
118
+ command: run('sh', GATE_SCRIPT_FILE),
119
+ },
120
+ {
121
+ // `afterFileEdit` rather than `postToolUse`: it is purpose-built for "a
122
+ // file changed", needs no matcher, and so cannot drift when Cursor renames
123
+ // a tool.
124
+ name: 'dirty',
125
+ event: 'afterFileEdit',
126
+ sentinels: [DIRTY_SCRIPT_FILE],
127
+ command: run('sh', DIRTY_SCRIPT_FILE),
128
+ },
129
+ {
130
+ // The shell half of the edit marker. `afterFileEdit` sees only the file
131
+ // tools, so a turn that wrote through the shell alone used to skip the
132
+ // gate entirely. The matcher does all the deciding here — the script is
133
+ // the same unconditional `touch` either event reaches it through.
134
+ name: 'dirty',
135
+ event: 'afterShellExecution',
136
+ matcher: SHELL_WRITES,
137
+ sentinels: [DIRTY_SCRIPT_FILE],
138
+ command: run('sh', DIRTY_SCRIPT_FILE),
139
+ },
140
+ {
141
+ name: 'protect',
142
+ event: 'preToolUse',
143
+ matcher: EDIT_TOOLS,
144
+ sentinels: [PROTECT_SCRIPT_FILE],
145
+ command: run('node', PROTECT_SCRIPT_FILE),
146
+ },
147
+ {
148
+ // The shell half of the same guard. A tool-name matcher cannot see
149
+ // `echo … > checkride.baseline.json`, because that is a shell call and not
150
+ // a `Write`; `beforeShellExecution` is where it becomes visible. Same
151
+ // script, same fail-open default — see `protectScript` in `./scripts.ts`
152
+ // for why the parse behind it refuses to guess.
153
+ name: 'protect',
154
+ event: 'beforeShellExecution',
155
+ matcher: ACCOUNTING_PATHS,
156
+ sentinels: [PROTECT_SCRIPT_FILE],
157
+ command: run('node', PROTECT_SCRIPT_FILE),
158
+ },
159
+ ];
160
+ }
161
+ /** True when an entry belongs to `spec` (matched by any of its sentinels). */
162
+ function isSpecHook(spec, hook) {
163
+ const command = hook.command;
164
+ return typeof command === 'string' && spec.sentinels.some((s) => command.includes(s));
165
+ }
166
+ /** The event's entry list, tolerating a malformed (non-array) config value. */
167
+ function eventHooks(config, event) {
168
+ const entries = config.hooks?.[event];
169
+ return Array.isArray(entries) ? entries.map((h) => ({ ...h })) : [];
170
+ }
171
+ /**
172
+ * The canonical fields for `spec`, omitting the optional ones it doesn't set.
173
+ * `loopLimit` is compared against `undefined` rather than truth-tested, because
174
+ * the value checkride writes is `null`.
175
+ */
176
+ function specFields(spec) {
177
+ return {
178
+ command: spec.command,
179
+ ...(spec.matcher !== undefined ? { matcher: spec.matcher } : {}),
180
+ ...(spec.timeout !== undefined ? { timeout: spec.timeout } : {}),
181
+ ...(spec.loopLimit !== undefined ? { loop_limit: spec.loopLimit } : {}),
182
+ ...(spec.failClosed !== undefined ? { failClosed: spec.failClosed } : {}),
183
+ };
184
+ }
185
+ /**
186
+ * Merge one hook into a parsed config, idempotently. An existing entry (found by
187
+ * sentinel) has its fields refreshed in place, keeping any key checkride does not
188
+ * own; otherwise a new entry is appended.
189
+ */
190
+ function applyHook(config, spec) {
191
+ const hooks = { ...config.hooks };
192
+ const entries = eventHooks(config, spec.event);
193
+ const idx = entries.findIndex((h) => isSpecHook(spec, h));
194
+ if (idx >= 0) {
195
+ entries[idx] = { ...entries[idx], ...specFields(spec) };
196
+ }
197
+ else {
198
+ entries.push(specFields(spec));
199
+ }
200
+ return { ...config, hooks: { ...hooks, [spec.event]: entries } };
201
+ }
202
+ /**
203
+ * Merge the selected hooks into a parsed config, seeding `version` when absent.
204
+ * An existing `version` is never overwritten: if a future Cursor bumps it, the
205
+ * repo's value is the one that matches the rest of the file.
206
+ */
207
+ export function applyCursorHooks(config = {}, names = HOOK_NAMES) {
208
+ const seeded = config.version === undefined ? { version: SCHEMA_VERSION, ...config } : config;
209
+ return hookSpecs()
210
+ .filter((spec) => names.includes(spec.name))
211
+ .reduce((acc, spec) => applyHook(acc, spec), seeded);
212
+ }
213
+ /**
214
+ * Strip one hook from a parsed config. An event left with no entries loses its
215
+ * key; `version` and every unrelated entry stay. Cursor's schema is flat, so
216
+ * there is no empty group to prune — the Claude writer's extra step has no
217
+ * counterpart here.
218
+ */
219
+ function removeHook(config, spec) {
220
+ const hooks = { ...config.hooks };
221
+ const entries = eventHooks(config, spec.event).filter((h) => !isSpecHook(spec, h));
222
+ if (entries.length > 0)
223
+ hooks[spec.event] = entries;
224
+ else
225
+ delete hooks[spec.event];
226
+ return { ...config, hooks };
227
+ }
228
+ /** Remove the named hooks from a parsed config — the inverse of {@link applyCursorHooks}. */
229
+ export function removeCursorHooks(config = {}, names = HOOK_NAMES) {
230
+ return hookSpecs()
231
+ .filter((spec) => names.includes(spec.name))
232
+ .reduce((acc, spec) => removeHook(acc, spec), config);
233
+ }
234
+ const SPEC = {
235
+ name: HARNESS,
236
+ configFile: CURSOR_HOOKS_FILE,
237
+ // Cursor's config has no field that renders the gate's command in its UI, so
238
+ // the package manager reaches its hooks only through the generated script.
239
+ apply: applyCursorHooks,
240
+ scripts: { gate: GATE_SCRIPT_FILE, dirty: DIRTY_SCRIPT_FILE, protect: PROTECT_SCRIPT_FILE },
241
+ remove: removeCursorHooks,
242
+ };
243
+ /** Write or refresh the selected Cursor hooks in `cwd`. */
244
+ export function writeCursorHooks(cwd, opts) {
245
+ return writeHarnessHooks(cwd, SPEC, opts);
246
+ }
247
+ //# sourceMappingURL=cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/agent-setup/cursor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Filesystem helpers shared by the per-harness hook writers.
3
+ *
4
+ * Every write `agent-setup` makes is idempotent — a second run must report
5
+ * `changed: false` for every file — so the writers never write blind: they read
6
+ * first, compare, and only then touch the disk. `dryRun` computes the same
7
+ * result without writing anything.
8
+ */
9
+ /**
10
+ * One file a hook writer produced, and whether this run actually changed it.
11
+ * `removed` marks a deletion, so a caller reporting the run can tell a file that
12
+ * was written from one that was taken away — both are changes.
13
+ */
14
+ export type HookFile = {
15
+ path: string;
16
+ changed: boolean;
17
+ removed?: boolean;
18
+ };
19
+ export declare function readIfExists(path: string): Promise<string | null>;
20
+ /** Write `content` to `cwd/rel` (creating directories) unless it already matches. */
21
+ export declare function putFile(cwd: string, rel: string, content: string, opts: {
22
+ dryRun: boolean;
23
+ executable?: boolean;
24
+ }): Promise<HookFile>;
25
+ /**
26
+ * Delete `cwd/rel` if it is there. Reports `changed: true` only when a file was
27
+ * actually removed, so a second removal is the no-op every other write here is.
28
+ */
29
+ export declare function dropFile(cwd: string, rel: string, opts: {
30
+ dryRun: boolean;
31
+ }): Promise<HookFile>;
32
+ /**
33
+ * Merge `next` into the JSON config at `cwd/rel` and write it back when it
34
+ * changed. `merge` receives the parsed file — or `undefined` when there is none
35
+ * — and returns the merged value; it must be idempotent.
36
+ *
37
+ * `skipIfMissing` suppresses creating the file at all: a run that only *removes*
38
+ * hooks has nothing to say to a repo that never had that harness configured, and
39
+ * writing `{}` into a `.claude/settings.json` that did not exist would be a
40
+ * confusing way to say "nothing to remove".
41
+ *
42
+ * A malformed file is named in the error, so a consumer sees
43
+ * `invalid .claude/settings.json: <reason>` instead of a bare `SyntaxError`
44
+ * stack (mirrors `invalidConfig` in `config.ts`).
45
+ */
46
+ export declare function putJson<T>(cwd: string, rel: string, merge: (current?: T) => T, opts: {
47
+ dryRun: boolean;
48
+ skipIfMissing?: boolean;
49
+ }): Promise<HookFile>;
50
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/agent-setup/files.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7E,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMvE;AAED,qFAAqF;AACrF,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9C,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACxB,OAAO,CAAC,QAAQ,CAAC,CAKnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,EACzB,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD,OAAO,CAAC,QAAQ,CAAC,CAoBnB"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Filesystem helpers shared by the per-harness hook writers.
3
+ *
4
+ * Every write `agent-setup` makes is idempotent — a second run must report
5
+ * `changed: false` for every file — so the writers never write blind: they read
6
+ * first, compare, and only then touch the disk. `dryRun` computes the same
7
+ * result without writing anything.
8
+ */
9
+ import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
10
+ import { dirname, join } from 'node:path';
11
+ export async function readIfExists(path) {
12
+ try {
13
+ return await readFile(path, 'utf8');
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /** Write `content` to `cwd/rel` (creating directories) unless it already matches. */
20
+ export async function putFile(cwd, rel, content, opts) {
21
+ const path = join(cwd, rel);
22
+ const raw = await readIfExists(path);
23
+ const changed = raw !== content;
24
+ if (changed && !opts.dryRun) {
25
+ await mkdir(dirname(path), { recursive: true });
26
+ await writeFile(path, content);
27
+ if (opts.executable)
28
+ await chmod(path, 0o755);
29
+ }
30
+ return { path: rel, changed };
31
+ }
32
+ /**
33
+ * Delete `cwd/rel` if it is there. Reports `changed: true` only when a file was
34
+ * actually removed, so a second removal is the no-op every other write here is.
35
+ */
36
+ export async function dropFile(cwd, rel, opts) {
37
+ const path = join(cwd, rel);
38
+ const changed = (await readIfExists(path)) !== null;
39
+ if (changed && !opts.dryRun)
40
+ await rm(path, { force: true });
41
+ return { path: rel, changed, removed: true };
42
+ }
43
+ /**
44
+ * Merge `next` into the JSON config at `cwd/rel` and write it back when it
45
+ * changed. `merge` receives the parsed file — or `undefined` when there is none
46
+ * — and returns the merged value; it must be idempotent.
47
+ *
48
+ * `skipIfMissing` suppresses creating the file at all: a run that only *removes*
49
+ * hooks has nothing to say to a repo that never had that harness configured, and
50
+ * writing `{}` into a `.claude/settings.json` that did not exist would be a
51
+ * confusing way to say "nothing to remove".
52
+ *
53
+ * A malformed file is named in the error, so a consumer sees
54
+ * `invalid .claude/settings.json: <reason>` instead of a bare `SyntaxError`
55
+ * stack (mirrors `invalidConfig` in `config.ts`).
56
+ */
57
+ export async function putJson(cwd, rel, merge, opts) {
58
+ const path = join(cwd, rel);
59
+ const raw = await readIfExists(path);
60
+ if (raw === null && opts.skipIfMissing === true)
61
+ return { path: rel, changed: false };
62
+ let current;
63
+ if (raw !== null) {
64
+ try {
65
+ current = JSON.parse(raw);
66
+ }
67
+ catch (err) {
68
+ const reason = err instanceof Error ? err.message : String(err);
69
+ throw new Error(`invalid ${rel}: ${reason}`, { cause: err });
70
+ }
71
+ }
72
+ const nextRaw = `${JSON.stringify(merge(current), null, 2)}\n`;
73
+ const changed = raw !== nextRaw;
74
+ if (changed && !opts.dryRun) {
75
+ await mkdir(dirname(path), { recursive: true });
76
+ await writeFile(path, nextRaw);
77
+ }
78
+ return { path: rel, changed };
79
+ }
80
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/agent-setup/files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The shape a harness writer fills in, and the one routine that drives it.
3
+ *
4
+ * Every harness needs the same two writes — merge its hook entries into its
5
+ * config file, then drop whichever scripts those entries invoke — and differs
6
+ * only in *where* each lands, *how* its config merges, and how much of the work
7
+ * its config can do without a script at all. Keeping that common body here is
8
+ * what makes a new harness a description rather than a copy: a `HarnessSpec`
9
+ * plus a merge function, and nothing else.
10
+ */
11
+ import { type HarnessName } from '../gate.js';
12
+ import { type PackageManager } from '../pm/index.js';
13
+ import type { HookFile } from './files.js';
14
+ /**
15
+ * Every hook `agent-setup` can write; `--hook <a,b>` selects a subset. It lives
16
+ * here rather than in `./hook.ts` so the per-harness writers can name it without
17
+ * importing the module that imports them.
18
+ */
19
+ export declare const HOOK_NAMES: readonly ["gate", "dirty", "protect"];
20
+ export type HookName = (typeof HOOK_NAMES)[number];
21
+ /**
22
+ * Ceiling for the gate run, in seconds, for any harness whose config carries
23
+ * one. Generous on purpose: it exists to break a hang, not to police a slow
24
+ * repo, and checkride's own per-check timeouts fire long before it does.
25
+ *
26
+ * Both harnesses default lower than a real pipeline takes — Cursor's default is
27
+ * short, and Claude Code's is 600s — and both read a timed-out stop hook as a
28
+ * *broken* one, which under Claude Code means the turn simply ends. A gate that
29
+ * quietly stops gating on the slowest repos is the vacuous green the contract
30
+ * exists to prevent, so neither default is left in place.
31
+ */
32
+ export declare const GATE_TIMEOUT_SECONDS = 900;
33
+ /**
34
+ * The spinner text a harness shows while the gate runs.
35
+ *
36
+ * Without it the only feedback during a full pipeline run is the harness's
37
+ * default spinner, which is indistinguishable from a hung model — the user is
38
+ * left guessing whether anything is happening at all. Naming the command that
39
+ * is actually running answers that in the one place they are already looking.
40
+ */
41
+ export declare function gateStatusMessage(pm: PackageManager): string;
42
+ /**
43
+ * Where one harness keeps its hooks, and how to merge into its config.
44
+ *
45
+ * `T` is that harness's config shape, so each writer keeps its own types all
46
+ * the way through — `apply` is never handed a config it does not understand.
47
+ */
48
+ export type HarnessSpec<T> = {
49
+ name: HarnessName;
50
+ /** The harness's hook config, relative to the repo root. */
51
+ configFile: string;
52
+ /**
53
+ * Where each hook's script lives, relative to the repo root — for the hooks
54
+ * that still need one.
55
+ *
56
+ * A hook is absent here when the harness can express it as *configuration*
57
+ * instead: Claude Code's `protect` is a `permissions.deny` rule and its
58
+ * `dirty` is an inline one-liner, so neither writes a file. Generated code in
59
+ * a consumer's repo is a cost — a file to review, to keep in sync, and to
60
+ * wonder about — and it should be paid only where config cannot do the job.
61
+ */
62
+ scripts: Partial<Record<HookName, string>>;
63
+ /**
64
+ * Scripts an earlier checkride wrote for this harness that this one does not.
65
+ * Deleted whenever their hook is written or removed, so a repo upgrading into
66
+ * the config-only form does not keep a dead file it will never run again.
67
+ */
68
+ retired?: Partial<Record<HookName, string>>;
69
+ /**
70
+ * Merge the named hooks into a parsed config. Must be idempotent. `pm` is the
71
+ * repo's package manager, which a harness that renders the gate's command in
72
+ * its own UI (Claude Code's `statusMessage`) needs in the config, not just in
73
+ * the script.
74
+ */
75
+ apply: (config: T | undefined, names: readonly string[], pm: PackageManager) => T;
76
+ /** Strip the named hooks from a parsed config. Must be idempotent. */
77
+ remove: (config: T | undefined, names: readonly string[]) => T;
78
+ };
79
+ export type WriteOptions = {
80
+ pm?: PackageManager;
81
+ dryRun?: boolean;
82
+ hooks: readonly string[];
83
+ /**
84
+ * Hooks to tear out: their config entries are stripped and their scripts
85
+ * deleted. Removal wins over `hooks`, so a name in both is removed — the
86
+ * caller-facing flags (`--hook`/`--remove-hook`) reject that combination
87
+ * before it gets here, and this is the safe reading if one ever slips through.
88
+ */
89
+ remove?: readonly string[];
90
+ };
91
+ /**
92
+ * Write or refresh `spec`'s hooks in `cwd` and tear out the ones marked for
93
+ * removal: the config entries first, then only the scripts the selected hooks
94
+ * actually invoke. Every write is idempotent, and `dryRun` computes the result
95
+ * without touching the disk.
96
+ *
97
+ * A run that only removes never *creates* the harness's config file — a repo
98
+ * that was never wired for this harness has nothing to un-wire.
99
+ */
100
+ export declare function writeHarnessHooks<T extends object>(cwd: string, spec: HarnessSpec<T>, opts: WriteOptions): Promise<HookFile[]>;
101
+ //# sourceMappingURL=harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/agent-setup/harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAwB,KAAK,cAAc,EAAa,MAAM,gBAAgB,CAAC;AACtF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,uCAAwC,CAAC;AAChE,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C;;;;;OAKG;IACH,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,cAAc,KAAK,CAAC,CAAC;IAClF,sEAAsE;IACtE,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,CAAC,CAAC;CAChE,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EACtD,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EACpB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAyDrB"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The shape a harness writer fills in, and the one routine that drives it.
3
+ *
4
+ * Every harness needs the same two writes — merge its hook entries into its
5
+ * config file, then drop whichever scripts those entries invoke — and differs
6
+ * only in *where* each lands, *how* its config merges, and how much of the work
7
+ * its config can do without a script at all. Keeping that common body here is
8
+ * what makes a new harness a description rather than a copy: a `HarnessSpec`
9
+ * plus a merge function, and nothing else.
10
+ */
11
+ import { gatePreflight } from '../gate.js';
12
+ import { detectPackageManager, runScript } from '../pm/index.js';
13
+ import { dropFile, putFile, putJson } from './files.js';
14
+ import { dirtyScript, gateScript, protectScript } from './scripts.js';
15
+ /**
16
+ * Every hook `agent-setup` can write; `--hook <a,b>` selects a subset. It lives
17
+ * here rather than in `./hook.ts` so the per-harness writers can name it without
18
+ * importing the module that imports them.
19
+ */
20
+ export const HOOK_NAMES = ['gate', 'dirty', 'protect'];
21
+ /**
22
+ * Ceiling for the gate run, in seconds, for any harness whose config carries
23
+ * one. Generous on purpose: it exists to break a hang, not to police a slow
24
+ * repo, and checkride's own per-check timeouts fire long before it does.
25
+ *
26
+ * Both harnesses default lower than a real pipeline takes — Cursor's default is
27
+ * short, and Claude Code's is 600s — and both read a timed-out stop hook as a
28
+ * *broken* one, which under Claude Code means the turn simply ends. A gate that
29
+ * quietly stops gating on the slowest repos is the vacuous green the contract
30
+ * exists to prevent, so neither default is left in place.
31
+ */
32
+ export const GATE_TIMEOUT_SECONDS = 900;
33
+ /**
34
+ * The spinner text a harness shows while the gate runs.
35
+ *
36
+ * Without it the only feedback during a full pipeline run is the harness's
37
+ * default spinner, which is indistinguishable from a hung model — the user is
38
+ * left guessing whether anything is happening at all. Naming the command that
39
+ * is actually running answers that in the one place they are already looking.
40
+ */
41
+ export function gateStatusMessage(pm) {
42
+ return `checkride gate — running \`${runScript(pm, 'check')}\``;
43
+ }
44
+ /**
45
+ * Write or refresh `spec`'s hooks in `cwd` and tear out the ones marked for
46
+ * removal: the config entries first, then only the scripts the selected hooks
47
+ * actually invoke. Every write is idempotent, and `dryRun` computes the result
48
+ * without touching the disk.
49
+ *
50
+ * A run that only removes never *creates* the harness's config file — a repo
51
+ * that was never wired for this harness has nothing to un-wire.
52
+ */
53
+ export async function writeHarnessHooks(cwd, spec, opts) {
54
+ const pm = opts.pm ?? detectPackageManager({ cwd });
55
+ const dryRun = opts.dryRun ?? false;
56
+ const gone = opts.remove ?? [];
57
+ const names = opts.hooks.filter((name) => !gone.includes(name));
58
+ // Read on every write, which is what makes the preflight the one seam that
59
+ // survives a refresh: `hooks add` rewrites the script and re-points the
60
+ // harness config, and re-derives this from the repo's own config each time.
61
+ const preflight = gatePreflight(cwd);
62
+ const bodies = {
63
+ // The gate guards on the edit marker only when the hook that sets it is
64
+ // written too; alone, the guard would disarm the gate entirely. Removing
65
+ // `dirty` therefore rewrites a surviving gate unguarded, which is why the
66
+ // config merge and the script bodies both read the post-removal selection.
67
+ gate: () => gateScript(pm, {
68
+ harness: spec.name,
69
+ dirtyGuard: names.includes('dirty'),
70
+ ...(preflight === null ? {} : { preflight }),
71
+ }),
72
+ dirty: dirtyScript,
73
+ protect: protectScript,
74
+ };
75
+ const config = await putJson(cwd, spec.configFile, (c) => spec.apply(spec.remove(c, gone), names, pm), { dryRun, skipIfMissing: names.length === 0 });
76
+ // A hook this harness expresses as config has no script to write, and may have
77
+ // a retired one to clear away. `touched` is both directions at once, because a
78
+ // dead file is worth deleting whether the hook is arriving or leaving.
79
+ const touched = HOOK_NAMES.filter((name) => names.includes(name) || gone.includes(name));
80
+ const [written, removed, swept] = await Promise.all([
81
+ Promise.all(HOOK_NAMES.filter((name) => names.includes(name) && spec.scripts[name] !== undefined).map((name) => putFile(cwd, spec.scripts[name] ?? '', bodies[name](), { dryRun, executable: true }))),
82
+ Promise.all(HOOK_NAMES.filter((name) => gone.includes(name) && spec.scripts[name] !== undefined).map((name) => dropFile(cwd, spec.scripts[name] ?? '', { dryRun }))),
83
+ Promise.all(touched
84
+ .map((name) => spec.retired?.[name])
85
+ .filter((path) => path !== undefined)
86
+ .map((path) => dropFile(cwd, path, { dryRun }))),
87
+ ]);
88
+ // Only report a retired script that was actually there. Every repo set up
89
+ // after these became configuration has none, and listing files that never
90
+ // existed as "absent" would make the common run read like a cleanup.
91
+ return [config, ...written, ...removed, ...swept.filter((f) => f.changed)];
92
+ }
93
+ //# sourceMappingURL=harness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/agent-setup/harness.ts"],"names":[],"mappings":"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"}