@lannguyensi/harness 0.63.0 → 1.0.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 (702) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +46 -42
  3. package/dist/cli/add/index.js +17 -21
  4. package/dist/cli/add/index.js.map +1 -1
  5. package/dist/cli/adopt/derive.d.ts +2 -2
  6. package/dist/cli/adopt/derive.js +2 -19
  7. package/dist/cli/adopt/derive.js.map +1 -1
  8. package/dist/cli/adopt/index.d.ts +1 -1
  9. package/dist/cli/adopt/index.js +1 -12
  10. package/dist/cli/adopt/index.js.map +1 -1
  11. package/dist/cli/apply/apply.d.ts +20 -35
  12. package/dist/cli/apply/apply.js +61 -136
  13. package/dist/cli/apply/apply.js.map +1 -1
  14. package/dist/cli/apply/generate-codex-config.js +10 -58
  15. package/dist/cli/apply/generate-codex-config.js.map +1 -1
  16. package/dist/cli/apply/generate-memory-index.js +1 -2
  17. package/dist/cli/apply/generate-memory-index.js.map +1 -1
  18. package/dist/cli/apply/generate-opencode-config.d.ts +2 -10
  19. package/dist/cli/apply/generate-opencode-config.js +74 -113
  20. package/dist/cli/apply/generate-opencode-config.js.map +1 -1
  21. package/dist/cli/apply/generate-settings.d.ts +11 -64
  22. package/dist/cli/apply/generate-settings.js +27 -132
  23. package/dist/cli/apply/generate-settings.js.map +1 -1
  24. package/dist/cli/apply/index.d.ts +1 -1
  25. package/dist/cli/apply/index.js +1 -1
  26. package/dist/cli/apply/index.js.map +1 -1
  27. package/dist/cli/apply/install-codex-config.d.ts +153 -1
  28. package/dist/cli/apply/install-codex-config.js +445 -70
  29. package/dist/cli/apply/install-codex-config.js.map +1 -1
  30. package/dist/cli/apply/next-steps.d.ts +7 -0
  31. package/dist/cli/apply/next-steps.js +13 -1
  32. package/dist/cli/apply/next-steps.js.map +1 -1
  33. package/dist/cli/bounded-stdin.d.ts +0 -23
  34. package/dist/cli/bounded-stdin.js +2 -49
  35. package/dist/cli/bounded-stdin.js.map +1 -1
  36. package/dist/cli/describe.d.ts +1 -1
  37. package/dist/cli/describe.js +0 -3
  38. package/dist/cli/describe.js.map +1 -1
  39. package/dist/cli/diff/engine.d.ts +0 -11
  40. package/dist/cli/diff/engine.js +1 -17
  41. package/dist/cli/diff/engine.js.map +1 -1
  42. package/dist/cli/diff/index.js +14 -25
  43. package/dist/cli/diff/index.js.map +1 -1
  44. package/dist/cli/doctor/claude-mcp.d.ts +4 -19
  45. package/dist/cli/doctor/claude-mcp.js +11 -22
  46. package/dist/cli/doctor/claude-mcp.js.map +1 -1
  47. package/dist/cli/doctor/codex.d.ts +12 -14
  48. package/dist/cli/doctor/codex.js +31 -144
  49. package/dist/cli/doctor/codex.js.map +1 -1
  50. package/dist/cli/doctor/format.js +23 -338
  51. package/dist/cli/doctor/format.js.map +1 -1
  52. package/dist/cli/doctor/hook-liveness.d.ts +56 -0
  53. package/dist/cli/doctor/hook-liveness.js +455 -0
  54. package/dist/cli/doctor/hook-liveness.js.map +1 -0
  55. package/dist/cli/doctor/index.d.ts +13 -87
  56. package/dist/cli/doctor/index.js +57 -705
  57. package/dist/cli/doctor/index.js.map +1 -1
  58. package/dist/cli/doctor/opencode.d.ts +2 -1
  59. package/dist/cli/doctor/opencode.js.map +1 -1
  60. package/dist/cli/doctor/types.d.ts +21 -378
  61. package/dist/cli/doctor/types.js.map +1 -1
  62. package/dist/cli/drain-stdio.d.ts +31 -0
  63. package/dist/cli/drain-stdio.js +76 -0
  64. package/dist/cli/drain-stdio.js.map +1 -0
  65. package/dist/cli/export.d.ts +3 -1
  66. package/dist/cli/export.js +7 -16
  67. package/dist/cli/export.js.map +1 -1
  68. package/dist/cli/gate/codex-disable.d.ts +29 -0
  69. package/dist/cli/gate/codex-disable.js +195 -0
  70. package/dist/cli/gate/codex-disable.js.map +1 -0
  71. package/dist/cli/gate/codex-enable.d.ts +41 -0
  72. package/dist/cli/gate/codex-enable.js +127 -0
  73. package/dist/cli/gate/codex-enable.js.map +1 -0
  74. package/dist/cli/gate/codex-snapshot.d.ts +208 -0
  75. package/dist/cli/gate/codex-snapshot.js +402 -0
  76. package/dist/cli/gate/codex-snapshot.js.map +1 -0
  77. package/dist/cli/gate/disable.d.ts +9 -0
  78. package/dist/cli/gate/disable.js +263 -28
  79. package/dist/cli/gate/disable.js.map +1 -1
  80. package/dist/cli/gate/enable.d.ts +26 -6
  81. package/dist/cli/gate/enable.js +152 -80
  82. package/dist/cli/gate/enable.js.map +1 -1
  83. package/dist/cli/gate/link-chain.d.ts +53 -0
  84. package/dist/cli/gate/link-chain.js +122 -0
  85. package/dist/cli/gate/link-chain.js.map +1 -0
  86. package/dist/cli/gate/snapshot.d.ts +289 -12
  87. package/dist/cli/gate/snapshot.js +481 -22
  88. package/dist/cli/gate/snapshot.js.map +1 -1
  89. package/dist/cli/index.d.ts +0 -9
  90. package/dist/cli/index.js +15 -2607
  91. package/dist/cli/index.js.map +1 -1
  92. package/dist/cli/init/composer.d.ts +3 -6
  93. package/dist/cli/init/composer.js +6 -370
  94. package/dist/cli/init/composer.js.map +1 -1
  95. package/dist/cli/init/dependencies.d.ts +1 -2
  96. package/dist/cli/init/dependencies.js +4 -80
  97. package/dist/cli/init/dependencies.js.map +1 -1
  98. package/dist/cli/init/detect.js +5 -4
  99. package/dist/cli/init/detect.js.map +1 -1
  100. package/dist/cli/init/interactive.d.ts +1 -23
  101. package/dist/cli/init/interactive.js +33 -247
  102. package/dist/cli/init/interactive.js.map +1 -1
  103. package/dist/cli/init/profiles.d.ts +2 -2
  104. package/dist/cli/init/profiles.js +51 -243
  105. package/dist/cli/init/profiles.js.map +1 -1
  106. package/dist/cli/init/templates.d.ts +2 -37
  107. package/dist/cli/init/templates.js +25 -1237
  108. package/dist/cli/init/templates.js.map +1 -1
  109. package/dist/cli/list.d.ts +1 -1
  110. package/dist/cli/list.js +1 -38
  111. package/dist/cli/list.js.map +1 -1
  112. package/dist/cli/loader.d.ts +15 -1
  113. package/dist/cli/loader.js +56 -39
  114. package/dist/cli/loader.js.map +1 -1
  115. package/dist/cli/main.js +7 -2
  116. package/dist/cli/main.js.map +1 -1
  117. package/dist/cli/migrate-home/index.js +2 -2
  118. package/dist/cli/migrate-home/index.js.map +1 -1
  119. package/dist/cli/pack/add.js +1 -1
  120. package/dist/cli/pack/add.js.map +1 -1
  121. package/dist/cli/pack/hook-bootstrap.d.ts +5 -164
  122. package/dist/cli/pack/hook-bootstrap.js +15 -291
  123. package/dist/cli/pack/hook-bootstrap.js.map +1 -1
  124. package/dist/cli/pack/hook-branch-protection.d.ts +28 -21
  125. package/dist/cli/pack/hook-branch-protection.js +365 -340
  126. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  127. package/dist/cli/pack/index.d.ts +0 -1
  128. package/dist/cli/pack/index.js +0 -1
  129. package/dist/cli/pack/index.js.map +1 -1
  130. package/dist/cli/pack/list.js +1 -6
  131. package/dist/cli/pack/list.js.map +1 -1
  132. package/dist/cli/pack/mutate.d.ts +7 -9
  133. package/dist/cli/pack/mutate.js +6 -9
  134. package/dist/cli/pack/mutate.js.map +1 -1
  135. package/dist/cli/pack/reseed.d.ts +2 -2
  136. package/dist/cli/pack/reseed.js +16 -22
  137. package/dist/cli/pack/reseed.js.map +1 -1
  138. package/dist/cli/register-gate-group.d.ts +5 -0
  139. package/dist/cli/register-gate-group.js +348 -0
  140. package/dist/cli/register-gate-group.js.map +1 -0
  141. package/dist/cli/register-inspect-group.d.ts +6 -0
  142. package/dist/cli/register-inspect-group.js +189 -0
  143. package/dist/cli/register-inspect-group.js.map +1 -0
  144. package/dist/cli/register-operator-lifecycle.d.ts +5 -0
  145. package/dist/cli/register-operator-lifecycle.js +20 -0
  146. package/dist/cli/register-operator-lifecycle.js.map +1 -0
  147. package/dist/cli/register-pack-group.d.ts +5 -0
  148. package/dist/cli/register-pack-group.js +134 -0
  149. package/dist/cli/register-pack-group.js.map +1 -0
  150. package/dist/cli/register-setup-group.d.ts +5 -0
  151. package/dist/cli/register-setup-group.js +495 -0
  152. package/dist/cli/register-setup-group.js.map +1 -0
  153. package/dist/cli/remove/index.d.ts +0 -14
  154. package/dist/cli/remove/index.js +2 -115
  155. package/dist/cli/remove/index.js.map +1 -1
  156. package/dist/cli/remove/mutate.d.ts +0 -2
  157. package/dist/cli/remove/mutate.js +1 -19
  158. package/dist/cli/remove/mutate.js.map +1 -1
  159. package/dist/cli/remove/register.d.ts +5 -0
  160. package/dist/cli/remove/register.js +25 -0
  161. package/dist/cli/remove/register.js.map +1 -0
  162. package/dist/cli/shell-quote.d.ts +7 -0
  163. package/dist/cli/shell-quote.js +17 -0
  164. package/dist/cli/shell-quote.js.map +1 -0
  165. package/dist/cli/uninstall/index.d.ts +4 -2
  166. package/dist/cli/uninstall/index.js +15 -7
  167. package/dist/cli/uninstall/index.js.map +1 -1
  168. package/dist/cli/uninstall/register.d.ts +5 -0
  169. package/dist/cli/uninstall/register.js +171 -0
  170. package/dist/cli/uninstall/register.js.map +1 -0
  171. package/dist/cli/uninstall/snapshot.d.ts +15 -0
  172. package/dist/cli/uninstall/snapshot.js +38 -0
  173. package/dist/cli/uninstall/snapshot.js.map +1 -1
  174. package/dist/cli/validate/checks.d.ts +0 -77
  175. package/dist/cli/validate/checks.js +3 -1049
  176. package/dist/cli/validate/checks.js.map +1 -1
  177. package/dist/cli/validate/index.js +19 -15
  178. package/dist/cli/validate/index.js.map +1 -1
  179. package/dist/index.d.ts +0 -3
  180. package/dist/index.js +0 -3
  181. package/dist/index.js.map +1 -1
  182. package/dist/io/claude-mcp.d.ts +1 -1
  183. package/dist/io/claude-mcp.js +27 -14
  184. package/dist/io/claude-mcp.js.map +1 -1
  185. package/dist/io/harness-lock.js +2 -2
  186. package/dist/io/harness-lock.js.map +1 -1
  187. package/dist/io/lock.d.ts +0 -26
  188. package/dist/io/lock.js +10 -27
  189. package/dist/io/lock.js.map +1 -1
  190. package/dist/io/merge-settings.js +6 -4
  191. package/dist/io/merge-settings.js.map +1 -1
  192. package/dist/io/project-name.d.ts +22 -39
  193. package/dist/io/project-name.js +25 -42
  194. package/dist/io/project-name.js.map +1 -1
  195. package/dist/io/read-regular-file.d.ts +103 -40
  196. package/dist/io/read-regular-file.js +196 -35
  197. package/dist/io/read-regular-file.js.map +1 -1
  198. package/dist/io/restart-hints.js +3 -6
  199. package/dist/io/restart-hints.js.map +1 -1
  200. package/dist/io/version-compare.d.ts +1 -3
  201. package/dist/io/version-compare.js +1 -3
  202. package/dist/io/version-compare.js.map +1 -1
  203. package/dist/io/write-regular-file.d.ts +30 -0
  204. package/dist/io/write-regular-file.js +85 -0
  205. package/dist/io/write-regular-file.js.map +1 -0
  206. package/dist/overrides/index.d.ts +1 -0
  207. package/dist/overrides/index.js +1 -0
  208. package/dist/overrides/index.js.map +1 -1
  209. package/dist/overrides/merge-layers.d.ts +43 -0
  210. package/dist/overrides/merge-layers.js +76 -0
  211. package/dist/overrides/merge-layers.js.map +1 -0
  212. package/dist/overrides/merge.d.ts +16 -0
  213. package/dist/overrides/merge.js +21 -8
  214. package/dist/overrides/merge.js.map +1 -1
  215. package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -69
  216. package/dist/policy-packs/builtin/branch-protection-runtime.js +12 -84
  217. package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
  218. package/dist/policy-packs/builtin/branch-protection.d.ts +7 -30
  219. package/dist/policy-packs/builtin/branch-protection.js +56 -89
  220. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  221. package/dist/policy-packs/config-check.d.ts +7 -5
  222. package/dist/policy-packs/config-check.js +2 -2
  223. package/dist/policy-packs/config-check.js.map +1 -1
  224. package/dist/policy-packs/expand.d.ts +1 -3
  225. package/dist/policy-packs/expand.js +5 -42
  226. package/dist/policy-packs/expand.js.map +1 -1
  227. package/dist/policy-packs/index.d.ts +3 -3
  228. package/dist/policy-packs/index.js +2 -2
  229. package/dist/policy-packs/index.js.map +1 -1
  230. package/dist/policy-packs/registry.d.ts +9 -38
  231. package/dist/policy-packs/registry.js +5 -65
  232. package/dist/policy-packs/registry.js.map +1 -1
  233. package/dist/policy-packs/runtime.d.ts +0 -4
  234. package/dist/policy-packs/runtime.js +9 -22
  235. package/dist/policy-packs/runtime.js.map +1 -1
  236. package/dist/policy-packs/types.d.ts +0 -19
  237. package/dist/policy-packs/ux-compare.d.ts +1 -11
  238. package/dist/policy-packs/ux-compare.js +6 -42
  239. package/dist/policy-packs/ux-compare.js.map +1 -1
  240. package/dist/policy-packs/ux-drift-check.d.ts +5 -5
  241. package/dist/policy-packs/ux-drift-check.js +17 -22
  242. package/dist/policy-packs/ux-drift-check.js.map +1 -1
  243. package/dist/policy-packs/version-check.d.ts +5 -26
  244. package/dist/policy-packs/version-check.js +19 -74
  245. package/dist/policy-packs/version-check.js.map +1 -1
  246. package/dist/probes/memory.d.ts +1 -1
  247. package/dist/probes/memory.js +1 -1
  248. package/dist/probes/memory.js.map +1 -1
  249. package/dist/runtime/agent-facing.d.ts +9 -0
  250. package/dist/runtime/agent-facing.js +28 -14
  251. package/dist/runtime/agent-facing.js.map +1 -1
  252. package/dist/runtime/bounded-text.d.ts +15 -0
  253. package/dist/runtime/bounded-text.js +27 -0
  254. package/dist/runtime/bounded-text.js.map +1 -0
  255. package/dist/runtime/git-branch.d.ts +125 -0
  256. package/dist/runtime/git-branch.js +381 -0
  257. package/dist/runtime/git-branch.js.map +1 -0
  258. package/dist/runtime/hermetic-spawn-guard.d.ts +2 -2
  259. package/dist/runtime/hermetic-spawn-guard.js +24 -34
  260. package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
  261. package/dist/runtime/home-dir.js +3 -2
  262. package/dist/runtime/home-dir.js.map +1 -1
  263. package/dist/runtime/tool-name-aliases.d.ts +0 -6
  264. package/dist/runtime/tool-name-aliases.js +5 -18
  265. package/dist/runtime/tool-name-aliases.js.map +1 -1
  266. package/dist/schema/grounding.d.ts +7 -54
  267. package/dist/schema/grounding.js +10 -12
  268. package/dist/schema/grounding.js.map +1 -1
  269. package/dist/schema/hooks.d.ts +39 -136
  270. package/dist/schema/hooks.js +3 -6
  271. package/dist/schema/hooks.js.map +1 -1
  272. package/dist/schema/index.d.ts +114 -3828
  273. package/dist/schema/index.js +41 -80
  274. package/dist/schema/index.js.map +1 -1
  275. package/dist/schema/memory.d.ts +54 -131
  276. package/dist/schema/memory.js +2 -2
  277. package/dist/schema/memory.js.map +1 -1
  278. package/dist/schema/policy-packs.d.ts +3 -45
  279. package/dist/schema/policy-packs.js +7 -13
  280. package/dist/schema/policy-packs.js.map +1 -1
  281. package/dist/schema/policy-ux.d.ts +7 -0
  282. package/dist/schema/policy-ux.js +18 -0
  283. package/dist/schema/policy-ux.js.map +1 -0
  284. package/dist/schema/removed-keys.d.ts +185 -0
  285. package/dist/schema/removed-keys.js +531 -0
  286. package/dist/schema/removed-keys.js.map +1 -0
  287. package/dist/schema/tools.d.ts +30 -268
  288. package/dist/schema/tools.js +4 -4
  289. package/dist/schema/tools.js.map +1 -1
  290. package/package.json +5 -16
  291. package/dist/cli/approve/branch-protection.d.ts +0 -69
  292. package/dist/cli/approve/branch-protection.js +0 -144
  293. package/dist/cli/approve/branch-protection.js.map +0 -1
  294. package/dist/cli/approve/risk.d.ts +0 -112
  295. package/dist/cli/approve/risk.js +0 -197
  296. package/dist/cli/approve/risk.js.map +0 -1
  297. package/dist/cli/approve/stdin-report.d.ts +0 -57
  298. package/dist/cli/approve/stdin-report.js +0 -145
  299. package/dist/cli/approve/stdin-report.js.map +0 -1
  300. package/dist/cli/approve/understanding.d.ts +0 -410
  301. package/dist/cli/approve/understanding.js +0 -996
  302. package/dist/cli/approve/understanding.js.map +0 -1
  303. package/dist/cli/audit.d.ts +0 -80
  304. package/dist/cli/audit.js +0 -252
  305. package/dist/cli/audit.js.map +0 -1
  306. package/dist/cli/delegate/index.d.ts +0 -76
  307. package/dist/cli/delegate/index.js +0 -443
  308. package/dist/cli/delegate/index.js.map +0 -1
  309. package/dist/cli/doctor/auto-approve-mode.d.ts +0 -37
  310. package/dist/cli/doctor/auto-approve-mode.js +0 -80
  311. package/dist/cli/doctor/auto-approve-mode.js.map +0 -1
  312. package/dist/cli/doctor/bypass-without-auto-approve.d.ts +0 -38
  313. package/dist/cli/doctor/bypass-without-auto-approve.js +0 -95
  314. package/dist/cli/doctor/bypass-without-auto-approve.js.map +0 -1
  315. package/dist/cli/doctor/codex-config-drift.d.ts +0 -28
  316. package/dist/cli/doctor/codex-config-drift.js +0 -240
  317. package/dist/cli/doctor/codex-config-drift.js.map +0 -1
  318. package/dist/cli/doctor/rogue-ledger.d.ts +0 -53
  319. package/dist/cli/doctor/rogue-ledger.js +0 -153
  320. package/dist/cli/doctor/rogue-ledger.js.map +0 -1
  321. package/dist/cli/doctor/session-start-preflight-setup-version.d.ts +0 -83
  322. package/dist/cli/doctor/session-start-preflight-setup-version.js +0 -114
  323. package/dist/cli/doctor/session-start-preflight-setup-version.js.map +0 -1
  324. package/dist/cli/doctor/settings-drift.d.ts +0 -25
  325. package/dist/cli/doctor/settings-drift.js +0 -213
  326. package/dist/cli/doctor/settings-drift.js.map +0 -1
  327. package/dist/cli/doctor/toolchain-parity.d.ts +0 -63
  328. package/dist/cli/doctor/toolchain-parity.js +0 -154
  329. package/dist/cli/doctor/toolchain-parity.js.map +0 -1
  330. package/dist/cli/doctor/ug-auto-approvals.d.ts +0 -40
  331. package/dist/cli/doctor/ug-auto-approvals.js +0 -91
  332. package/dist/cli/doctor/ug-auto-approvals.js.map +0 -1
  333. package/dist/cli/doctor/ug-delegations.d.ts +0 -26
  334. package/dist/cli/doctor/ug-delegations.js +0 -121
  335. package/dist/cli/doctor/ug-delegations.js.map +0 -1
  336. package/dist/cli/doctor/ug-inflight.d.ts +0 -35
  337. package/dist/cli/doctor/ug-inflight.js +0 -57
  338. package/dist/cli/doctor/ug-inflight.js.map +0 -1
  339. package/dist/cli/doctor/understanding-mode-env.d.ts +0 -51
  340. package/dist/cli/doctor/understanding-mode-env.js +0 -98
  341. package/dist/cli/doctor/understanding-mode-env.js.map +0 -1
  342. package/dist/cli/dry-run.d.ts +0 -46
  343. package/dist/cli/dry-run.js +0 -250
  344. package/dist/cli/dry-run.js.map +0 -1
  345. package/dist/cli/event-input.d.ts +0 -28
  346. package/dist/cli/event-input.js +0 -73
  347. package/dist/cli/event-input.js.map +0 -1
  348. package/dist/cli/explain-action.d.ts +0 -20
  349. package/dist/cli/explain-action.js +0 -27
  350. package/dist/cli/explain-action.js.map +0 -1
  351. package/dist/cli/explain-policy.d.ts +0 -135
  352. package/dist/cli/explain-policy.js +0 -266
  353. package/dist/cli/explain-policy.js.map +0 -1
  354. package/dist/cli/explain.d.ts +0 -31
  355. package/dist/cli/explain.js +0 -159
  356. package/dist/cli/explain.js.map +0 -1
  357. package/dist/cli/gc/index.d.ts +0 -63
  358. package/dist/cli/gc/index.js +0 -566
  359. package/dist/cli/gc/index.js.map +0 -1
  360. package/dist/cli/pack/approve-escape.d.ts +0 -19
  361. package/dist/cli/pack/approve-escape.js +0 -190
  362. package/dist/cli/pack/approve-escape.js.map +0 -1
  363. package/dist/cli/pack/auto-approve-path.d.ts +0 -146
  364. package/dist/cli/pack/auto-approve-path.js +0 -462
  365. package/dist/cli/pack/auto-approve-path.js.map +0 -1
  366. package/dist/cli/pack/hook-codex-post-tool-use.d.ts +0 -25
  367. package/dist/cli/pack/hook-codex-post-tool-use.js +0 -160
  368. package/dist/cli/pack/hook-codex-post-tool-use.js.map +0 -1
  369. package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +0 -48
  370. package/dist/cli/pack/hook-codex-pre-tool-use.js +0 -474
  371. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +0 -1
  372. package/dist/cli/pack/hook-codex-stop.d.ts +0 -35
  373. package/dist/cli/pack/hook-codex-stop.js +0 -329
  374. package/dist/cli/pack/hook-codex-stop.js.map +0 -1
  375. package/dist/cli/pack/hook-codex-user-prompt-submit.d.ts +0 -24
  376. package/dist/cli/pack/hook-codex-user-prompt-submit.js +0 -93
  377. package/dist/cli/pack/hook-codex-user-prompt-submit.js.map +0 -1
  378. package/dist/cli/pack/hook-post-merge-gate-record.d.ts +0 -37
  379. package/dist/cli/pack/hook-post-merge-gate-record.js +0 -190
  380. package/dist/cli/pack/hook-post-merge-gate-record.js.map +0 -1
  381. package/dist/cli/pack/hook-post-merge-gate.d.ts +0 -34
  382. package/dist/cli/pack/hook-post-merge-gate.js +0 -277
  383. package/dist/cli/pack/hook-post-merge-gate.js.map +0 -1
  384. package/dist/cli/pack/hook-post-tool-use.d.ts +0 -45
  385. package/dist/cli/pack/hook-post-tool-use.js +0 -137
  386. package/dist/cli/pack/hook-post-tool-use.js.map +0 -1
  387. package/dist/cli/pack/hook-pre-tool-use.d.ts +0 -92
  388. package/dist/cli/pack/hook-pre-tool-use.js +0 -1328
  389. package/dist/cli/pack/hook-pre-tool-use.js.map +0 -1
  390. package/dist/cli/pack/hook-runtime-reality.d.ts +0 -63
  391. package/dist/cli/pack/hook-runtime-reality.js +0 -210
  392. package/dist/cli/pack/hook-runtime-reality.js.map +0 -1
  393. package/dist/cli/pack/hook-solution-acceptance-writeguard.d.ts +0 -32
  394. package/dist/cli/pack/hook-solution-acceptance-writeguard.js +0 -337
  395. package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +0 -1
  396. package/dist/cli/pack/hook-solution-acceptance.d.ts +0 -157
  397. package/dist/cli/pack/hook-solution-acceptance.js +0 -530
  398. package/dist/cli/pack/hook-solution-acceptance.js.map +0 -1
  399. package/dist/cli/pack/hook-stay-in-scope.d.ts +0 -37
  400. package/dist/cli/pack/hook-stay-in-scope.js +0 -128
  401. package/dist/cli/pack/hook-stay-in-scope.js.map +0 -1
  402. package/dist/cli/pack/hook-subagent-start.d.ts +0 -23
  403. package/dist/cli/pack/hook-subagent-start.js +0 -88
  404. package/dist/cli/pack/hook-subagent-start.js.map +0 -1
  405. package/dist/cli/pack/hook-subagent-stop.d.ts +0 -19
  406. package/dist/cli/pack/hook-subagent-stop.js +0 -41
  407. package/dist/cli/pack/hook-subagent-stop.js.map +0 -1
  408. package/dist/cli/pack/hook-track-active-claim.d.ts +0 -27
  409. package/dist/cli/pack/hook-track-active-claim.js +0 -275
  410. package/dist/cli/pack/hook-track-active-claim.js.map +0 -1
  411. package/dist/cli/pack/transcript-report-scan.d.ts +0 -71
  412. package/dist/cli/pack/transcript-report-scan.js +0 -359
  413. package/dist/cli/pack/transcript-report-scan.js.map +0 -1
  414. package/dist/cli/pack/understanding-report-schema-hint.d.ts +0 -26
  415. package/dist/cli/pack/understanding-report-schema-hint.js +0 -120
  416. package/dist/cli/pack/understanding-report-schema-hint.js.map +0 -1
  417. package/dist/cli/pack/upgrade.d.ts +0 -32
  418. package/dist/cli/pack/upgrade.js +0 -273
  419. package/dist/cli/pack/upgrade.js.map +0 -1
  420. package/dist/cli/pause/index.d.ts +0 -113
  421. package/dist/cli/pause/index.js +0 -251
  422. package/dist/cli/pause/index.js.map +0 -1
  423. package/dist/cli/pause-check.d.ts +0 -22
  424. package/dist/cli/pause-check.js +0 -54
  425. package/dist/cli/pause-check.js.map +0 -1
  426. package/dist/cli/policy/intercept.d.ts +0 -233
  427. package/dist/cli/policy/intercept.js +0 -1020
  428. package/dist/cli/policy/intercept.js.map +0 -1
  429. package/dist/cli/policy/risk-envelope-enrichment.d.ts +0 -40
  430. package/dist/cli/policy/risk-envelope-enrichment.js +0 -108
  431. package/dist/cli/policy/risk-envelope-enrichment.js.map +0 -1
  432. package/dist/cli/record/index.d.ts +0 -81
  433. package/dist/cli/record/index.js +0 -348
  434. package/dist/cli/record/index.js.map +0 -1
  435. package/dist/cli/resolve-env.d.ts +0 -32
  436. package/dist/cli/resolve-env.js +0 -47
  437. package/dist/cli/resolve-env.js.map +0 -1
  438. package/dist/cli/session-export/index.d.ts +0 -46
  439. package/dist/cli/session-export/index.js +0 -169
  440. package/dist/cli/session-export/index.js.map +0 -1
  441. package/dist/cli/session-export/redact.d.ts +0 -22
  442. package/dist/cli/session-export/redact.js +0 -47
  443. package/dist/cli/session-export/redact.js.map +0 -1
  444. package/dist/cli/session-export/transcript.d.ts +0 -24
  445. package/dist/cli/session-export/transcript.js +0 -164
  446. package/dist/cli/session-export/transcript.js.map +0 -1
  447. package/dist/cli/session-start/branch-check.d.ts +0 -18
  448. package/dist/cli/session-start/branch-check.js +0 -140
  449. package/dist/cli/session-start/branch-check.js.map +0 -1
  450. package/dist/cli/session-start/index.d.ts +0 -121
  451. package/dist/cli/session-start/index.js +0 -591
  452. package/dist/cli/session-start/index.js.map +0 -1
  453. package/dist/cli/session-start/shared-options.d.ts +0 -103
  454. package/dist/cli/session-start/shared-options.js +0 -107
  455. package/dist/cli/session-start/shared-options.js.map +0 -1
  456. package/dist/cli/session-start/stale-base-check.d.ts +0 -46
  457. package/dist/cli/session-start/stale-base-check.js +0 -352
  458. package/dist/cli/session-start/stale-base-check.js.map +0 -1
  459. package/dist/cli/session-start/toolchain-parity.d.ts +0 -267
  460. package/dist/cli/session-start/toolchain-parity.js +0 -797
  461. package/dist/cli/session-start/toolchain-parity.js.map +0 -1
  462. package/dist/cli/smoke/assertions.d.ts +0 -46
  463. package/dist/cli/smoke/assertions.js +0 -170
  464. package/dist/cli/smoke/assertions.js.map +0 -1
  465. package/dist/cli/smoke/index.d.ts +0 -76
  466. package/dist/cli/smoke/index.js +0 -245
  467. package/dist/cli/smoke/index.js.map +0 -1
  468. package/dist/cli/smoke/runner.d.ts +0 -55
  469. package/dist/cli/smoke/runner.js +0 -134
  470. package/dist/cli/smoke/runner.js.map +0 -1
  471. package/dist/cli/smoke/stream-parser.d.ts +0 -65
  472. package/dist/cli/smoke/stream-parser.js +0 -115
  473. package/dist/cli/smoke/stream-parser.js.map +0 -1
  474. package/dist/cli/test-risk.d.ts +0 -26
  475. package/dist/cli/test-risk.js +0 -34
  476. package/dist/cli/test-risk.js.map +0 -1
  477. package/dist/io/display-path.d.ts +0 -11
  478. package/dist/io/display-path.js +0 -29
  479. package/dist/io/display-path.js.map +0 -1
  480. package/dist/io/duration.d.ts +0 -5
  481. package/dist/io/duration.js +0 -50
  482. package/dist/io/duration.js.map +0 -1
  483. package/dist/io/extract.d.ts +0 -79
  484. package/dist/io/extract.js +0 -226
  485. package/dist/io/extract.js.map +0 -1
  486. package/dist/io/ledger-record.d.ts +0 -83
  487. package/dist/io/ledger-record.js +0 -301
  488. package/dist/io/ledger-record.js.map +0 -1
  489. package/dist/io/read-json-dir-entries.d.ts +0 -36
  490. package/dist/io/read-json-dir-entries.js +0 -48
  491. package/dist/io/read-json-dir-entries.js.map +0 -1
  492. package/dist/io/safe-json-parse.d.ts +0 -5
  493. package/dist/io/safe-json-parse.js +0 -21
  494. package/dist/io/safe-json-parse.js.map +0 -1
  495. package/dist/policies/index.d.ts +0 -5
  496. package/dist/policies/index.js +0 -6
  497. package/dist/policies/index.js.map +0 -1
  498. package/dist/policies/ledger-client.d.ts +0 -113
  499. package/dist/policies/ledger-client.js +0 -500
  500. package/dist/policies/ledger-client.js.map +0 -1
  501. package/dist/policies/producers.d.ts +0 -12
  502. package/dist/policies/producers.js +0 -61
  503. package/dist/policies/producers.js.map +0 -1
  504. package/dist/policies/requires.d.ts +0 -75
  505. package/dist/policies/requires.js +0 -240
  506. package/dist/policies/requires.js.map +0 -1
  507. package/dist/policies/timestamp.d.ts +0 -14
  508. package/dist/policies/timestamp.js +0 -36
  509. package/dist/policies/timestamp.js.map +0 -1
  510. package/dist/policy-packs/builtin/permission-profiles.d.ts +0 -11
  511. package/dist/policy-packs/builtin/permission-profiles.js +0 -74
  512. package/dist/policy-packs/builtin/permission-profiles.js.map +0 -1
  513. package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +0 -286
  514. package/dist/policy-packs/builtin/post-merge-gate-runtime.js +0 -397
  515. package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +0 -1
  516. package/dist/policy-packs/builtin/post-merge-gate.d.ts +0 -56
  517. package/dist/policy-packs/builtin/post-merge-gate.js +0 -347
  518. package/dist/policy-packs/builtin/post-merge-gate.js.map +0 -1
  519. package/dist/policy-packs/builtin/solution-acceptance-reconnect.d.ts +0 -50
  520. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js +0 -151
  521. package/dist/policy-packs/builtin/solution-acceptance-reconnect.js.map +0 -1
  522. package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +0 -323
  523. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +0 -672
  524. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +0 -1
  525. package/dist/policy-packs/builtin/solution-acceptance.d.ts +0 -45
  526. package/dist/policy-packs/builtin/solution-acceptance.js +0 -226
  527. package/dist/policy-packs/builtin/solution-acceptance.js.map +0 -1
  528. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +0 -25
  529. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +0 -94
  530. package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +0 -1
  531. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.d.ts +0 -35
  532. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js +0 -86
  533. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve-default.js.map +0 -1
  534. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.d.ts +0 -210
  535. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js +0 -361
  536. package/dist/policy-packs/builtin/understanding-before-execution/auto-approve.js.map +0 -1
  537. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.d.ts +0 -304
  538. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js +0 -734
  539. package/dist/policy-packs/builtin/understanding-before-execution/delegation-markers.js.map +0 -1
  540. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.d.ts +0 -36
  541. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js +0 -73
  542. package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js.map +0 -1
  543. package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +0 -19
  544. package/dist/policy-packs/builtin/understanding-before-execution/index.js +0 -83
  545. package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +0 -1
  546. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.d.ts +0 -135
  547. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js +0 -618
  548. package/dist/policy-packs/builtin/understanding-before-execution/inflight-records.js.map +0 -1
  549. package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +0 -25
  550. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +0 -56
  551. package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +0 -1
  552. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +0 -20
  553. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +0 -93
  554. package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +0 -1
  555. package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +0 -126
  556. package/dist/policy-packs/builtin/understanding-before-execution/markers.js +0 -245
  557. package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +0 -1
  558. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.d.ts +0 -29
  559. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js +0 -82
  560. package/dist/policy-packs/builtin/understanding-before-execution/measured-permission-modes.js.map +0 -1
  561. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.d.ts +0 -71
  562. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js +0 -175
  563. package/dist/policy-packs/builtin/understanding-before-execution/permission-mode-observations.js.map +0 -1
  564. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +0 -620
  565. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +0 -1047
  566. package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +0 -1
  567. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +0 -128
  568. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +0 -213
  569. package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +0 -1
  570. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.d.ts +0 -129
  571. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js +0 -166
  572. package/dist/policy-packs/builtin/understanding-before-execution/stay-in-scope-config.js.map +0 -1
  573. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +0 -164
  574. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +0 -275
  575. package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +0 -1
  576. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +0 -1
  577. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +0 -10
  578. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +0 -1
  579. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +0 -526
  580. package/dist/policy-packs/builtin/understanding-before-execution.js +0 -1074
  581. package/dist/policy-packs/builtin/understanding-before-execution.js.map +0 -1
  582. package/dist/policy-packs/permission-translator.d.ts +0 -9
  583. package/dist/policy-packs/permission-translator.js +0 -76
  584. package/dist/policy-packs/permission-translator.js.map +0 -1
  585. package/dist/runtime/action-envelope.d.ts +0 -64
  586. package/dist/runtime/action-envelope.js +0 -46
  587. package/dist/runtime/action-envelope.js.map +0 -1
  588. package/dist/runtime/approval-signing.d.ts +0 -111
  589. package/dist/runtime/approval-signing.js +0 -384
  590. package/dist/runtime/approval-signing.js.map +0 -1
  591. package/dist/runtime/bash-match-facts.d.ts +0 -60
  592. package/dist/runtime/bash-match-facts.js +0 -196
  593. package/dist/runtime/bash-match-facts.js.map +0 -1
  594. package/dist/runtime/bash-match-registry.d.ts +0 -112
  595. package/dist/runtime/bash-match-registry.js +0 -406
  596. package/dist/runtime/bash-match-registry.js.map +0 -1
  597. package/dist/runtime/bash-prefix-parse.d.ts +0 -31
  598. package/dist/runtime/bash-prefix-parse.js +0 -576
  599. package/dist/runtime/bash-prefix-parse.js.map +0 -1
  600. package/dist/runtime/command-normalize.d.ts +0 -658
  601. package/dist/runtime/command-normalize.js +0 -2410
  602. package/dist/runtime/command-normalize.js.map +0 -1
  603. package/dist/runtime/deletion-target-resolve.d.ts +0 -28
  604. package/dist/runtime/deletion-target-resolve.js +0 -1078
  605. package/dist/runtime/deletion-target-resolve.js.map +0 -1
  606. package/dist/runtime/destructive-shell-floor.d.ts +0 -19
  607. package/dist/runtime/destructive-shell-floor.js +0 -611
  608. package/dist/runtime/destructive-shell-floor.js.map +0 -1
  609. package/dist/runtime/environment-resolver.d.ts +0 -36
  610. package/dist/runtime/environment-resolver.js +0 -138
  611. package/dist/runtime/environment-resolver.js.map +0 -1
  612. package/dist/runtime/git-context.d.ts +0 -201
  613. package/dist/runtime/git-context.js +0 -422
  614. package/dist/runtime/git-context.js.map +0 -1
  615. package/dist/runtime/index.d.ts +0 -12
  616. package/dist/runtime/index.js +0 -13
  617. package/dist/runtime/index.js.map +0 -1
  618. package/dist/runtime/intercept.d.ts +0 -494
  619. package/dist/runtime/intercept.js +0 -1313
  620. package/dist/runtime/intercept.js.map +0 -1
  621. package/dist/runtime/kube-context.d.ts +0 -16
  622. package/dist/runtime/kube-context.js +0 -63
  623. package/dist/runtime/kube-context.js.map +0 -1
  624. package/dist/runtime/kubectl-target-parse.d.ts +0 -31
  625. package/dist/runtime/kubectl-target-parse.js +0 -249
  626. package/dist/runtime/kubectl-target-parse.js.map +0 -1
  627. package/dist/runtime/ledger-add.d.ts +0 -16
  628. package/dist/runtime/ledger-add.js +0 -150
  629. package/dist/runtime/ledger-add.js.map +0 -1
  630. package/dist/runtime/ledger-writer.d.ts +0 -27
  631. package/dist/runtime/ledger-writer.js +0 -46
  632. package/dist/runtime/ledger-writer.js.map +0 -1
  633. package/dist/runtime/pause-sentinel.d.ts +0 -60
  634. package/dist/runtime/pause-sentinel.js +0 -156
  635. package/dist/runtime/pause-sentinel.js.map +0 -1
  636. package/dist/runtime/pending-approval.d.ts +0 -23
  637. package/dist/runtime/pending-approval.js +0 -67
  638. package/dist/runtime/pending-approval.js.map +0 -1
  639. package/dist/runtime/read-only-bash.d.ts +0 -141
  640. package/dist/runtime/read-only-bash.js +0 -2204
  641. package/dist/runtime/read-only-bash.js.map +0 -1
  642. package/dist/runtime/recovery-git-commit.d.ts +0 -22
  643. package/dist/runtime/recovery-git-commit.js +0 -284
  644. package/dist/runtime/recovery-git-commit.js.map +0 -1
  645. package/dist/runtime/reject-malformed-session-id.d.ts +0 -8
  646. package/dist/runtime/reject-malformed-session-id.js +0 -16
  647. package/dist/runtime/reject-malformed-session-id.js.map +0 -1
  648. package/dist/runtime/risk-classifier.d.ts +0 -38
  649. package/dist/runtime/risk-classifier.js +0 -311
  650. package/dist/runtime/risk-classifier.js.map +0 -1
  651. package/dist/runtime/session-id.d.ts +0 -113
  652. package/dist/runtime/session-id.js +0 -193
  653. package/dist/runtime/session-id.js.map +0 -1
  654. package/dist/runtime/shell-word.d.ts +0 -62
  655. package/dist/runtime/shell-word.js +0 -410
  656. package/dist/runtime/shell-word.js.map +0 -1
  657. package/dist/runtime/task-providers/agent-tasks.d.ts +0 -103
  658. package/dist/runtime/task-providers/agent-tasks.js +0 -245
  659. package/dist/runtime/task-providers/agent-tasks.js.map +0 -1
  660. package/dist/runtime/when-eval.d.ts +0 -62
  661. package/dist/runtime/when-eval.js +0 -212
  662. package/dist/runtime/when-eval.js.map +0 -1
  663. package/dist/runtime/workflow-policies.d.ts +0 -207
  664. package/dist/runtime/workflow-policies.js +0 -596
  665. package/dist/runtime/workflow-policies.js.map +0 -1
  666. package/dist/schema/audit.d.ts +0 -71
  667. package/dist/schema/audit.js +0 -32
  668. package/dist/schema/audit.js.map +0 -1
  669. package/dist/schema/doctor.d.ts +0 -25
  670. package/dist/schema/doctor.js +0 -23
  671. package/dist/schema/doctor.js.map +0 -1
  672. package/dist/schema/environments.d.ts +0 -215
  673. package/dist/schema/environments.js +0 -101
  674. package/dist/schema/environments.js.map +0 -1
  675. package/dist/schema/extract.d.ts +0 -9
  676. package/dist/schema/extract.js +0 -86
  677. package/dist/schema/extract.js.map +0 -1
  678. package/dist/schema/permission-profiles.d.ts +0 -2292
  679. package/dist/schema/permission-profiles.js +0 -60
  680. package/dist/schema/permission-profiles.js.map +0 -1
  681. package/dist/schema/policies.d.ts +0 -974
  682. package/dist/schema/policies.js +0 -298
  683. package/dist/schema/policies.js.map +0 -1
  684. package/dist/schema/requires.d.ts +0 -131
  685. package/dist/schema/requires.js +0 -88
  686. package/dist/schema/requires.js.map +0 -1
  687. package/dist/schema/risk.d.ts +0 -144
  688. package/dist/schema/risk.js +0 -194
  689. package/dist/schema/risk.js.map +0 -1
  690. package/dist/schema/session-start-preflight.d.ts +0 -10
  691. package/dist/schema/session-start-preflight.js +0 -106
  692. package/dist/schema/session-start-preflight.js.map +0 -1
  693. package/dist/schema/stale-base-check.d.ts +0 -18
  694. package/dist/schema/stale-base-check.js +0 -37
  695. package/dist/schema/stale-base-check.js.map +0 -1
  696. package/dist/schema/toolchain-parity.d.ts +0 -21
  697. package/dist/schema/toolchain-parity.js +0 -48
  698. package/dist/schema/toolchain-parity.js.map +0 -1
  699. package/dist/schema/workflows.d.ts +0 -519
  700. package/dist/schema/workflows.js +0 -81
  701. package/dist/schema/workflows.js.map +0 -1
  702. package/scripts/runtime-reality-docker-probe.mjs +0 -71
package/dist/cli/index.js CHANGED
@@ -1,9 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
2
  import { Command } from "commander";
6
- import { escapeForDisplay } from "../io/display-path.js";
7
3
  import { HermeticSpawnViolationError } from "../runtime/hermetic-spawn-guard.js";
8
4
  // Production version probe for `harness doctor`: synchronous --version
9
5
  // invocation with a 5s timeout. Tests inject their own probe; the CLI
@@ -24,71 +20,15 @@ export function defaultVersionProbe(cmd) {
24
20
  return null;
25
21
  }
26
22
  }
27
- import { add } from "./add/index.js";
28
- import { adopt } from "./adopt/index.js";
29
- import { apply, CODEX_CONFIG_BASENAME, CodexInstallRefusalError, DRIFT_HINT_MESSAGE, formatNextSteps, formatRuntimeLine, OPENCODE_CONFIG_BASENAME, SETTINGS_BASENAME, } from "./apply/index.js";
30
- import { isRemoveType, KNOWN_REMOVE_TYPES, remove } from "./remove/index.js";
31
- import { packAdd, packList, packRemove, packReseed, packUpgrade } from "./pack/index.js";
32
- import { runPackHookPreToolUseCli } from "./pack/hook-pre-tool-use.js";
33
- import { runPackHookPostToolUseCli } from "./pack/hook-post-tool-use.js";
34
- import { runPackHookTrackActiveClaimCli } from "./pack/hook-track-active-claim.js";
35
- import { runPackHookStayInScopeCli } from "./pack/hook-stay-in-scope.js";
36
- import { runPackHookSubagentStartCli } from "./pack/hook-subagent-start.js";
37
- import { runPackHookSubagentStopCli } from "./pack/hook-subagent-stop.js";
38
- import { runPackHookCodexPreToolUseCli } from "./pack/hook-codex-pre-tool-use.js";
39
- import { runPackHookCodexPostToolUseCli } from "./pack/hook-codex-post-tool-use.js";
40
- import { runPackHookCodexStopCli } from "./pack/hook-codex-stop.js";
41
- import { runPackHookCodexUserPromptSubmitCli } from "./pack/hook-codex-user-prompt-submit.js";
42
- import { isRuntime, KNOWN_RUNTIMES } from "../policy-packs/index.js";
43
- import { approveBranchProtection } from "./approve/branch-protection.js";
44
- import { approveRisk } from "./approve/risk.js";
45
- import { approveUnderstanding } from "./approve/understanding.js";
46
- import { readPipedStdin } from "./approve/stdin-report.js";
47
- import { issueDelegation } from "./delegate/index.js";
48
- import { InvalidDurationError, parseDurationSeconds } from "../policies/index.js";
49
- import { runRecordDogfood, runRecordReview, runRecordReviewSubagent, } from "./record/index.js";
50
- import { describe, isPillar } from "./describe.js";
51
- import { diff as diffRun } from "./diff/index.js";
52
- import { diffSinceApply } from "./diff/since-apply.js";
53
- import { exportManifest } from "./export.js";
54
- import { doctor, isDoctorTarget, KNOWN_DOCTOR_TARGETS } from "./doctor/index.js";
55
- import { format as formatDoctor } from "./doctor/format.js";
56
- import { deleteRogueLedgers, scanForRogueLedgers, } from "./doctor/rogue-ledger.js";
57
- import { EX_FAIL, EX_USAGE, HarnessExitError } from "./exit-codes.js";
58
- import { explain } from "./explain.js";
59
- import { explainAction } from "./explain-action.js";
60
- import { explainPolicy } from "./explain-policy.js";
61
- import { testRisk } from "./test-risk.js";
62
- import { resolveEnv } from "./resolve-env.js";
63
- import { detect as detectInit } from "./init/detect.js";
64
- import { init, isTemplate, KNOWN_TEMPLATES } from "./init/index.js";
65
- import { runInteractive } from "./init/interactive.js";
66
- import { isListCategory, list } from "./list.js";
67
- import { audit } from "./audit.js";
68
- import { sessionExport } from "./session-export/index.js";
69
- import { dryRun } from "./dry-run.js";
70
- import { runInterceptCli } from "./policy/intercept.js";
71
- import { runSessionStartPreflight } from "./session-start/index.js";
72
- import { writePendingApproval } from "../runtime/pending-approval.js";
73
- import { runSessionStartBranchCheck } from "./session-start/branch-check.js";
74
- import { runSessionStartToolchainParity } from "./session-start/toolchain-parity.js";
75
- import { runSessionStartStaleBaseCheck } from "./session-start/stale-base-check.js";
76
- import { addCwdOption, addIdentityOptions, addLedgerTimeoutOption, applyCliOptions, } from "./session-start/shared-options.js";
77
- import { runPackHookBranchProtectionCli } from "./pack/hook-branch-protection.js";
78
- import { runPackHookSolutionAcceptanceCli } from "./pack/hook-solution-acceptance.js";
79
- import { runPackHookSolutionAcceptanceWriteguardCli } from "./pack/hook-solution-acceptance-writeguard.js";
80
- import { runPackHookPostMergeGateRecordCli } from "./pack/hook-post-merge-gate-record.js";
81
- import { runPackHookPostMergeGateCli } from "./pack/hook-post-merge-gate.js";
82
- import { runPackHookRuntimeRealityCli } from "./pack/hook-runtime-reality.js";
83
- import { gateDisable, GateDisableError } from "./gate/disable.js";
84
- import { gateEnable, GateEnableError } from "./gate/enable.js";
85
- import { DEFAULT_RETENTION_DAYS, gc } from "./gc/index.js";
86
- import { uninstall, UninstallError } from "./uninstall/index.js";
87
- import { migrateHome } from "./migrate-home/index.js";
88
- import { pause as pauseHarness, resume as resumeHarness } from "./pause/index.js";
89
- import { formatSmokeReport, runSmoke, splitCommaList, } from "./smoke/index.js";
90
- import { formatReport, validate } from "./validate/index.js";
23
+ import { EX_USAGE, HarnessExitError } from "./exit-codes.js";
91
24
  import { VERSION } from "../version.js";
25
+ import { registerRemove } from "./remove/register.js";
26
+ import { registerUninstall } from "./uninstall/register.js";
27
+ import { registerOperatorLifecycle } from "./register-operator-lifecycle.js";
28
+ import { registerInspectGroup } from "./register-inspect-group.js";
29
+ import { registerSetupGroup } from "./register-setup-group.js";
30
+ import { registerPackGroup } from "./register-pack-group.js";
31
+ import { registerGateGroup } from "./register-gate-group.js";
92
32
  export function buildProgram(opts = {}) {
93
33
  const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
94
34
  const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
@@ -114,2545 +54,13 @@ export function buildProgram(opts = {}) {
114
54
  const code = err.exitCode === 1 ? EX_USAGE : (err.exitCode ?? EX_USAGE);
115
55
  throw new HarnessExitError("", code);
116
56
  });
117
- program
118
- .command("describe")
119
- .description("Print the effective merged manifest")
120
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
121
- .option("--project <name>", "apply per-project overrides for this project name")
122
- .option("--pillar <pillar>", "filter output to one section: grounding | tools | memory | hooks | policies | workflows | review_templates")
123
- .option("--json", "emit JSON instead of YAML")
124
- .action((options) => {
125
- let pillar;
126
- if (options.pillar !== undefined) {
127
- if (!isPillar(options.pillar)) {
128
- throw new HarnessExitError(`unknown pillar "${options.pillar}"; expected one of grounding, tools, memory, hooks, policies, workflows, review_templates`, EX_USAGE);
129
- }
130
- pillar = options.pillar;
131
- }
132
- const result = describe({
133
- configPath: options.config,
134
- project: options.project,
135
- pillar,
136
- json: options.json,
137
- });
138
- stdout(result.output);
139
- if (!result.output.endsWith("\n"))
140
- stdout("\n");
141
- });
142
- program
143
- .command("validate")
144
- .description("Lint the manifest + referenced assets")
145
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
146
- .option("--project <name>", "apply per-project overrides for this project name")
147
- .option("--strict", "promote warnings to errors")
148
- .option("--check-lock", "surface harness.lock asset-content drift as warnings (or errors with --strict)")
149
- .option("--json", "emit a structured JSON report ({ diagnostics, errorCount, warningCount }) instead of prose")
150
- .action((options) => {
151
- const result = validate({
152
- configPath: options.config,
153
- project: options.project,
154
- strict: options.strict,
155
- checkLock: options.checkLock,
156
- });
157
- if (options.json) {
158
- // JSON goes to stdout regardless of outcome so pipelines can
159
- // always parse it; the exit code still carries pass/fail.
160
- stdout(`${JSON.stringify({
161
- diagnostics: result.diagnostics,
162
- errorCount: result.errorCount,
163
- warningCount: result.warningCount,
164
- }, null, 2)}\n`);
165
- }
166
- else {
167
- const report = formatReport(result);
168
- if (result.diagnostics.length > 0)
169
- stderr(report);
170
- else
171
- stdout(report);
172
- }
173
- if (result.errorCount > 0) {
174
- throw new HarnessExitError("", EX_FAIL);
175
- }
176
- });
177
- program
178
- .command("doctor")
179
- .description("Health summary across all pillars")
180
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
181
- .option("--project <name>", "apply per-project overrides for this project name")
182
- .option("--shallow", "skip MCP probes (CLI --version probes still run); report manifest-reference state only")
183
- .option("--target <runtime>", `additionally evaluate the harness-side adapter health for a runtime (allowed: ${KNOWN_DOCTOR_TARGETS.join(", ")})`)
184
- .option("--recent-sessions <n>", "understanding-gate auto-approval doctor window: how many of the newest .approvals/ session markers to scan (default: 20)")
185
- .option("--json", "emit a structured JSON DoctorReport instead of prose (--rm-rogue-ledgers is ignored with --json)")
186
- .option("--rm-rogue-ledgers", "after reporting, delete each rogue evidence-ledger directory found (prompts per hit; combine with --yes to skip prompts)")
187
- .option("--yes", "with --rm-rogue-ledgers, skip per-hit confirmation prompts")
188
- .action(async (options) => {
189
- let target;
190
- if (options.target !== undefined) {
191
- if (!isDoctorTarget(options.target)) {
192
- stderr(`unknown --target ${JSON.stringify(options.target)}; expected one of ${KNOWN_DOCTOR_TARGETS.join(", ")}\n`);
193
- throw new HarnessExitError("", EX_USAGE);
194
- }
195
- target = options.target;
196
- }
197
- let recentSessions;
198
- if (options.recentSessions !== undefined) {
199
- const parsed = Number(options.recentSessions);
200
- if (!Number.isInteger(parsed) || parsed < 1) {
201
- stderr(`--recent-sessions must be an integer >= 1, got ${JSON.stringify(options.recentSessions)}\n`);
202
- throw new HarnessExitError("", EX_USAGE);
203
- }
204
- recentSessions = parsed;
205
- }
206
- const report = await doctor({
207
- configPath: options.config,
208
- project: options.project,
209
- shallow: options.shallow,
210
- versionProbe: defaultVersionProbe,
211
- ...(target !== undefined ? { target } : {}),
212
- ...(recentSessions !== undefined ? { recentSessions } : {}),
213
- ...(opts.rogueLedgerScanOptions !== undefined
214
- ? { rogueLedgerScanOptions: opts.rogueLedgerScanOptions }
215
- : {}),
216
- });
217
- // Non-zero exit when the report carries errors (task a07b379a):
218
- // callers previously had no way to gate CI/scripts on doctor
219
- // health, since this action always fell through to exit 0
220
- // regardless of report.errorCount. Warnings alone still exit 0.
221
- // Checked before every return below (json / no --rm-rogue-ledgers /
222
- // no hits found / after the delete+rescan flow) so the exit code
223
- // is consistent across all output modes.
224
- const failIfErrors = () => {
225
- if (report.errorCount > 0) {
226
- throw new HarnessExitError("", EX_FAIL);
227
- }
228
- };
229
- if (options.json) {
230
- stdout(`${JSON.stringify(report, null, 2)}\n`);
231
- failIfErrors();
232
- return;
233
- }
234
- stdout(formatDoctor(report));
235
- if (!options.rmRogueLedgers) {
236
- failIfErrors();
237
- return;
238
- }
239
- const hits = report.rogueLedgerDbs;
240
- if (hits.length === 0) {
241
- stdout("no rogue evidence-ledger DBs found; nothing to delete\n");
242
- failIfErrors();
243
- return;
244
- }
245
- const result = await deleteRogueLedgers(hits, { yes: options.yes });
246
- for (const hit of result.deleted) {
247
- stdout(`deleted: ${hit.rogueDir}\n`);
248
- }
249
- for (const hit of result.skipped) {
250
- stdout(`skipped: ${hit.rogueDir}\n`);
251
- }
252
- // Re-scan and print clean delta so the operator can confirm the
253
- // on-disk state after deletion. Uses the same scan options as the
254
- // initial scan (injectable via RunOptions.rogueLedgerScanOptions for
255
- // tests; production falls back to os.homedir() / process.cwd()).
256
- const afterScan = scanForRogueLedgers({
257
- homeDir: opts.rogueLedgerScanOptions?.homeDir ?? os.homedir(),
258
- cwd: opts.rogueLedgerScanOptions?.cwd ?? process.cwd(),
259
- ...(opts.rogueLedgerScanOptions?.fsInterface !== undefined
260
- ? { fsInterface: opts.rogueLedgerScanOptions.fsInterface }
261
- : {}),
262
- });
263
- stdout(`rogue evidence-ledger DBs remaining: ${afterScan.length}\n`);
264
- failIfErrors();
265
- });
266
- program
267
- .command("list <category>")
268
- .description("Flat denormalised listing per category: mcp / cli / skills / memories / hooks / policies / workflows")
269
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
270
- .option("--project <name>", "apply per-project overrides")
271
- .option("--filter <substr>", "case-insensitive substring filter on name (or path for memories)")
272
- .option("--json", "emit JSON array instead of an aligned text table")
273
- .action((category, options) => {
274
- if (!isListCategory(category)) {
275
- throw new HarnessExitError(`unknown list category "${category}"; expected one of mcp, cli, skills, memories, hooks, policies, workflows`, EX_USAGE);
276
- }
277
- const result = list(category, {
278
- configPath: options.config,
279
- project: options.project,
280
- filter: options.filter,
281
- json: options.json,
282
- });
283
- stdout(result.output);
284
- });
285
- program
286
- .command("diff")
287
- .description("Diff the manifest against a git ref (--since <ref>) or against the last " +
288
- "applied state (--since-apply). --memory-detail expands per-memory-dir " +
289
- "Merkle drift back to per-file changes.")
290
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
291
- .option("--project <name>", "apply per-project overrides")
292
- .option("--since <ref>", "git ref to diff against")
293
- .option("--since-apply", "diff against harness.generated/.last-apply")
294
- .option("--memory-detail", "expand per-memory-dir drift to per-file changes")
295
- .option("--json", "emit structured JSON output")
296
- .action((options) => {
297
- if (options.since && options.sinceApply) {
298
- throw new HarnessExitError("--since <ref> and --since-apply are mutually exclusive", EX_USAGE);
299
- }
300
- if (options.sinceApply) {
301
- const r = diffSinceApply({
302
- ...(options.config !== undefined ? { configPath: options.config } : {}),
303
- ...(options.memoryDetail ? { memoryDetail: true } : {}),
304
- });
305
- if (options.json) {
306
- stdout(`${JSON.stringify(r.json, null, 2)}\n`);
307
- }
308
- else if (!r.hasDrift) {
309
- stdout("no drift since last apply\n");
310
- }
311
- else {
312
- stdout(r.output);
313
- }
314
- for (const w of r.warnings) {
315
- stderr(`warning: ${w}\n`);
316
- }
317
- if (r.hasDrift)
318
- throw new HarnessExitError("", EX_FAIL);
319
- return;
320
- }
321
- const result = diffRun({
322
- configPath: options.config,
323
- project: options.project,
324
- since: options.since,
325
- });
326
- // Override-layer diagnostics go to stderr so stdout stays a clean
327
- // diff for piping (task b2660f9e).
328
- for (const w of result.warnings)
329
- stderr(`harness diff: ${w}\n`);
330
- stdout(result.output);
331
- });
332
- program
333
- .command("init")
334
- .description("Bootstrap a starter harness.yaml from a template")
335
- .option("--template <name>", `template to instantiate: ${KNOWN_TEMPLATES.join(" | ")} (default: minimal)`)
336
- .option("--force", "overwrite an existing manifest")
337
- .option("--config <path>", "manifest path to write (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
338
- .option("--probe", "skip writing, print a JSON snapshot of detected runtimes (Claude Code, Codex), the existing ~/.harness/harness.yaml (or legacy ~/.claude/harness.yaml), and MCP servers wired in settings.json. Read-only.")
339
- .option("--interactive", "run the guided wizard (detect environment, pick profile, preview + write). Mutually exclusive with --probe / --template.")
340
- .action(async (options) => {
341
- if (options.probe && options.interactive) {
342
- throw new HarnessExitError("--probe and --interactive are mutually exclusive", EX_USAGE);
343
- }
344
- if (options.probe) {
345
- if (options.template !== undefined || options.force || options.config !== undefined) {
346
- throw new HarnessExitError("--probe is read-only; pass it without --template / --force / --config", EX_USAGE);
347
- }
348
- const result = await detectInit();
349
- stdout(`${JSON.stringify(result, null, 2)}\n`);
350
- return;
351
- }
352
- if (options.interactive) {
353
- if (options.template !== undefined || options.config !== undefined) {
354
- throw new HarnessExitError("--interactive owns its own template + path choices; do not combine with --template / --config", EX_USAGE);
355
- }
356
- // Thread --force into the wizard's forceOverwrite: without it
357
- // an existing manifest makes the wizard re-prompt, and
358
- // `harness init --interactive --force` looked like it should
359
- // overwrite non-interactively but the flag never reached
360
- // runInteractive (harness/418cebd4).
361
- const r = await runInteractive({
362
- stdout,
363
- stderr,
364
- ...(options.force ? { forceOverwrite: true } : {}),
365
- });
366
- if (r.aborted) {
367
- return;
368
- }
369
- if (r.validateClean === false) {
370
- throw new HarnessExitError(`manifest written but failed harness validate; see stderr for diagnostics`, EX_FAIL);
371
- }
372
- if (r.binResolutionClean === false) {
373
- throw new HarnessExitError(`manifest written but one or more declared MCP/CLI binaries do not resolve on PATH; see stderr for diagnostics and remediation hints`, EX_FAIL);
374
- }
375
- return;
376
- }
377
- if (options.template !== undefined && !isTemplate(options.template)) {
378
- throw new HarnessExitError(`unknown template "${options.template}"; expected one of ${KNOWN_TEMPLATES.join(", ")}`, EX_USAGE);
379
- }
380
- const result = await init({
381
- template: options.template,
382
- force: options.force,
383
- configPath: options.config,
384
- });
385
- if (result.stderr)
386
- stderr(result.stderr);
387
- stdout(result.stdout);
388
- });
389
- const addCmd = program
390
- .command("add")
391
- .description("Insert a new entry into harness.yaml (managed mutation)");
392
- function addCommonOptions(c) {
393
- return c
394
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
395
- .option("--dry-run", "print the unified diff and exit without writing");
396
- }
397
- function parseBlocking(s) {
398
- if (s === "false")
399
- return false;
400
- if (s === "soft" || s === "hard")
401
- return s;
402
- throw new HarnessExitError(`invalid --blocking value "${s}"; expected one of false, soft, hard`, EX_USAGE);
403
- }
404
- function parseIntFlag(s, label) {
405
- const n = Number.parseInt(s, 10);
406
- if (!Number.isFinite(n) || String(n) !== s.trim()) {
407
- throw new HarnessExitError(`invalid ${label} value "${s}"; expected an integer`, EX_USAGE);
408
- }
409
- return n;
410
- }
411
- async function runAdd(action, opts) {
412
- const result = await add(action, { configPath: opts.config, dryRun: opts.dryRun });
413
- for (const w of result.warnings) {
414
- stderr(`warning: ${w}\n`);
415
- }
416
- if (opts.dryRun) {
417
- stdout(result.diff);
418
- return;
419
- }
420
- stdout(`added ${result.type} ${JSON.stringify(result.name)} to ${result.path}\n`);
421
- }
422
- addCommonOptions(addCmd
423
- .command("mcp <name>")
424
- .description("Add an MCP server entry under tools.mcp[]")
425
- .option("--command <cmd>", "argv-style command; comma-separated for multi-token")
426
- .option("--health-verb <v>", "MCP verb to invoke for liveness")
427
- .option("--health-timeout-ms <n>", "verb timeout in ms (default 5000 when --health-verb is set)")
428
- .option("--enabled <bool>", "true|false (default true)")).action(async (name, options) => {
429
- const command = options.command
430
- ? options.command.includes(",")
431
- ? options.command.split(",").map((s) => s.trim())
432
- : options.command
433
- : "";
434
- if (!command) {
435
- throw new HarnessExitError("harness add mcp: --command is required", EX_USAGE);
436
- }
437
- const entry = {
438
- name,
439
- command,
440
- };
441
- if (options.healthVerb !== undefined) {
442
- const timeoutMs = options.healthTimeoutMs
443
- ? parseIntFlag(options.healthTimeoutMs, "--health-timeout-ms")
444
- : 5000;
445
- entry.health = {
446
- verb: options.healthVerb,
447
- timeout_ms: timeoutMs,
448
- };
449
- }
450
- if (options.enabled !== undefined) {
451
- if (options.enabled !== "true" && options.enabled !== "false") {
452
- throw new HarnessExitError(`invalid --enabled value "${options.enabled}"; expected true or false`, EX_USAGE);
453
- }
454
- entry.enabled = options.enabled === "true";
455
- }
456
- await runAdd({ type: "mcp", entry: entry }, { config: options.config, dryRun: options.dryRun });
457
- });
458
- addCommonOptions(addCmd
459
- .command("cli <name>")
460
- .description("Add a CLI tool entry under tools.cli[]")
461
- .requiredOption("--binary <b>", "binary name on PATH or absolute path")
462
- .option("--required", "validate fails if the binary is missing")
463
- .option("--min-version <v>", "minimum semver")).action(async (name, options) => {
464
- const entry = {
465
- name,
466
- binary: options.binary,
467
- };
468
- if (options.required)
469
- entry.required = true;
470
- if (options.minVersion !== undefined)
471
- entry.min_version = options.minVersion;
472
- await runAdd({ type: "cli", entry }, { config: options.config, dryRun: options.dryRun });
473
- });
474
- addCommonOptions(addCmd
475
- .command("skill <name>")
476
- .description("Enable a skill by name under tools.skills.enabled[]")).action(async (name, options) => {
477
- await runAdd({ type: "skill", entry: name }, { config: options.config, dryRun: options.dryRun });
478
- });
479
- addCommonOptions(addCmd
480
- .command("hook <name>")
481
- .description("Add a hook entry under hooks[]")
482
- .requiredOption("--event <e>", "runtime event (e.g. SessionStart, PreToolUse)")
483
- .requiredOption("--command <c>", "shell command (executable path or script with args)")
484
- .option("--match <r>", "tool-name regex filter (PreToolUse / PostToolUse only)")
485
- .option("--blocking <m>", "false | soft | hard (default false)")
486
- .option("--budget-ms <n>", "timeout in ms (default 30000)")).action(async (name, options) => {
487
- const entry = {
488
- name,
489
- event: options.event,
490
- command: options.command,
491
- blocking: options.blocking !== undefined ? parseBlocking(options.blocking) : false,
492
- };
493
- if (options.match !== undefined)
494
- entry.match = options.match;
495
- if (options.budgetMs !== undefined) {
496
- entry.budget_ms = parseIntFlag(options.budgetMs, "--budget-ms");
497
- }
498
- await runAdd({ type: "hook", entry }, { config: options.config, dryRun: options.dryRun });
499
- });
500
- program
501
- .command("export")
502
- .description("Emit the effective merged manifest as a single self-contained YAML " +
503
- "(or JSON). --sanitize redacts /home/<user>/ paths and env values whose " +
504
- "key looks credential-shaped.")
505
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
506
- .option("--project <name>", "apply per-project overrides for this project name")
507
- .option("--sanitize", "redact /home/<user>/ paths and credential-shaped env values")
508
- .option("--json", "emit JSON instead of YAML")
509
- .option("-o, --output <file>", "write to <file> atomically instead of stdout")
510
- .action((options) => {
511
- const result = exportManifest({
512
- configPath: options.config,
513
- project: options.project,
514
- sanitize: options.sanitize,
515
- json: options.json,
516
- outputPath: options.output,
517
- });
518
- if (result.wroteTo === null) {
519
- stdout(result.output);
520
- if (!result.output.endsWith("\n"))
521
- stdout("\n");
522
- }
523
- else {
524
- stderr(`wrote ${result.wroteTo}\n`);
525
- }
526
- });
527
- program
528
- .command("adopt <file>")
529
- .description("Capture hand-edits into the manifest. Hooks are diffed against <file> " +
530
- "(today: ~/.claude/settings.json). MCP servers are ALWAYS diffed " +
531
- "against the effective Claude Code registry (top-level mcpServers in " +
532
- "~/.claude.json, respecting CLAUDE_CONFIG_DIR) regardless of <file> — " +
533
- "Claude Code does not read <file>'s mcpServers block at runtime. " +
534
- "Prompts y/N before writing.")
535
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
536
- .option("--yes", "skip the confirmation prompt (for non-interactive use)")
537
- .action(async (file, options) => {
538
- const result = await adopt(file, { configPath: options.config, yes: options.yes });
539
- if (result.deadSettingsMcpNames.length > 0) {
540
- stderr(`⚠ ${file} has a dead \`mcpServers\` block (${result.deadSettingsMcpNames.join(", ")}); ` +
541
- "Claude Code does not read this file for MCP registration — adopt diffed against the " +
542
- "effective registry instead. Safe to remove by hand.\n");
543
- }
544
- if (result.registryReadError !== undefined) {
545
- stderr(`⚠ could not read the Claude Code MCP registry: ${result.registryReadError}; ` +
546
- "treated as empty for MCP drift purposes.\n");
547
- }
548
- if (result.outcome === "no-drift") {
549
- stdout(`nothing to adopt (no drift between ${file} and ${result.manifestPath})\n`);
550
- return;
551
- }
552
- if (result.outcome === "declined") {
553
- stdout(`adoption declined; ${result.manifestPath} unchanged\n`);
554
- return;
555
- }
556
- const parts = [];
557
- if (result.hookDriftCount > 0) {
558
- parts.push(`${result.hookDriftCount} hook${result.hookDriftCount === 1 ? "" : "s"}` +
559
- ` (${result.adoptedNames.join(", ")})`);
560
- }
561
- if (result.mcpDriftCount > 0) {
562
- const mcpFrag = `${result.mcpDriftCount} MCP server${result.mcpDriftCount === 1 ? "" : "s"}` +
563
- ` (${result.adoptedMcpNames.join(", ")})`;
564
- parts.push(result.replacedMcpNames.length > 0
565
- ? `${mcpFrag}; replaced existing manifest entry for: ${result.replacedMcpNames.join(", ")}`
566
- : mcpFrag);
567
- }
568
- stdout(`adopted ${parts.join(" + ")} from ${result.settingsPath} into ${result.manifestPath}\n`);
569
- });
570
- program
571
- .command("apply")
572
- .description("Regenerate harness.generated/ outputs (settings.json + MEMORY.md index) " +
573
- "from the manifest. Refuses to overwrite hand-edits without --overwrite-drift; " +
574
- "use `harness adopt <file>` to capture them back into the manifest instead.")
575
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
576
- .option("--project <name>", "apply per-project overrides for this project name")
577
- .option("--dry-run", "print the would-be diff + restart hints; do not write")
578
- .option("--overwrite-drift", "discard any on-disk hand-edits to harness.generated/ files (prompts for `yes`; pair with --yes for non-interactive runs)")
579
- .option("--yes", "with --overwrite-drift, skip the confirmation prompt (for non-interactive use)")
580
- .option("--strict-lock", "refuse with exit 1 (no write) when harness.lock asset drift is detected; dry-run wins")
581
- .option("--target <path>", "additionally write the generated settings.json to <path> (e.g. .claude/settings.local.json); " +
582
- "claude-code only: without --runtime it implies claude-code, with --runtime codex or opencode it is refused")
583
- .option("--merge", "with --target, 3-way merge into an existing target file (replace owned keys, preserve others)")
584
- .option("--force", "with --target, overwrite an existing target file (no merge)")
585
- .option("--runtime <runtime>", `policy-pack adapter runtime (${KNOWN_RUNTIMES.join(" | ")}; default: claude-code with --target, ` +
586
- "otherwise the runtime of the last apply (inferred from its files for an older record), " +
587
- "or claude-code when there is none; --install follows the same default). " +
588
- "Selects which adapter shape policy-pack hooks expand into and which artefacts apply writes. " +
589
- "`codex` emits harness.generated/codex/config.toml in place of settings.json. " +
590
- "`opencode` emits harness.generated/opencode/opencode.json (MCP servers only; not auto-installed).")
591
- .option("--install", "with --runtime codex, merge the generated hook block into ~/.codex/config.toml")
592
- .option("--codex-config <path>", "with --runtime codex --install, override the Codex config path (default ~/.codex/config.toml)")
593
- .option("--quiet", "suppress the post-apply Next-steps hint")
594
- .option("--json", "emit a structured JSON summary instead of prose (implies --quiet)")
595
- .action(async (options) => {
596
- let runtime;
597
- if (options.runtime !== undefined) {
598
- if (!isRuntime(options.runtime)) {
599
- stderr(`unknown --runtime ${JSON.stringify(options.runtime)}; expected one of ${KNOWN_RUNTIMES.join(", ")}\n`);
600
- throw new HarnessExitError("", EX_USAGE);
601
- }
602
- runtime = options.runtime;
603
- }
604
- // --json is documented as implying --quiet. Normalize early so any
605
- // future fall-through path (or new prose branch) honors it without
606
- // depending on the JSON early-return below as the only chokepoint.
607
- if (options.json)
608
- options.quiet = true;
609
- // `apply()` throws `CodexInstallRefusalError` (rather than
610
- // returning a refuse `outcome`, the way `target-exists-refuse` /
611
- // `drift-refuse` do) for a codex-install refusal, because the
612
- // refusal is detected deep inside `planCodexConfigInstall`, before
613
- // an `ApplyResult` can be assembled. Catch it here so `--json`
614
- // still gets a structured error on stdout the same way every
615
- // other refusal does, instead of only the plain-text message
616
- // `run()`'s top-level catch would otherwise print.
617
- let result;
618
- try {
619
- result = await apply({
620
- ...(options.config !== undefined ? { configPath: options.config } : {}),
621
- ...(options.project !== undefined ? { project: options.project } : {}),
622
- ...(options.dryRun ? { dryRun: true } : {}),
623
- ...(options.overwriteDrift ? { overwriteDrift: true } : {}),
624
- ...(options.yes ? { yes: true } : {}),
625
- ...(options.strictLock ? { strictLock: true } : {}),
626
- ...(options.target !== undefined ? { target: options.target } : {}),
627
- ...(options.merge ? { merge: true } : {}),
628
- ...(options.force ? { force: true } : {}),
629
- ...(runtime !== undefined ? { runtime } : {}),
630
- ...(options.install ? { installCodex: true } : {}),
631
- ...(options.codexConfig !== undefined
632
- ? { codexConfigPath: options.codexConfig }
633
- : {}),
634
- });
635
- }
636
- catch (err) {
637
- if (err instanceof CodexInstallRefusalError && options.json) {
638
- stdout(`${JSON.stringify({ outcome: "codex-install-refuse", configPath: err.configPath, error: err.message }, null, 2)}\n`);
639
- throw new HarnessExitError("", EX_FAIL);
640
- }
641
- throw err;
642
- }
643
- if (options.json) {
644
- // Machine-readable: one JSON object on stdout regardless of
645
- // outcome. Refusals still set the non-zero exit below; consumers
646
- // should check both `outcome` in the JSON and the process exit.
647
- stdout(`${JSON.stringify(result, null, 2)}\n`);
648
- if (result.outcome === "target-exists-refuse" ||
649
- result.outcome === "lock-drift-refuse" ||
650
- result.outcome === "drift-refuse") {
651
- throw new HarnessExitError("", EX_FAIL);
652
- }
653
- return;
654
- }
655
- if (result.outcome === "target-exists-refuse") {
656
- stderr(`target ${result.targetPath} exists; pass --merge to merge into it, or --force to overwrite\n`);
657
- throw new HarnessExitError("", EX_FAIL);
658
- }
659
- if (result.outcome === "lock-drift-refuse") {
660
- for (const d of result.lockDrift) {
661
- if (d.reason === "missing") {
662
- stderr(`asset drift detected: ${d.entry.path} missing since last apply\n`);
663
- }
664
- else {
665
- stderr(`asset drift detected: ${d.entry.path} changed since last apply\n`);
666
- }
667
- }
668
- stderr("--strict-lock: refusing to overwrite the lock; re-run without --strict-lock to acknowledge, or revert the upstream asset edit\n");
669
- throw new HarnessExitError("", EX_FAIL);
670
- }
671
- if (result.outcome === "drift-refuse") {
672
- for (const f of result.files) {
673
- if (f.diff) {
674
- stderr(`drift detected in ${f.path}:\n`);
675
- stderr(f.diff);
676
- if (!f.diff.endsWith("\n"))
677
- stderr("\n");
678
- }
679
- }
680
- stderr(`${DRIFT_HINT_MESSAGE}\n`);
681
- throw new HarnessExitError("", EX_FAIL);
682
- }
683
- if (result.outcome === "drift-discarded") {
684
- stdout("overwrite-drift declined; nothing written\n");
685
- return;
686
- }
687
- const changedFiles = result.files.filter((f) => f.changed);
688
- // Name a reused or switched runtime before the file list, in the
689
- // dry-run and the real apply alike (agent-tasks b9e6d63c).
690
- const runtimeLine = formatRuntimeLine(result);
691
- if (runtimeLine !== null)
692
- stdout(`${runtimeLine}\n`);
693
- if (result.outcome === "no-changes") {
694
- stdout("no changes\n");
695
- }
696
- else if (result.outcome === "would-apply") {
697
- stdout(`would apply ${changedFiles.length} file(s):\n`);
698
- for (const f of changedFiles) {
699
- stdout(` ${f.path}\n`);
700
- }
701
- if (result.codexConfigInstall?.changed) {
702
- stdout(` ${result.codexConfigInstall.configPath} (Codex install)\n`);
703
- for (const id of result.codexConfigInstall.removedHookIds) {
704
- stdout(` removing hook block: ${id}\n`);
705
- }
706
- for (const section of result.codexConfigInstall.foreignSectionsPreserved) {
707
- stdout(` preserving foreign section: ${section}\n`);
708
- }
709
- }
710
- }
711
- else {
712
- stdout(`applied ${changedFiles.length} file(s):\n`);
713
- for (const f of changedFiles) {
714
- stdout(` ${f.path}\n`);
715
- }
716
- if (result.targetPath && (result.targetWritten || result.targetInSync)) {
717
- if (result.targetWritten) {
718
- if (result.targetMergeSummary) {
719
- stdout(`${result.targetMergeSummary}\n`);
720
- }
721
- else {
722
- stdout(`wrote target: ${result.targetPath}\n`);
723
- }
724
- }
725
- else {
726
- // Idempotent re-apply: the merge was a no-op because the
727
- // target already held the merged content. Report it as
728
- // success, not silence.
729
- stdout(`target already in sync: ${result.targetPath}\n`);
730
- }
731
- }
732
- if (result.codexConfigInstall?.written) {
733
- stdout(`${result.codexConfigInstall.summary}\n`);
734
- for (const id of result.codexConfigInstall.removedHookIds) {
735
- stdout(` removed hook block: ${id}\n`);
736
- }
737
- for (const section of result.codexConfigInstall.foreignSectionsPreserved) {
738
- stdout(` preserved foreign section: ${section}\n`);
739
- }
740
- if (result.codexConfigInstall.backupPath) {
741
- stdout(`backup written to ${result.codexConfigInstall.backupPath}\n`);
742
- }
743
- }
744
- stdout(`harness.lock written to ${result.lockPath}\n`);
745
- if (!options.quiet) {
746
- // `formatNextSteps` collapses to the single "wired into ..."
747
- // line when it receives `targetPath`. Pass it whenever the
748
- // target ended this run in sync — written this run, OR
749
- // already byte-identical (`targetInSync`). Gating on
750
- // `targetWritten` alone misclassified an idempotent
751
- // re-apply as "nothing is wired" and looped the operator
752
- // through redundant apply commands.
753
- // `anyChanged` softens the no-target lede when the generated
754
- // manifest is already up to date (avoids over-claiming
755
- // "nothing is wired" against operators who wired a target
756
- // on a previous run).
757
- const generatedSettingsPath = path.join(result.generatedDir, SETTINGS_BASENAME);
758
- const codexConfigPath = path.join(result.generatedDir, CODEX_CONFIG_BASENAME);
759
- const opencodeConfigPath = path.join(result.generatedDir, OPENCODE_CONFIG_BASENAME);
760
- const anyChanged = result.files.some((f) => f.changed);
761
- stdout(formatNextSteps({
762
- generatedSettingsPath,
763
- codexConfigPath,
764
- opencodeConfigPath,
765
- anyChanged,
766
- runtime: result.runtime,
767
- ...((result.targetWritten || result.targetInSync) && result.targetPath
768
- ? { targetPath: result.targetPath }
769
- : {}),
770
- // Pass the --install outcome through whenever one was
771
- // requested this run (written or a no-op), so the codex
772
- // lede reflects reality instead of always claiming
773
- // "nothing is installed yet".
774
- ...(result.codexConfigInstall !== undefined
775
- ? {
776
- codexInstall: {
777
- configPath: result.codexConfigInstall.configPath,
778
- written: result.codexConfigInstall.written,
779
- ...(result.codexConfigInstall.backupPath !== undefined
780
- ? { backupPath: result.codexConfigInstall.backupPath }
781
- : {}),
782
- },
783
- }
784
- : {}),
785
- }));
786
- }
787
- }
788
- for (const d of result.lockDrift) {
789
- if (d.reason === "missing") {
790
- stderr(`asset drift detected: ${d.entry.path} missing since last apply\n`);
791
- }
792
- else {
793
- stderr(`asset drift detected: ${d.entry.path} changed since last apply\n`);
794
- }
795
- }
796
- for (const w of result.warnings) {
797
- stderr(`warning: ${w}\n`);
798
- }
799
- for (const h of result.restartHints) {
800
- stderr(`restart hint: ${h}\n`);
801
- }
802
- });
803
- program
804
- .command("remove <type> <name>")
805
- .description(`Remove an entry by name. <type> is one of ${KNOWN_REMOVE_TYPES.join(" | ")}. ` +
806
- "Refuses to remove a hook still referenced by a policy unless --force.")
807
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
808
- .option("--dry-run", "print the unified diff and exit without writing")
809
- .option("--force", "remove even if a policy references this entry (dangling policy.hook is then caught by schema)")
810
- .action(async (type, name, options) => {
811
- if (!isRemoveType(type)) {
812
- throw new HarnessExitError(`unknown remove type "${type}"; expected one of ${KNOWN_REMOVE_TYPES.join(", ")}`, EX_USAGE);
813
- }
814
- const result = await remove(type, name, {
815
- configPath: options.config,
816
- dryRun: options.dryRun,
817
- force: options.force,
818
- });
819
- if (result.forcedReferences.length > 0) {
820
- stderr(`(forced removal — referenced by: ${result.forcedReferences.join(", ")})\n`);
821
- }
822
- // F3 (review round 3, 99f47307 Slice 1): printed on the dry-run AND
823
- // the write path. A --force'd removal of an evidence hook silently
824
- // disables the workflows[]-derived merge gate (no schema safety
825
- // net, unlike a dangling policy.hook), so this line is the only
826
- // warning the operator gets.
827
- if (result.derivedGateReferences.length > 0) {
828
- stderr(`(forced removal disables the workflows[]-derived merge gate for: ` +
829
- `${result.derivedGateReferences.join(", ")}; harness policy intercept ` +
830
- `no longer blocks merges for ${result.derivedGateReferences.length === 1 ? "this workflow" : "these workflows"} ` +
831
- `and for any other workflow sharing the same merge surface)\n`);
832
- }
833
- if (options.dryRun) {
834
- stdout(result.diff);
835
- return;
836
- }
837
- stdout(`removed ${result.type} ${JSON.stringify(result.name)} from ${result.path}\n`);
838
- });
839
- // `harness pack` subtree (Phase 6 #3): managed CRUD over policy_packs[].
840
- const packCmd = program
841
- .command("pack")
842
- .description("Manage policy_packs[] entries (add / remove / list / reseed)");
843
- packCmd
844
- .command("add <name>")
845
- .description("Insert a new policy_packs entry. <name> must be a known builtin (see docs/policy-packs/).")
846
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
847
- .option("--mode <mode>", "pack-specific config.mode value (e.g. fast_confirm | grill_me | strict)")
848
- .option("--source <src>", "pack source (default: builtin)")
849
- .option("--description <text>", "operator-facing description")
850
- .option("--disabled", "register as enabled: false")
851
- .option("--dry-run", "print the unified diff and exit without writing")
852
- .action(async (name, options) => {
853
- const entry = { name };
854
- if (options.source !== undefined)
855
- entry.source = options.source;
856
- if (options.disabled === true)
857
- entry.enabled = false;
858
- if (options.description !== undefined)
859
- entry.description = options.description;
860
- if (options.mode !== undefined)
861
- entry.config = { mode: options.mode };
862
- const result = await packAdd(entry, {
863
- configPath: options.config,
864
- dryRun: options.dryRun,
865
- });
866
- if (options.dryRun) {
867
- stdout(result.diff);
868
- return;
869
- }
870
- stdout(`added policy_packs entry ${JSON.stringify(result.name)} to ${result.path}\n`);
871
- });
872
- packCmd
873
- .command("remove <name>")
874
- .description("Remove a policy_packs entry. Refuses without --force when applied state " +
875
- "is recorded in .last-apply.")
876
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
877
- .option("--dry-run", "print the unified diff and exit without writing")
878
- .option("--force", "remove the manifest entry AND clean up the on-disk pack files + .last-apply state")
879
- .action(async (name, options) => {
880
- const result = await packRemove(name, {
881
- configPath: options.config,
882
- dryRun: options.dryRun,
883
- force: options.force,
884
- });
885
- if (result.cleanedFiles.length > 0) {
886
- stderr(`(forced cleanup — removed ${result.cleanedFiles.length} pack file(s) and pruned .last-apply)\n`);
887
- }
888
- if (options.dryRun) {
889
- stdout(result.diff);
890
- return;
891
- }
892
- stdout(`removed policy_packs entry ${JSON.stringify(result.name)} from ${result.path}\n`);
893
- });
894
- // `harness pack reseed <name>` (task 68b9ad9c): pull the shipped
895
- // builtin template's config.ux (and config.producers) into an already-
896
- // installed manifest. Explicit-only, mirroring add/remove — never
897
- // invoked by `apply`, so an upgrade never silently rewrites an
898
- // operator's deliberate ux customisation. Paired with the `harness
899
- // doctor` divergence warning (src/policy-packs/ux-drift-check.ts).
900
- packCmd
901
- .command("reseed <name>")
902
- .description("Pull the shipped builtin template's config.ux (and config.producers) for <name> into " +
903
- "the manifest, preserving other config keys (mode, approval_lifecycle, ...). No-op if " +
904
- "already up to date. See `harness doctor` for a divergence warning.")
905
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
906
- .option("--dry-run", "print the unified diff and exit without writing")
907
- .action(async (name, options) => {
908
- const result = await packReseed(name, {
909
- configPath: options.config,
910
- dryRun: options.dryRun,
911
- });
912
- if (result.fieldsChanged.length === 0) {
913
- stdout(`policy_packs entry ${JSON.stringify(result.name)} already matches the shipped template; nothing to reseed.\n`);
914
- return;
915
- }
916
- if (options.dryRun) {
917
- stdout(result.diff);
918
- return;
919
- }
920
- stdout(`reseeded ${result.fieldsChanged.map((f) => `config.${f}`).join(", ")} for policy_packs entry ${JSON.stringify(result.name)} in ${result.path}\n`);
921
- });
922
- // `harness pack upgrade <name>` (task 8f637efd, D-004): text-level
923
- // insertion of a pack's missing default config block into an existing
924
- // manifest, idempotent, never rewrites content outside the inserted
925
- // block. Today's only wired upgrade is `understanding-before-
926
- // execution`'s `auto_approve` default; see upgrade.ts's module header
927
- // for why this is text-level rather than a Document-API mutation like
928
- // `add` / `reseed`.
929
- packCmd
930
- .command("upgrade <name>")
931
- .description("Insert a pack's missing default config block into an existing manifest (today: " +
932
- "understanding-before-execution's auto_approve default). Idempotent; " +
933
- "refuses on ambiguity rather than guessing where to insert.")
934
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
935
- .option("--dry-run", "print the unified diff and exit without writing")
936
- .action(async (name, options) => {
937
- const result = await packUpgrade(name, {
938
- configPath: options.config,
939
- dryRun: options.dryRun,
940
- });
941
- if (result.alreadyPresent) {
942
- stdout(`policy_packs entry ${JSON.stringify(result.name)} already carries this upgrade; nothing to insert (${result.path} unchanged).\n`);
943
- return;
944
- }
945
- if (options.dryRun) {
946
- stdout(result.diff);
947
- return;
948
- }
949
- stdout(`upgraded policy_packs entry ${JSON.stringify(result.name)} in ${result.path}\n`);
950
- });
951
- // `harness pack hook` runtime sub-tree (Phase 6 #4): wired by the
952
- // pack's PreToolUse hook contribution; reads PreToolUse JSON from
953
- // stdin, consults the signed approval marker (the persisted report and
954
- // the ledger are audit evidence only, task 7402301d), emits Claude Code
955
- // deny JSON on block.
956
- const packHookCmd = packCmd
957
- .command("hook")
958
- .description("Pack runtime hook entrypoints (called by Claude Code via settings.json)");
959
- packHookCmd
960
- .command("pre-tool-use")
961
- .description("PreToolUse blocker: read tool-event JSON from stdin, consult the signed approval marker (persisted report and ledger are audit evidence only), emit deny JSON on block")
962
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
963
- .option("--project <name>", "apply per-project overrides")
964
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
965
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
966
- .option("--reports-dir <path>", "override the persisted-report directory (default: ./.understanding-gate/reports)")
967
- .action(async (options) => {
968
- const cliOpts = {};
969
- if (options.config)
970
- cliOpts.configPath = options.config;
971
- if (options.project)
972
- cliOpts.project = options.project;
973
- if (options.pack)
974
- cliOpts.pack = options.pack;
975
- if (options.reportsDir)
976
- cliOpts.reportsDir = options.reportsDir;
977
- if (options.ledgerTimeout) {
978
- const n = Number.parseInt(options.ledgerTimeout, 10);
979
- if (Number.isFinite(n) && n > 0)
980
- cliOpts.ledgerTimeoutMs = n;
981
- }
982
- await runPackHookPreToolUseCli(cliOpts);
983
- });
984
- packHookCmd
985
- .command("post-tool-use")
986
- .description("PostToolUse marker-expiry: read tool-event JSON from stdin, delete the per-session approval marker when the just-completed tool matches config.approval_lifecycle.expire_on_tool_match (agent-tasks/d8ee60ca)")
987
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
988
- .option("--project <name>", "apply per-project overrides")
989
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
990
- .action(async (options) => {
991
- const cliOpts = {};
992
- if (options.config)
993
- cliOpts.configPath = options.config;
994
- if (options.project)
995
- cliOpts.project = options.project;
996
- if (options.pack)
997
- cliOpts.pack = options.pack;
998
- await runPackHookPostToolUseCli(cliOpts);
999
- });
1000
- packHookCmd
1001
- .command("track-active-claim")
1002
- .description("PostToolUse: read tool-event JSON from stdin, maintain ~/.claude/harness.generated/active-claim on agent-tasks task_start / task_finish / task_abandon / task_merge so `harness approve understanding` can auto-resolve the task id without --task (harness/494fd1e5). task_finish keeps the marker when the resulting status is review and clears it otherwise (task c86e3c4a); task_abandon / task_merge / a non-review task_finish clear it only when the marker already names the same task id, otherwise it is left in place (task c86e3c4a round 2).")
1003
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1004
- .option("--project <name>", "apply per-project overrides")
1005
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1006
- .action(async (options) => {
1007
- const cliOpts = {};
1008
- if (options.config)
1009
- cliOpts.configPath = options.config;
1010
- if (options.project)
1011
- cliOpts.project = options.project;
1012
- if (options.pack)
1013
- cliOpts.pack = options.pack;
1014
- await runPackHookTrackActiveClaimCli(cliOpts);
1015
- });
1016
- packHookCmd
1017
- .command("stay-in-scope")
1018
- .description("PostToolUse: read tool-event JSON from stdin and, when explicitly configured, emit a non-blocking reminder and JSONL audit row for configured task-tool payloads. Current manifest configuration controls matching; STAY_IN_SCOPE_DISABLED=1 disables a live hook and STAY_IN_SCOPE_LOG overrides its log path.")
1019
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1020
- .option("--project <name>", "apply per-project overrides")
1021
- .action(async (options) => {
1022
- const cliOpts = {};
1023
- if (options.config)
1024
- cliOpts.configPath = options.config;
1025
- if (options.project)
1026
- cliOpts.project = options.project;
1027
- await runPackHookStayInScopeCli(cliOpts);
1028
- });
1029
- packHookCmd
1030
- .command("subagent-start")
1031
- .description("SubagentStart: read event JSON from stdin, write a signed in-flight record for this (session_id, agent_id) when the parent session currently holds a valid understanding-gate approval (docs/decisions/2026-08-27-ug-auto-mode-approval.md).")
1032
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1033
- .option("--project <name>", "apply per-project overrides")
1034
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1035
- .action(async (options) => {
1036
- const cliOpts = {};
1037
- if (options.config)
1038
- cliOpts.configPath = options.config;
1039
- if (options.project)
1040
- cliOpts.project = options.project;
1041
- if (options.pack)
1042
- cliOpts.pack = options.pack;
1043
- await runPackHookSubagentStartCli(cliOpts);
1044
- });
1045
- packHookCmd
1046
- .command("subagent-stop")
1047
- .description("SubagentStop: read event JSON from stdin, remove the in-flight record written for this (session_id, agent_id) by subagent-start.")
1048
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1049
- .option("--project <name>", "apply per-project overrides")
1050
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1051
- .action(async (options) => {
1052
- const cliOpts = {};
1053
- if (options.config)
1054
- cliOpts.configPath = options.config;
1055
- if (options.project)
1056
- cliOpts.project = options.project;
1057
- if (options.pack)
1058
- cliOpts.pack = options.pack;
1059
- await runPackHookSubagentStopCli(cliOpts);
1060
- });
1061
- // Phase 6 #6 — Codex adapter sub-commands. Mirror the pre-tool-use
1062
- // shape; UserPromptSubmit emits developer-context instructions on stdout.
1063
- packHookCmd
1064
- .command("codex-pre-tool-use")
1065
- .description("Codex PreToolUse blocker: read tool-event JSON from stdin, consult the signed approval marker (persisted report and ledger are audit evidence only), exit 2 with stderr reason on block")
1066
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1067
- .option("--project <name>", "apply per-project overrides")
1068
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1069
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
1070
- .option("--reports-dir <path>", "override the persisted-report directory (default: ./.understanding-gate/reports)")
1071
- .action(async (options) => {
1072
- const cliOpts = {};
1073
- if (options.config)
1074
- cliOpts.configPath = options.config;
1075
- if (options.project)
1076
- cliOpts.project = options.project;
1077
- if (options.pack)
1078
- cliOpts.pack = options.pack;
1079
- if (options.reportsDir)
1080
- cliOpts.reportsDir = options.reportsDir;
1081
- if (options.ledgerTimeout) {
1082
- const n = Number.parseInt(options.ledgerTimeout, 10);
1083
- if (Number.isFinite(n) && n > 0)
1084
- cliOpts.ledgerTimeoutMs = n;
1085
- }
1086
- const result = await runPackHookCodexPreToolUseCli(cliOpts);
1087
- if (result.exitCode !== 0) {
1088
- throw new HarnessExitError("", result.exitCode);
1089
- }
1090
- });
1091
- packHookCmd
1092
- .command("codex-post-tool-use")
1093
- .description("Codex PostToolUse marker-expiry: read tool-event JSON from stdin, delete the per-session (and, where applicable, per-task) approval marker and expire the persisted report when the just-completed tool matches config.approval_lifecycle.expire_on_tool_match / expire_on_bash_match (task a1348c89, mirrors the Claude `post-tool-use` hook).")
1094
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1095
- .option("--project <name>", "apply per-project overrides")
1096
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1097
- .option("--reports-dir <path>", "override the persisted-report directory (default: ./.understanding-gate/reports)")
1098
- .action(async (options) => {
1099
- const cliOpts = {};
1100
- if (options.config)
1101
- cliOpts.configPath = options.config;
1102
- if (options.project)
1103
- cliOpts.project = options.project;
1104
- if (options.pack)
1105
- cliOpts.pack = options.pack;
1106
- if (options.reportsDir)
1107
- cliOpts.reportsDir = options.reportsDir;
1108
- await runPackHookCodexPostToolUseCli(cliOpts);
1109
- });
1110
- packHookCmd
1111
- .command("codex-user-prompt-submit")
1112
- .description("Codex UserPromptSubmit injector: emit developer-context instructions on stdout whenever Codex invokes the event")
1113
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1114
- .option("--project <name>", "apply per-project overrides")
1115
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1116
- .action(async (options) => {
1117
- const cliOpts = {};
1118
- if (options.config)
1119
- cliOpts.configPath = options.config;
1120
- if (options.project)
1121
- cliOpts.project = options.project;
1122
- if (options.pack)
1123
- cliOpts.pack = options.pack;
1124
- await runPackHookCodexUserPromptSubmitCli(cliOpts);
1125
- });
1126
- packHookCmd
1127
- .command("branch-protection")
1128
- .description("PreToolUse blocker for the branch-protection pack: read tool-event JSON from stdin, consult the " +
1129
- "evidence ledger, emit a deny envelope on protected branches unless either a fresh " +
1130
- "`branch:non-protected` tag (within 5m) or the operator-only override marker " +
1131
- "(written by `harness approve branch-protection`) is present.")
1132
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1133
- .option("--project <name>", "apply per-project overrides")
1134
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
1135
- .option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
1136
- .action(async (options) => {
1137
- const cliOpts = {};
1138
- if (options.config)
1139
- cliOpts.configPath = options.config;
1140
- if (options.project)
1141
- cliOpts.project = options.project;
1142
- if (options.cwd)
1143
- cliOpts.cwd = options.cwd;
1144
- if (options.ledgerTimeout) {
1145
- const n = Number.parseInt(options.ledgerTimeout, 10);
1146
- if (Number.isFinite(n) && n > 0)
1147
- cliOpts.ledgerTimeoutMs = n;
1148
- }
1149
- const result = await runPackHookBranchProtectionCli(cliOpts);
1150
- if (result.exitCode !== 0) {
1151
- throw new HarnessExitError("", result.exitCode);
1152
- }
1153
- });
1154
- packHookCmd
1155
- .command("solution-acceptance")
1156
- .description("PreToolUse completion-gate for the solution-acceptance pack: read tool-event JSON from stdin, " +
1157
- "and on a task-finishing tool (agent-tasks completion verb or `git push` / `gh pr merge`) emit a " +
1158
- "deny envelope unless a ready solution-acceptance verdict exists at the current git HEAD for the " +
1159
- "active-claim task. Fail-closed.")
1160
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1161
- .option("--project <name>", "apply per-project overrides")
1162
- .option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
1163
- .action(async (options) => {
1164
- const cliOpts = {};
1165
- if (options.config)
1166
- cliOpts.configPath = options.config;
1167
- if (options.project)
1168
- cliOpts.project = options.project;
1169
- if (options.cwd)
1170
- cliOpts.cwd = options.cwd;
1171
- const result = await runPackHookSolutionAcceptanceCli(cliOpts);
1172
- if (result.exitCode !== 0) {
1173
- throw new HarnessExitError("", result.exitCode);
1174
- }
1175
- });
1176
- packHookCmd
1177
- .command("solution-acceptance-writeguard")
1178
- .description("PreToolUse anti-forgery write-guard for the solution-acceptance pack: read tool-event JSON from " +
1179
- "stdin, emit a deny envelope on any agent write into the solution-verdict dir (path-tool file_path " +
1180
- "inside it, or a non-read-only Bash command that references it). The producer (grounding-mcp) is the " +
1181
- "only legitimate writer.")
1182
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1183
- .option("--project <name>", "apply per-project overrides")
1184
- .option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
1185
- .action(async (options) => {
1186
- const cliOpts = {};
1187
- if (options.config)
1188
- cliOpts.configPath = options.config;
1189
- if (options.project)
1190
- cliOpts.project = options.project;
1191
- if (options.cwd)
1192
- cliOpts.cwd = options.cwd;
1193
- const result = await runPackHookSolutionAcceptanceWriteguardCli(cliOpts);
1194
- if (result.exitCode !== 0) {
1195
- throw new HarnessExitError("", result.exitCode);
1196
- }
1197
- });
1198
- packHookCmd
1199
- .command("post-merge-gate-record")
1200
- .description("PostToolUse producer for the post-merge-gate pack: read tool-event JSON from stdin and, on a " +
1201
- "`gh pr merge` Bash call whose tool_output.exit_code is the number 0, record a " +
1202
- "`post-merge-gate:merged:<repo>:<branch>:<sha>` fact to the evidence ledger. blocking:false; every " +
1203
- "non-match / failure path is a no-op.")
1204
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1205
- .option("--project <name>", "apply per-project overrides")
1206
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
1207
- .option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
1208
- .action(async (options) => {
1209
- const cliOpts = {};
1210
- if (options.config)
1211
- cliOpts.configPath = options.config;
1212
- if (options.project)
1213
- cliOpts.project = options.project;
1214
- if (options.cwd)
1215
- cliOpts.cwd = options.cwd;
1216
- if (options.ledgerTimeout) {
1217
- const n = Number.parseInt(options.ledgerTimeout, 10);
1218
- if (Number.isFinite(n) && n > 0)
1219
- cliOpts.ledgerTimeoutMs = n;
1220
- }
1221
- await runPackHookPostMergeGateRecordCli(cliOpts);
1222
- });
1223
- packHookCmd
1224
- .command("post-merge-gate")
1225
- .description("PreToolUse blocker for the post-merge-gate pack: read tool-event JSON from stdin, and on a curated " +
1226
- "history-mutating Bash command (git commit/add/push/merge/rebase/cherry-pick/revert/reset/stash " +
1227
- "pop|apply, gh pr create/merge) emit a deny envelope when the current branch tip matches a recorded " +
1228
- "merged-tip fact. The recovery vocabulary (git switch/checkout/pull/fetch, git branch -d/-D, git stash " +
1229
- "list/show, any `harness ...` command) always passes as its own command, classified before any manifest " +
1230
- "or ledger access; chaining one of those with a curated mutation does NOT exempt the mutation. Fails " +
1231
- "open when the ledger is unreachable.")
1232
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1233
- .option("--project <name>", "apply per-project overrides")
1234
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
1235
- .option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
1236
- .action(async (options) => {
1237
- const cliOpts = {};
1238
- if (options.config)
1239
- cliOpts.configPath = options.config;
1240
- if (options.project)
1241
- cliOpts.project = options.project;
1242
- if (options.cwd)
1243
- cliOpts.cwd = options.cwd;
1244
- if (options.ledgerTimeout) {
1245
- const n = Number.parseInt(options.ledgerTimeout, 10);
1246
- if (Number.isFinite(n) && n > 0)
1247
- cliOpts.ledgerTimeoutMs = n;
1248
- }
1249
- const result = await runPackHookPostMergeGateCli(cliOpts);
1250
- if (result.exitCode !== 0) {
1251
- throw new HarnessExitError("", result.exitCode);
1252
- }
1253
- });
1254
- packHookCmd
1255
- .command("runtime-reality")
1256
- .description("PreToolUse drift gate: read tool-event JSON from stdin, run the operator-configured " +
1257
- "RUNTIME_REALITY_PROBE_CMD to capture actual runtime state, compare against the " +
1258
- "RUNTIME_REALITY_KEYWORD expectations file, and emit a deny envelope on critical drift. " +
1259
- "Env-driven (no manifest options); fail-open on any probe / load error.")
1260
- .action(async () => {
1261
- const result = await runPackHookRuntimeRealityCli();
1262
- if (result.exitCode !== 0) {
1263
- throw new HarnessExitError("", result.exitCode);
1264
- }
1265
- });
1266
- packHookCmd
1267
- .command("codex-stop")
1268
- .description("Codex Stop-equivalent: parse the agent's last message for an Understanding Report and persist it under .understanding-gate/reports/ as approvalStatus:pending. Failure modes resolve to exit 0 (capture must never block the agent's stop path).")
1269
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1270
- .option("--project <name>", "apply per-project overrides")
1271
- .option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
1272
- .option("--reports-dir <path>", "override the persisted-report directory (default: ./.understanding-gate/reports)")
1273
- .action(async (options) => {
1274
- const cliOpts = {};
1275
- if (options.config)
1276
- cliOpts.configPath = options.config;
1277
- if (options.project)
1278
- cliOpts.project = options.project;
1279
- if (options.pack)
1280
- cliOpts.pack = options.pack;
1281
- if (options.reportsDir)
1282
- cliOpts.reportsDir = options.reportsDir;
1283
- // Intentional: codex-stop's contract is fail-open. The runner
1284
- // returns exitCode 0 on every code path (including malformed
1285
- // input, missing session, parser misses); we deliberately do
1286
- // NOT throw HarnessExitError on a non-zero exit the way the
1287
- // codex-pre-tool-use sibling does. A future runner change
1288
- // that introduces a non-zero exit must also revisit this
1289
- // contract.
1290
- await runPackHookCodexStopCli(cliOpts);
1291
- });
1292
- packCmd
1293
- .command("list")
1294
- .description("Print policy_packs entries as a flat table or JSON.")
1295
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1296
- .option("--project <name>", "apply per-project overrides")
1297
- .option("--enabled-only", "skip entries with enabled: false")
1298
- .option("--json", "emit JSON array instead of an aligned text table")
1299
- .action((options) => {
1300
- const result = packList({
1301
- ...(options.config !== undefined ? { configPath: options.config } : {}),
1302
- ...(options.project !== undefined ? { project: options.project } : {}),
1303
- ...(options.enabledOnly === true ? { enabledOnly: true } : {}),
1304
- ...(options.json === true ? { json: true } : {}),
1305
- });
1306
- stdout(result.output);
1307
- });
1308
- // `harness approve` (Phase 6 #4): operator-driven approval verbs.
1309
- // Today only `understanding` is implemented; other packs can plug in
1310
- // sister sub-commands (e.g. `harness approve preflight`) without
1311
- // restructuring this surface.
1312
- const approveCmd = program
1313
- .command("approve")
1314
- .description("Operator-driven approval verbs (writes evidence-ledger tags + flips persisted artefacts)");
1315
- approveCmd
1316
- .command("understanding")
1317
- .description("Mark the latest Understanding Report as approved AND write the evidence-ledger tag. " +
1318
- "Writes the signed approval marker and round-trips the audit records (persisted report, ledger row) " +
1319
- "so harnessed and solo (@lannguyensi/understanding-gate) stacks stay in sync.")
1320
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1321
- .option("--project <name>", "apply per-project overrides")
1322
- .option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then $CODEX_SESSION_ID, then staged .pending-approval, then newest pending Understanding Report)")
1323
- .option("--task <ids...>", "agent-tasks task id(s): writes one task-scoped marker per id. Pass several (--task a b c, or --task a,b,c) to pre-approve a whole batch in a single operator action so a multi-task session does not re-prompt for each task it claims (outside approval_lifecycle mode: session the session approval covers only the task claimed when it was granted, and a done, abandoned or merged task expires it) (harness/1ee26e77, harness/0dce3880, harness 5018c0c4)")
1324
- .option("--reports-dir <path>", "override the persisted-report directory (default: ./.understanding-gate/reports)")
1325
- .option("--approved-by <actor>", "actor to record on the persisted report (default: harness-approve-cli)")
1326
- .option("--force", "bypass approve-time report validation (priorArt enforcement on grill_me reports). Writes the marker / ledger / report-flip anyway and stamps the ledger tag with `:forced:<field>` so audit can distinguish forced approvals from clean ones. Emergency-unblock path: default refuses the marker when a grill_me report fails validation. Also overrides the nesting-depth refusal (the marker is then signed with no content binding); never overrides the size refusal (also on the pretty-printed approval rewrite), an unreadable report, or one that is not a regular file.")
1327
- .action(async (options) => {
1328
- const cliOpts = {};
1329
- if (options.config)
1330
- cliOpts.configPath = options.config;
1331
- if (options.project)
1332
- cliOpts.project = options.project;
1333
- if (options.session)
1334
- cliOpts.session = options.session;
1335
- // Commander's variadic `<ids...>` yields a string[]; comma-split
1336
- // and de-dup happen inside approveUnderstanding (dedupeTaskIds).
1337
- if (options.task && options.task.length > 0)
1338
- cliOpts.tasks = options.task;
1339
- if (options.reportsDir)
1340
- cliOpts.reportsDir = options.reportsDir;
1341
- if (options.approvedBy)
1342
- cliOpts.approvedBy = options.approvedBy;
1343
- if (options.force)
1344
- cliOpts.force = true;
1345
- // Report capture (task 61fd36db): the agent attaches the
1346
- // Understanding Report as a quoted heredoc on stdin. A TTY stdin
1347
- // means an interactive operator shell with nothing piped — skip
1348
- // the read entirely so the CLI never sits waiting for input.
1349
- // Incomplete input (timeout, error, size cap) is refused rather
1350
- // than captured: a truncated-but-parseable report must never be
1351
- // persisted and approved as if it were whole.
1352
- let stdinIncomplete = false;
1353
- if (!process.stdin.isTTY) {
1354
- const piped = await readPipedStdin(process.stdin);
1355
- if (piped.text.trim().length > 0) {
1356
- if (piped.complete)
1357
- cliOpts.reportMarkdown = piped.text;
1358
- else
1359
- stdinIncomplete = true;
1360
- }
1361
- }
1362
- const result = await approveUnderstanding(cliOpts);
1363
- const lines = [];
1364
- // Annotate non-explicit session sources so the operator can spot
1365
- // a wrong id before it lands in the ledger.
1366
- const sourceNote = result.sessionSource === "pending-approval"
1367
- ? " (resolved from .pending-approval staged by the gate hook)"
1368
- : result.sessionSource === "env-claude-code"
1369
- ? " (from $CLAUDE_CODE_SESSION_ID)"
1370
- : result.sessionSource === "env-claude"
1371
- ? " (from $CLAUDE_SESSION_ID)"
1372
- : result.sessionSource === "env-codex"
1373
- ? " (from $CODEX_SESSION_ID)"
1374
- : result.sessionSource === "newest-report"
1375
- ? " (GUESSED from the newest pending Understanding Report)"
1376
- : "";
1377
- lines.push(`session: ${escapeForDisplay(result.sessionId)}${sourceNote}`);
1378
- if (result.sessionSource === "newest-report") {
1379
- // Tier-5 is a guess: no --session, no env var, no gate-staged
1380
- // .pending-approval. It is restricted to `pending` reports
1381
- // (harness/56f51f2b), but a stale session that left a never-
1382
- // approved report can still be picked. Name the report file
1383
- // so the operator can open it and confirm the id before
1384
- // trusting a marker that may approve the wrong session.
1385
- lines.push("");
1386
- lines.push("⚠ WARNING: the session id was GUESSED, not confirmed. There was no");
1387
- lines.push(" --session flag, no $CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID /");
1388
- lines.push(" $CODEX_SESSION_ID, and no");
1389
- lines.push(" gate-staged .pending-approval, so it was read from the newest pending");
1390
- lines.push(" Understanding Report:");
1391
- if (result.newestReportPath) {
1392
- lines.push(` ${escapeForDisplay(result.newestReportPath)}`);
1393
- }
1394
- lines.push(" If that is not your live session, the marker above approves the wrong");
1395
- lines.push(" session and the gate stays blocked. Confirm the id matches the running");
1396
- lines.push(" agent ($CLAUDE_CODE_SESSION_ID / $CLAUDE_SESSION_ID / $CODEX_SESSION_ID); if it differs, re-run");
1397
- lines.push(" with --session <live-id>.");
1398
- }
1399
- if (result.modeWarning) {
1400
- lines.push(`mode: ⚠ ${result.modeWarning}`);
1401
- }
1402
- if (result.marker.ok) {
1403
- lines.push(`marker: ✓ ${escapeForDisplay(result.marker.filePath)} (canonical gate signal)`);
1404
- }
1405
- else {
1406
- lines.push(`marker: ✗ FAILED (${escapeForDisplay(result.marker.reason)})`);
1407
- lines.push(" the gate WILL block the next tool call until the marker exists.");
1408
- }
1409
- for (const tm of result.taskMarkers) {
1410
- const sourceNote = tm.source === "active-claim"
1411
- ? " (auto-resolved from active-claim file)"
1412
- : "";
1413
- if (tm.ok) {
1414
- lines.push(`task: ✓ ${tm.filePath} (task-scoped, expires when this task ends)${sourceNote}`);
1415
- }
1416
- else {
1417
- lines.push(`task: ✗ FAILED for task ${tm.taskId}${sourceNote} (${tm.reason})`);
1418
- lines.push(" the session marker above is still in effect; the task-scoped path is degraded.");
1419
- }
1420
- }
1421
- if (result.taskMarkers.length > 1) {
1422
- const okCount = result.taskMarkers.filter((t) => t.ok).length;
1423
- lines.push(` (${okCount}/${result.taskMarkers.length} task markers written — the batch is pre-approved)`);
1424
- }
1425
- if (result.ledger.ok) {
1426
- lines.push(`ledger: ✓ wrote ${escapeForDisplay(result.ledger.tag)} (audit only)`);
1427
- }
1428
- else {
1429
- lines.push(`ledger: ⚠ skipped (${result.ledger.reason ?? "unknown"}) (audit only)`);
1430
- }
1431
- if (stdinIncomplete) {
1432
- lines.push("stdin: ⚠ piped input arrived incomplete (timeout, stream error, or size cap) — report NOT captured.");
1433
- lines.push(" the approval itself proceeds below; re-run with the report as a quoted heredoc to persist the audit trail.");
1434
- }
1435
- if (result.stdinReport) {
1436
- if (result.stdinReport.ok) {
1437
- lines.push(`stdin: ✓ report captured from stdin → ${result.stdinReport.filePath}`);
1438
- }
1439
- else {
1440
- lines.push(`stdin: ⚠ report on stdin NOT captured (${result.stdinReport.reason})`);
1441
- if (result.stdinReport.parseErrorLogPath) {
1442
- lines.push(` raw text + parser reasons kept at ${result.stdinReport.parseErrorLogPath}`);
1443
- }
1444
- // Task 5d73d78d review MEDIUM (fix-round-3): this line used to
1445
- // print unconditionally, but since HIGH-2 a rejected stdin
1446
- // submission with nothing else to fall back on REFUSES the
1447
- // approval (marker.ok === false) — "the approval itself
1448
- // proceeds below" was then simply false. It only still holds
1449
- // when a genuine earlier same-session report (the HIGH-2
1450
- // carve-out) governs approval instead, i.e. when the marker
1451
- // WAS written despite this submission being rejected.
1452
- if (result.marker.ok) {
1453
- lines.push(" the approval itself proceeds below; re-run with a schema-conform report to persist the audit trail.");
1454
- }
1455
- else {
1456
- lines.push(" the approval is REFUSED — no marker, no ledger tag, no report flip; see `validation` below for why.");
1457
- }
1458
- }
1459
- }
1460
- if (result.persistedReport.ok) {
1461
- const prev = result.persistedReport.previousStatus ?? "<missing>";
1462
- const stampNote = result.persistedReport.sessionIdStamped
1463
- ? "; stamped sessionId"
1464
- : "";
1465
- lines.push(`report: ✓ ${escapeForDisplay(result.persistedReport.filePath)} (approvalStatus: ${escapeForDisplay(prev)} → approved${stampNote})`);
1466
- const fb = result.persistedReport.fallbackAdopted;
1467
- if (fb) {
1468
- lines.push(` ⚠ adopted via sessionId-less fallback: created ${escapeForDisplay(fb.createdAt ?? "<unknown>")} (${fb.ageMinutes}m ago).`);
1469
- lines.push(" The live session's report was never persisted, or an older producer", " wrote it without a sessionId. Verify this is the report you just read", " before trusting the approval.");
1470
- }
1471
- }
1472
- else {
1473
- lines.push(`report: ⚠ skipped (${result.persistedReport.reason})`);
1474
- }
1475
- // Surface the validation outcome explicitly. Three cases:
1476
- // - ok: report was loaded and structurally compliant for its mode.
1477
- // - failure-enforced: short-circuited, no writes ran. Hard error.
1478
- // - failure-forced: writes ran, ledger tag carries `:forced:`.
1479
- // - skipped: no report loaded; nothing to validate.
1480
- const v = result.validation;
1481
- if ("ok" in v && v.ok) {
1482
- // mode === null is the legacy / pre-v0.4.0 case the validator
1483
- // waives by design (the schema bump must not retroactively
1484
- // reject historical reports). Distinguish it from a positive
1485
- // check so the operator does not read "passed" and assume the
1486
- // grill_me priorArt rule actually fired.
1487
- if (v.mode === null) {
1488
- lines.push("validation: ⓘ legacy report (no mode field) — priorArt rule waived");
1489
- }
1490
- else {
1491
- lines.push(`validation: ✓ ${escapeForDisplay(v.mode)} report passed structural checks`);
1492
- }
1493
- }
1494
- else if ("ok" in v && v.ok === false) {
1495
- if (v.enforced) {
1496
- lines.push(`validation: ✗ ${v.field} FAILED: ${v.reason}`);
1497
- lines.push(" the marker was NOT written; the gate stays closed.");
1498
- lines.push(" pass --force to bypass a content check (the ledger tag will be stamped `:forced:<field>` for audit); a report that is oversized, not a regular file or unreadable cannot be forced.");
1499
- }
1500
- else {
1501
- lines.push(`validation: ⚠ ${v.field} failed but --force bypassed (${v.reason})`);
1502
- lines.push(" the marker IS written; the ledger tag carries a `:forced:` suffix for audit.");
1503
- }
1504
- }
1505
- stdout(`${lines.join("\n")}\n`);
1506
- // Non-zero exit when validation refused the approval. Throwing
1507
- // HarnessExitError reaches the CLI entrypoint's catch-all and
1508
- // surfaces both the lines above and the chosen exit code so the
1509
- // operator sees the rejection and shell pipelines (`&&`,
1510
- // CI guards) can react.
1511
- if ("ok" in v && v.ok === false && v.enforced) {
1512
- throw new HarnessExitError(`approve refused: ${v.field} validation failed. ` +
1513
- `Run with --force to bypass a content check (the ledger tag will be stamped \`:forced:${v.field}\` for audit); a report that is oversized, not a regular file or unreadable cannot be forced.`, EX_FAIL);
1514
- }
1515
- });
1516
- approveCmd
1517
- .command("risk")
1518
- .description("Grant a Risk Gate require_approval decision (default), or deliberately override " +
1519
- "a deny-tier block with --force <reason>. The default path writes the " +
1520
- "risk-approved:${SESSION_ID} ledger tag the production-scoped require_approval " +
1521
- "policy's requires consults; --scope deletion writes " +
1522
- "risk-approved:deletion:${SESSION_ID} instead, for the dev-context deletion arm " +
1523
- "only; --force writes risk-override:${SESSION_ID}:forced:<reason> for the " +
1524
- "deny-tier policy. Operator action.")
1525
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1526
- .option("--project <name>", "apply per-project overrides")
1527
- .option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then $CODEX_SESSION_ID, then staged .pending-approval)")
1528
- .option("--scope <scope>", "restrict the ledger tag to one Risk Gate arm instead of the shared production tag; only 'deletion' is recognized (writes risk-approved:deletion:${SESSION_ID} for gate-dev-unsafe-deletion). Omit for the default production-scoped tag. Ignored with --force.")
1529
- .option("--force <reason>", "operator-deliberate override of a deny-tier Risk Gate block; writes the risk-override tag with the reason stamped into the audit trail")
1530
- .option("--i-am-the-operator", "acknowledge a scripted / non-TTY --force invocation (otherwise refused)")
1531
- .action(async (options) => {
1532
- const cliOpts = {};
1533
- if (options.config)
1534
- cliOpts.configPath = options.config;
1535
- if (options.project)
1536
- cliOpts.project = options.project;
1537
- if (options.session)
1538
- cliOpts.session = options.session;
1539
- if (options.scope !== undefined) {
1540
- if (options.scope !== "deletion") {
1541
- throw new HarnessExitError(`--scope "${options.scope}" is not recognized; the only supported value is "deletion".`, EX_USAGE);
1542
- }
1543
- cliOpts.scope = "deletion";
1544
- }
1545
- if (typeof options.force === "string") {
1546
- const reason = options.force.trim();
1547
- if (reason.length === 0) {
1548
- throw new HarnessExitError("--force requires a non-empty <reason>; the reason is recorded in the audit trail.", EX_USAGE);
1549
- }
1550
- cliOpts.force = { reason };
1551
- }
1552
- if (options.iAmTheOperator)
1553
- cliOpts.iAmTheOperator = true;
1554
- const result = await approveRisk(cliOpts);
1555
- const lines = [];
1556
- const sourceNote = result.sessionSource === "pending-approval"
1557
- ? " (resolved from .pending-approval staged by the gate hook)"
1558
- : result.sessionSource === "env-claude-code"
1559
- ? " (from $CLAUDE_CODE_SESSION_ID)"
1560
- : result.sessionSource === "env-claude"
1561
- ? " (from $CLAUDE_SESSION_ID)"
1562
- : result.sessionSource === "env-codex"
1563
- ? " (from $CODEX_SESSION_ID)"
1564
- : "";
1565
- lines.push(`session: ${result.sessionId}${sourceNote}`);
1566
- if (result.ledger.ok) {
1567
- lines.push(`ledger: ✓ wrote ${result.ledger.tag}`);
1568
- if (result.forced) {
1569
- lines.push(" deny-tier override recorded; the deny-tier Risk Gate policy now passes for this session.");
1570
- }
1571
- else {
1572
- lines.push(" the Risk Gate require_approval policy now passes for this session.");
1573
- }
1574
- }
1575
- else {
1576
- lines.push(`ledger: ✗ FAILED (${result.ledger.reason ?? "unknown"})`);
1577
- if (result.forced) {
1578
- lines.push(" the deny-tier override stays unrecorded; the gate keeps blocking.");
1579
- }
1580
- else {
1581
- lines.push(" the require_approval gate stays blocked until the tag is recorded.");
1582
- }
1583
- }
1584
- stdout(`${lines.join("\n")}\n`);
1585
- });
1586
- approveCmd
1587
- .command("branch-protection")
1588
- .description("Bless a deliberate protected-branch edit for one session. Writes the canonical " +
1589
- "operator-only approval marker under harness.generated/.approvals/ that the " +
1590
- "branch-protection blocker consults, plus a best-effort branch-protection-ack " +
1591
- "ledger row for audit. Operator action: the marker (not the ledger tag) is the " +
1592
- "trusted override, because the ledger is agent-writable.")
1593
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1594
- .option("--project <name>", "apply per-project overrides")
1595
- .option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then $CODEX_SESSION_ID, then staged .pending-approval)")
1596
- .option("--reason <text>", "free-form note recorded in the audit ledger tag (why the override fired)")
1597
- .option("--approved-by <actor>", "actor to record on the marker (default: harness-approve-cli)")
1598
- .action(async (options) => {
1599
- const cliOpts = {};
1600
- if (options.config)
1601
- cliOpts.configPath = options.config;
1602
- if (options.project)
1603
- cliOpts.project = options.project;
1604
- if (options.session)
1605
- cliOpts.session = options.session;
1606
- if (options.reason)
1607
- cliOpts.reason = options.reason;
1608
- if (options.approvedBy)
1609
- cliOpts.approvedBy = options.approvedBy;
1610
- const result = await approveBranchProtection(cliOpts);
1611
- const lines = [];
1612
- const sourceNote = result.sessionSource === "pending-approval"
1613
- ? " (resolved from .pending-approval staged by the gate hook)"
1614
- : result.sessionSource === "env-claude-code"
1615
- ? " (from $CLAUDE_CODE_SESSION_ID)"
1616
- : result.sessionSource === "env-claude"
1617
- ? " (from $CLAUDE_SESSION_ID)"
1618
- : result.sessionSource === "env-codex"
1619
- ? " (from $CODEX_SESSION_ID)"
1620
- : "";
1621
- lines.push(`session: ${result.sessionId}${sourceNote}`);
1622
- if (result.marker.ok) {
1623
- lines.push(`marker: ✓ ${result.marker.filePath} (canonical gate signal)`);
1624
- lines.push(" the branch-protection gate now allows protected-branch edits for this session.");
1625
- }
1626
- else {
1627
- lines.push(`marker: ✗ FAILED (${result.marker.reason})`);
1628
- lines.push(" the gate WILL keep blocking the next tool call until the marker exists.");
1629
- }
1630
- if (result.ledger.ok) {
1631
- lines.push(`ledger: ✓ wrote ${result.ledger.tag} (audit only)`);
1632
- }
1633
- else {
1634
- lines.push(`ledger: ⚠ skipped (${result.ledger.reason ?? "unknown"}) (audit only)`);
1635
- }
1636
- stdout(`${lines.join("\n")}\n`);
1637
- });
1638
- // `harness delegate` (Slice 3 of docs/decisions/2026-08-27-ug-auto-mode-approval.md,
1639
- // agent-tasks 37ad0b05): issue a signed pre-authorization for a
1640
- // headless `claude -p` child session, bound to an already-approved
1641
- // PARENT session. NOT an approval: the child still writes and gets its
1642
- // own Understanding Report checked by its own PreToolUse hook before
1643
- // its auto-marker is minted. Writes harness.generated/.delegations/,
1644
- // never .approvals/.
1645
- program
1646
- .command("delegate")
1647
- .description("Issue a signed delegation for a headless `claude -p` child session, bound to " +
1648
- "an already-approved parent session (pre-authorization, not an approval: the " +
1649
- "child still writes and gets its own Understanding Report checked). Writes " +
1650
- "harness.generated/.delegations/<child-sid>, never .approvals/. Consumption is " +
1651
- "Claude Code only: the Codex adapter has no delegation consumer, so a Codex " +
1652
- "session cannot be the delegated child. A Codex session CAN be the delegating " +
1653
- "parent today (parent resolution reads $CODEX_SESSION_ID and accepts any " +
1654
- "validly signed marker). See docs/decisions/2026-08-27-ug-auto-mode-approval.md.")
1655
- .requiredOption("--child-session <uuid>", "session id of the claude -p child this delegation authorizes (UUID)")
1656
- .option("--cwd <path>", "bind the delegation to this working directory; at least one of --cwd/--task is required")
1657
- .option("--task <id>", "bind the delegation to this agent-tasks task id; at least one of --cwd/--task is required")
1658
- .option("--ttl <duration>", "delegation lifetime (e.g. 30m, 1h); default: the pack's approval_lifecycle.max_age when set, else 1h")
1659
- .option("--report <path>", "fallback shape: copy the child's Understanding Report to harness.generated/.delegation-reports/<child-sid>.md and bind it by content + that conventional path's hash at spawn time; the child's PreToolUse hook reads it back from there, see the pack doc")
1660
- .option("--session-id <id>", "explicit parent session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then $CODEX_SESSION_ID, then staged .pending-approval)")
1661
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1662
- .option("--project <name>", "apply per-project overrides")
1663
- .action(async (options) => {
1664
- // Usage-level check ahead of every deeper resolution, with the
1665
- // exact message the ADR slice 3 acceptance criterion pins.
1666
- // `issueDelegation` carries the identical check (reason
1667
- // "no-binding") for callers that invoke it directly (the smoke
1668
- // runner, tests) without going through commander's flag parsing.
1669
- if (options.cwd === undefined && options.task === undefined) {
1670
- throw new HarnessExitError("a delegation must bind a cwd or a task", EX_FAIL);
1671
- }
1672
- let ttlSeconds;
1673
- if (options.ttl) {
1674
- try {
1675
- ttlSeconds = parseDurationSeconds(options.ttl);
1676
- }
1677
- catch (err) {
1678
- if (err instanceof InvalidDurationError) {
1679
- throw new HarnessExitError(`--ttl: ${err.message}`, EX_USAGE);
1680
- }
1681
- throw err;
1682
- }
1683
- }
1684
- const cliOpts = {
1685
- childSessionId: options.childSession,
1686
- };
1687
- // `!== undefined` (not truthy), agreeing with the usage check
1688
- // above: a truthy check would silently drop an explicit `--cwd
1689
- // ''` / `--task ''` instead of letting `issueDelegation` refuse
1690
- // it with `invalid-cwd` / `invalid-task` (L3 fix, agent-tasks
1691
- // 37ad0b05).
1692
- if (options.cwd !== undefined)
1693
- cliOpts.cwd = options.cwd;
1694
- if (options.task !== undefined)
1695
- cliOpts.taskId = options.task;
1696
- if (ttlSeconds !== undefined)
1697
- cliOpts.ttlSeconds = ttlSeconds;
1698
- if (options.report)
1699
- cliOpts.reportPath = options.report;
1700
- if (options.sessionId)
1701
- cliOpts.parentSessionId = options.sessionId;
1702
- if (options.config)
1703
- cliOpts.configPath = options.config;
1704
- if (options.project)
1705
- cliOpts.project = options.project;
1706
- const result = await issueDelegation(cliOpts);
1707
- if (!result.ok) {
1708
- throw new HarnessExitError(`delegate refused (${result.reason}): ${result.detail}`, EX_FAIL);
1709
- }
1710
- const lines = [
1711
- `delegation: ✓ ${result.filePath} (child ${result.childSessionId}, parent ${result.parentSessionId}, expires ${result.expiresAt})`,
1712
- ];
1713
- if (result.ledgerFact.written) {
1714
- lines.push(`ledger: ✓ wrote understanding-delegated:${result.childSessionId}:${result.parentSessionId} (audit only)`);
1715
- }
1716
- else {
1717
- lines.push(`ledger: ⚠ skipped (${result.ledgerFact.reason ?? "unknown"}) (audit only)`);
1718
- }
1719
- stdout(`${lines.join("\n")}\n`);
1720
- });
1721
- const VALID_DECISION_FILTERS = [
1722
- "allow",
1723
- "warn",
1724
- "require_approval",
1725
- "deny",
1726
- "warn-degraded",
1727
- "deny-degraded",
1728
- ];
1729
- const isDecisionFilter = (v) => VALID_DECISION_FILTERS.includes(v);
1730
- program
1731
- .command("explain [policy]")
1732
- .description("Print a policy's definition; --trace reads the last recorded evaluation; --last traces the most recent decision in the ledger")
1733
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1734
- .option("--project <name>", "apply per-project overrides")
1735
- .option("--json", "emit JSON instead of YAML")
1736
- .option("--trace", "include the full decision trail from the most recent evaluation")
1737
- .option("--last", "trace the most recent policy decision in the ledger (any policy); mutually exclusive with <policy>")
1738
- .option("--decision <outcome>", `with --last, restrict to decisions of this outcome (${VALID_DECISION_FILTERS.join(" / ")})`)
1739
- .option("--session <id>", "grounding session whose audit log to read (default: $CLAUDE_SESSION_ID, then 'default')")
1740
- .action(async (policyName, options) => {
1741
- if (options.last && policyName !== undefined) {
1742
- throw new HarnessExitError("explain: <policy> and --last are mutually exclusive", EX_USAGE);
1743
- }
1744
- if (options.decision !== undefined && !options.last) {
1745
- throw new HarnessExitError("explain: --decision requires --last", EX_USAGE);
1746
- }
1747
- if (options.decision !== undefined && !isDecisionFilter(options.decision)) {
1748
- throw new HarnessExitError(`explain: --decision must be one of ${VALID_DECISION_FILTERS.join(", ")} (got "${options.decision}")`, EX_USAGE);
1749
- }
1750
- const explainOpts = {};
1751
- if (options.config)
1752
- explainOpts.configPath = options.config;
1753
- if (options.project)
1754
- explainOpts.project = options.project;
1755
- if (options.json)
1756
- explainOpts.json = options.json;
1757
- if (options.trace)
1758
- explainOpts.trace = options.trace;
1759
- if (options.last)
1760
- explainOpts.last = options.last;
1761
- if (options.decision !== undefined && isDecisionFilter(options.decision)) {
1762
- explainOpts.decisionFilter = options.decision;
1763
- }
1764
- if (options.session)
1765
- explainOpts.sessionId = options.session;
1766
- const result = await explain(policyName, explainOpts);
1767
- stdout(result.output);
1768
- });
1769
- program
1770
- .command("explain-action <event.json>")
1771
- .description("Risk Gate debug verb (Phase 7): read a tool-event JSON file (the Claude Code PreToolUse hook payload shape: " +
1772
- "{ hook_event_name, tool_name, tool_input, session_id, cwd }) and print the normalized Action Envelope. " +
1773
- "Inspection surface for the envelope that downstream Risk Gate stages consume; does not evaluate policies.")
1774
- .option("--json", "emit the envelope as JSON instead of YAML")
1775
- .action((eventPath, options) => {
1776
- const result = explainAction({
1777
- eventPath,
1778
- ...(options.json === true && { json: true }),
1779
- });
1780
- stdout(result.output);
1781
- if (!result.output.endsWith("\n"))
1782
- stdout("\n");
1783
- });
1784
- program
1785
- .command("test-risk <event.json>")
1786
- .description("Risk Gate debug verb (Phase 7): read a tool-event JSON file, build its Action Envelope, and classify it " +
1787
- "against the manifest's risk.classifiers[]. Prints the risk profile (severity, categories, reversibility, " +
1788
- "confidence, reasons). An action no pattern matches reports as unclassified, not as safe.")
1789
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1790
- .option("--project <name>", "apply per-project overrides")
1791
- .option("--json", "emit the risk profile as JSON instead of YAML")
1792
- .action((eventPath, options) => {
1793
- const result = testRisk({
1794
- eventPath,
1795
- ...(options.config !== undefined && { configPath: options.config }),
1796
- ...(options.project !== undefined && { project: options.project }),
1797
- ...(options.json === true && { json: true }),
1798
- });
1799
- stdout(result.output);
1800
- if (!result.output.endsWith("\n"))
1801
- stdout("\n");
1802
- });
1803
- program
1804
- .command("resolve-env <event.json>")
1805
- .description("Risk Gate debug verb (Phase 7): read a tool-event JSON file, build its Action Envelope, and resolve its " +
1806
- "target environment against the manifest's environments.resolvers[] (branch / env-var / kube-context / " +
1807
- "kube-namespace signals). An action no resolver matches resolves to `unknown`, not to a safe default.")
1808
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1809
- .option("--project <name>", "apply per-project overrides")
1810
- .option("--json", "emit the environment resolution as JSON instead of YAML")
1811
- .action((eventPath, options) => {
1812
- const result = resolveEnv({
1813
- eventPath,
1814
- ...(options.config !== undefined && { configPath: options.config }),
1815
- ...(options.project !== undefined && { project: options.project }),
1816
- ...(options.json === true && { json: true }),
1817
- });
1818
- stdout(result.output);
1819
- if (!result.output.endsWith("\n"))
1820
- stdout("\n");
1821
- });
1822
- program
1823
- .command("explain-policy <policy>")
1824
- .description("Risk Gate debug verb (Phase 7): explain whether <policy> would APPLY to a tool event. " +
1825
- "Reads the event from --event, builds and enriches the Action Envelope, and shows the " +
1826
- "trigger match, the risk classification, the resolved environment, and a per-clause " +
1827
- "`when:` breakdown. Evaluates a hypothetical event live and reads nothing from the " +
1828
- "ledger (use `harness explain <policy> --trace` for the last recorded decision).")
1829
- .requiredOption("--event <event.json>", "path to the tool-event JSON file")
1830
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1831
- .option("--project <name>", "apply per-project overrides")
1832
- .option("--json", "emit the explanation as JSON instead of YAML")
1833
- .action((policyName, options) => {
1834
- const result = explainPolicy(policyName, {
1835
- eventPath: options.event,
1836
- ...(options.config !== undefined && { configPath: options.config }),
1837
- ...(options.project !== undefined && { project: options.project }),
1838
- ...(options.json === true && { json: true }),
1839
- });
1840
- stdout(result.output);
1841
- if (!result.output.endsWith("\n"))
1842
- stdout("\n");
1843
- });
1844
- program
1845
- .command("audit")
1846
- .description("Replay policy decisions from the evidence ledger for a time window, plus an approvals section listing raw understanding-gate approval facts")
1847
- .option("--since <duration>", "time window (default: 24h)")
1848
- .option("--policy <name>", "filter to a single policy by name")
1849
- .option("--outcome <outcome>", "filter by decision outcome (allow / warn / require_approval / deny / warn-degraded / deny-degraded)")
1850
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1851
- .option("--project <name>", "apply per-project overrides")
1852
- .option("--session <id>", "grounding session whose audit log to read (default: $CLAUDE_SESSION_ID, then 'default')")
1853
- .option("--json", "emit JSON instead of a table")
1854
- .action(async (options) => {
1855
- const auditOpts = {};
1856
- if (options.since)
1857
- auditOpts.since = options.since;
1858
- if (options.policy)
1859
- auditOpts.policy = options.policy;
1860
- if (options.outcome)
1861
- auditOpts.outcome = options.outcome;
1862
- if (options.config)
1863
- auditOpts.configPath = options.config;
1864
- if (options.project)
1865
- auditOpts.project = options.project;
1866
- if (options.session)
1867
- auditOpts.sessionId = options.session;
1868
- if (options.json)
1869
- auditOpts.json = options.json;
1870
- const result = await audit(auditOpts);
1871
- stdout(result.output);
1872
- });
1873
- program
1874
- .command("session-export [sessionId]")
1875
- .description("Export a chronological audit artifact joining the on-disk transcript JSONL and the evidence ledger for a session")
1876
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1877
- .option("--project <name>", "apply per-project overrides")
1878
- .option("--format <fmt>", "output format: json (default) or jsonl", "json")
1879
- .option("-o, --out <file>", "write the export to <file> instead of stdout")
1880
- .action(async (sessionIdArg, options) => {
1881
- const fmt = options.format ?? "json";
1882
- if (fmt !== "json" && fmt !== "jsonl") {
1883
- throw new HarnessExitError(`unknown --format "${fmt}"; expected json or jsonl`, EX_USAGE);
1884
- }
1885
- const exportOpts = {
1886
- format: fmt,
1887
- };
1888
- if (sessionIdArg)
1889
- exportOpts.sessionId = sessionIdArg;
1890
- if (options.config)
1891
- exportOpts.configPath = options.config;
1892
- if (options.project)
1893
- exportOpts.project = options.project;
1894
- if (options.out)
1895
- exportOpts.outFile = options.out;
1896
- const result = await sessionExport(exportOpts);
1897
- if (!options.out) {
1898
- stdout(result.output);
1899
- }
1900
- else {
1901
- stdout(`session-export wrote ${result.events.length} events to ${options.out}\n`);
1902
- }
1903
- });
1904
- program
1905
- .command("dry-run <prompt>")
1906
- .description("Statically predict which hooks fire / policies match / memories route for a prompt")
1907
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1908
- .option("--project <name>", "apply per-project overrides")
1909
- .option("--tool <name>", "simulate a PreToolUse event for this tool name")
1910
- .option("--tool-args <json>", "JSON for tool_input (default: {})")
1911
- .option("--json", "emit JSON instead of YAML")
1912
- .action((prompt, options) => {
1913
- const dryRunOpts = {};
1914
- if (options.config)
1915
- dryRunOpts.configPath = options.config;
1916
- if (options.project)
1917
- dryRunOpts.project = options.project;
1918
- if (options.tool)
1919
- dryRunOpts.tool = options.tool;
1920
- if (options.toolArgs)
1921
- dryRunOpts.toolArgs = options.toolArgs;
1922
- if (options.json)
1923
- dryRunOpts.json = options.json;
1924
- const result = dryRun(prompt, dryRunOpts);
1925
- stdout(result.output);
1926
- });
1927
- program
1928
- .command("smoke")
1929
- .description("Drive `claude -p` end-to-end against the apply'd manifest and assert per --expect-* flags. " +
1930
- "Writes stream.jsonl + stderr.log + settings.json under --output-dir; exits 1 on any expectation miss. " +
1931
- "Replaces the hand-rolled dogfood recipes under dogfood/phase5/.")
1932
- .requiredOption("--prompt <text>", "Prompt fed to claude -p")
1933
- .requiredOption("--output-dir <path>", "Directory for stream.jsonl + stderr.log + settings.json")
1934
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
1935
- .option("--project <name>", "apply per-project overrides")
1936
- .option("--session-id <id>", "session id (default: fresh uuid)")
1937
- .option("--claude-bin <path>", "claude binary (default: $CLAUDE_BIN, then 'claude' on PATH)")
1938
- .option("--timeout-ms <n>", "wall-clock budget in milliseconds (default: 60000)")
1939
- .option("--expect-hook <names>", "comma-separated list of hook names / events that MUST fire (repeatable)", (value, prev = []) => prev.concat(splitCommaList(value)), [])
1940
- .option("--expect-no-hook <names>", "comma-separated list of hook names / events that MUST NOT fire (repeatable)", (value, prev = []) => prev.concat(splitCommaList(value)), [])
1941
- .option("--expect-exit <n>", "expected result.is_error: 0 ⇒ false, !=0 ⇒ true")
1942
- .option("--expect-decision <kind>", "policy decision must be one of allow|deny|warn")
1943
- .option("--no-delegate", "do not pre-authorize the spawned child (docs/decisions/2026-08-27-ug-auto-mode-approval.md); " +
1944
- "it then has no delegation binding it to this approved session and falls back to the plain opt-in path")
1945
- .action(async (options) => {
1946
- const expectations = {};
1947
- if (options.expectHook && options.expectHook.length > 0) {
1948
- expectations.expectHooks = options.expectHook;
1949
- }
1950
- if (options.expectNoHook && options.expectNoHook.length > 0) {
1951
- expectations.expectNoHooks = options.expectNoHook;
1952
- }
1953
- if (options.expectExit !== undefined) {
1954
- const n = Number.parseInt(options.expectExit, 10);
1955
- if (!Number.isFinite(n)) {
1956
- throw new HarnessExitError(`harness smoke: --expect-exit must be an integer (got "${options.expectExit}")`, EX_USAGE);
1957
- }
1958
- expectations.expectExit = n;
1959
- }
1960
- if (options.expectDecision !== undefined) {
1961
- expectations.expectDecision = options.expectDecision;
1962
- }
1963
- const smokeOpts = {
1964
- prompt: options.prompt,
1965
- outputDir: options.outputDir,
1966
- expectations,
1967
- };
1968
- // commander's `--no-delegate` negates `options.delegate` (default
1969
- // true); only an explicit `--no-delegate` flips it to `false`.
1970
- if (options.delegate === false)
1971
- smokeOpts.noDelegate = true;
1972
- if (options.config)
1973
- smokeOpts.configPath = options.config;
1974
- if (options.project)
1975
- smokeOpts.project = options.project;
1976
- if (options.sessionId)
1977
- smokeOpts.sessionId = options.sessionId;
1978
- if (options.claudeBin)
1979
- smokeOpts.claudeBin = options.claudeBin;
1980
- if (options.timeoutMs !== undefined) {
1981
- const n = Number.parseInt(options.timeoutMs, 10);
1982
- if (!Number.isFinite(n) || n <= 0) {
1983
- throw new HarnessExitError(`harness smoke: --timeout-ms must be a positive integer`, EX_USAGE);
1984
- }
1985
- smokeOpts.timeoutMs = n;
1986
- }
1987
- const result = await runSmoke(smokeOpts);
1988
- stdout(formatSmokeReport(result));
1989
- if (result.exitCode !== 0) {
1990
- throw new HarnessExitError("", result.exitCode);
1991
- }
1992
- });
1993
- // The action shared by `harness session-start preflight` and its
1994
- // top-level alias `harness preflight`: the alias delegates to the same
1995
- // implementation, with the same CLI options.
1996
- const preflightAction = async (options) => {
1997
- const cliOpts = {};
1998
- applyCliOptions(options, cliOpts);
1999
- if (options.timeout) {
2000
- const n = Number.parseInt(options.timeout, 10);
2001
- if (Number.isFinite(n) && n > 0)
2002
- cliOpts.preflightTimeoutMs = n;
2003
- }
2004
- // Opt into the bootstrap-staging side effect from the CLI entry
2005
- // point only. Library callers (vitest cases) get the no-op default
2006
- // so they cannot clobber the operator's real pending-approval file.
2007
- cliOpts.stagePendingApproval = writePendingApproval;
2008
- await runSessionStartPreflight(cliOpts);
2009
- };
2010
- // Top-level alias for `harness session-start preflight`, so the
2011
- // policy `ux.run:` field can show the short form the agent should
2012
- // type: `Run: harness preflight`.
2013
- addLedgerTimeoutOption(addIdentityOptions(program
2014
- .command("preflight")
2015
- .description("Alias for `harness session-start preflight`: run agent-preflight against the session cwd " +
2016
- "and, on a ready:true result, record a `preflight:${REPO}` fact to the evidence ledger. " +
2017
- "Opt-in `session_start_preflight.setup: true` (default off) passes --setup through; " +
2018
- "see docs/CLI.md for the trust and scope caveats."), "explicit session id (overrides stdin event + env)").option("--timeout <ms>", "agent-preflight subprocess timeout in milliseconds (default 60000)")).action(preflightAction);
2019
- // Shared by the three `record` verbs' action handlers below: parse
2020
- // `--ledger-timeout <ms>` into `cliOpts.ledgerTimeoutMs`, and report a
2021
- // `RecordResult` (print the recorded fact on success; on failure, throw
2022
- // with the runner's own exit code and an EMPTY message, since the
2023
- // runner already wrote the reason to stderr itself — throwing the same
2024
- // text again would double-print it).
2025
- //
2026
- // An unparseable / non-positive value used to fall back to the
2027
- // default silently (review finding, T-004): an operator who typo'd
2028
- // `--ledger-timeout 5ooo` got the default timeout with zero
2029
- // diagnostic. Now a malformed value warns once on stderr before
2030
- // falling back, same "never a silent gap" convention `resolveBase`'s
2031
- // own degrade path already uses below.
2032
- const applyLedgerTimeout = (raw, cliOpts) => {
2033
- if (!raw)
2034
- return;
2035
- const n = Number.parseInt(raw, 10);
2036
- if (Number.isFinite(n) && n > 0) {
2037
- cliOpts.ledgerTimeoutMs = n;
2038
- return;
2039
- }
2040
- stderr(`harness record: --ledger-timeout ${JSON.stringify(raw)} is not a positive integer; using the default timeout.\n`);
2041
- };
2042
- const reportRecordResult = (result) => {
2043
- if (result.wrote) {
2044
- stdout(`recorded ${result.content} for session ${result.sessionId}\n`);
2045
- }
2046
- if (result.exitCode !== 0) {
2047
- throw new HarnessExitError("", result.exitCode);
2048
- }
2049
- };
2050
- // `harness record {review,review-subagent,dogfood}` (task T-001):
2051
- // evidence-ledger producers for the review-before-merge,
2052
- // review-subagent-before-pr-create, and dogfood-before-release gate
2053
- // families (see src/cli/init/templates.ts for the exact policies).
2054
- // Unlike the `preflight` / `session-start preflight` pair above,
2055
- // these are NOT hooks: they are invoked deliberately by an agent or
2056
- // operator, so a failure exits non-zero with a clear stderr message
2057
- // (written by the runner itself) rather than degrading silently.
2058
- const recordCmd = program
2059
- .command("record")
2060
- .description("Evidence-ledger producers for the review / review-subagent / dogfood gate families. " +
2061
- "Interactive verbs (not hooks): a failure exits non-zero.");
2062
- recordCmd
2063
- .command("review <summary>")
2064
- .description("Record a review:${PR_NUMBER} + review:${BRANCH} (+ review:${BASE}, + review:${TASK_ID} " +
2065
- "with --task) fact for the review-before-merge / review-before-merge-bash / " +
2066
- "review-before-task-merge / review-before-task-finish-automerge gates.")
2067
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2068
- .option("--project <name>", "apply per-project overrides")
2069
- .requiredOption("--pr <number>", "PR number the review:${PR_NUMBER} tag is namespaced by")
2070
- .option("--base <branch>", "base branch for the review:${BASE} tag. Default: the remote's default branch read from " +
2071
- "refs/remotes/origin/HEAD (packed-refs fallback included); omitted with a stderr warning " +
2072
- "when neither resolves. No `gh` shell-out.")
2073
- .option("--branch <name>", "explicit branch override for review:${BRANCH} (default: current git branch)")
2074
- .option("--task <id>", "agent-tasks task id for the review:${TASK_ID} tag the task_merge / task_finish(autoMerge) gates read. " +
2075
- "Optional; pass it and one recorded review satisfies all four merge surfaces.")
2076
- .option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then newest Claude Code transcript)")
2077
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
2078
- .action(async (summary, options) => {
2079
- const cliOpts = { pr: options.pr, summary };
2080
- if (options.config)
2081
- cliOpts.configPath = options.config;
2082
- if (options.project)
2083
- cliOpts.project = options.project;
2084
- if (options.base)
2085
- cliOpts.base = options.base;
2086
- if (options.branch)
2087
- cliOpts.branch = options.branch;
2088
- if (options.task !== undefined)
2089
- cliOpts.task = options.task;
2090
- if (options.session)
2091
- cliOpts.session = options.session;
2092
- applyLedgerTimeout(options.ledgerTimeout, cliOpts);
2093
- reportRecordResult(await runRecordReview(cliOpts));
2094
- });
2095
- recordCmd
2096
- .command("review-subagent [summary]")
2097
- .description("Record a review-subagent:${TASK_ID} + review-subagent:${BRANCH} fact for the " +
2098
- "review-subagent-before-pr-create / review-subagent-before-pr-create-bash gates. " +
2099
- "Exactly one of --task or --adhoc is required; --adhoc (work without an agent-tasks " +
2100
- "task) writes review-subagent:${BRANCH} only.")
2101
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2102
- .option("--project <name>", "apply per-project overrides")
2103
- .option("--task <id>", "agent-tasks task id the review-subagent:${TASK_ID} tag is namespaced by (exactly one of --task or --adhoc)")
2104
- .option("--adhoc", "record for work without an agent-tasks task: writes review-subagent:${BRANCH} only, no task tag (exactly one of --task or --adhoc)")
2105
- .requiredOption("--verdict <text>", "reviewer verdict recorded in the fact content")
2106
- .option("--branch <name>", "explicit branch override for review-subagent:${BRANCH} (default: current git branch)")
2107
- .option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then newest Claude Code transcript)")
2108
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
2109
- .action(async (summary, options) => {
2110
- const cliOpts = {
2111
- verdict: options.verdict,
2112
- };
2113
- if (options.task !== undefined)
2114
- cliOpts.task = options.task;
2115
- if (options.adhoc)
2116
- cliOpts.adhoc = true;
2117
- if (options.config)
2118
- cliOpts.configPath = options.config;
2119
- if (options.project)
2120
- cliOpts.project = options.project;
2121
- if (options.branch)
2122
- cliOpts.branch = options.branch;
2123
- if (options.session)
2124
- cliOpts.session = options.session;
2125
- if (summary)
2126
- cliOpts.summary = summary;
2127
- applyLedgerTimeout(options.ledgerTimeout, cliOpts);
2128
- reportRecordResult(await runRecordReviewSubagent(cliOpts));
2129
- });
2130
- recordCmd
2131
- .command("dogfood <summary>")
2132
- .description("Record a dogfood:${SESSION_ID} fact for the dogfood-before-release gate.")
2133
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2134
- .option("--project <name>", "apply per-project overrides")
2135
- .option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then newest Claude Code transcript)")
2136
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
2137
- .action(async (summary, options) => {
2138
- const cliOpts = { summary };
2139
- if (options.config)
2140
- cliOpts.configPath = options.config;
2141
- if (options.project)
2142
- cliOpts.project = options.project;
2143
- if (options.session)
2144
- cliOpts.session = options.session;
2145
- applyLedgerTimeout(options.ledgerTimeout, cliOpts);
2146
- reportRecordResult(await runRecordDogfood(cliOpts));
2147
- });
2148
- const sessionStart = program
2149
- .command("session-start")
2150
- .description("SessionStart hook entrypoints (called by Claude Code via settings.json)");
2151
- addLedgerTimeoutOption(addIdentityOptions(sessionStart
2152
- .command("preflight")
2153
- .description("SessionStart producer: run agent-preflight against the session cwd and, on a ready:true result, " +
2154
- "record a `preflight:${REPO}` fact to the evidence ledger so the preflight-before-* policies have a " +
2155
- "fresh tag to match. Reads SessionStart event JSON from stdin ({ session_id, cwd, hook_event_name }). " +
2156
- "Opt-in `session_start_preflight.setup: true` (default off) passes --setup through; " +
2157
- "see docs/CLI.md for the trust and scope caveats. " +
2158
- "blocking:false \u2014 every failure path logs to stderr and exits 0."), "explicit session id (overrides stdin event + env). Use for manual / scripted invocations " +
2159
- "where no SessionStart event JSON is piped on stdin. Without it the resolver tries " +
2160
- "stdin event → $CLAUDE_SESSION_ID → newest Claude Code transcript → 'default' (which logs " +
2161
- "a loud warning since the literal 'default' session never satisfies a preflight-before-* gate).").option("--timeout <ms>", "agent-preflight subprocess timeout in milliseconds (default 60000)")).action(preflightAction);
2162
- // The three advisory producers below share one option set and one
2163
- // action shape: only the description and the runner differ.
2164
- const addSessionStartProducer = (name, description, run) => {
2165
- addLedgerTimeoutOption(addCwdOption(addIdentityOptions(sessionStart.command(name).description(description), "explicit session id (overrides stdin event + env)"))).action(async (options) => {
2166
- const cliOpts = {};
2167
- applyCliOptions(options, cliOpts);
2168
- await run(cliOpts);
2169
- });
2170
- };
2171
- addSessionStartProducer("branch-check", "SessionStart producer for the branch-protection pack: read .git/HEAD for the session cwd and, " +
2172
- "when the branch is NOT in the operator's protected list (default: master, main, develop), " +
2173
- "record a `branch:non-protected:<branch>` fact to the evidence ledger so the pack's PreToolUse " +
2174
- "blocker has a fresh tag to satisfy its 5-minute freshness window. Also runnable on demand from " +
2175
- "the operator's shell. blocking:false \u2014 every failure path logs to stderr and exits 0.", runSessionStartBranchCheck);
2176
- addSessionStartProducer("toolchain-parity", "SessionStart producer (opt-in via `toolchain_parity.enabled: true`): writes THIS machine's " +
2177
- "toolchain snapshot (node version, npm globals, OW-Kit version, MCP server names) to " +
2178
- "`<machine_state_dir>/<profile>.json`, compares it against every OTHER snapshot file already " +
2179
- "in that directory, and records a `toolchain-parity:ok` / `toolchain-parity:drift:<n>` fact " +
2180
- "(with a `:unparseable-peer:<n>` suffix whenever a peer file failed to parse as JSON, so an " +
2181
- "unparseable peer never silently vanishes from the comparison) to the evidence ledger. " +
2182
- "Purely advisory \u2014 never blocking, and never touches a peer's file. " +
2183
- "Cross-machine transport of the snapshot files is agent-memory-sync's job, not this command's.", runSessionStartToolchainParity);
2184
- addSessionStartProducer("stale-base-check", "SessionStart producer (opt-in via `stale_base_check.enabled: true`; task ce3903b0, incident " +
2185
- "ea8becf5): runs a LIVE `git fetch` of the remote default branch (never trusting the local " +
2186
- "origin/<default> ref, which can itself be stale \u2014 that is the exact bug this closes) and, when " +
2187
- "the current branch's base is behind, writes a WARNING to stderr naming how many commits behind, " +
2188
- "how old the missing work is, and the recovery command. Records a `stale-base:ok` / " +
2189
- "`stale-base:behind:<n>` fact to the evidence ledger (audit-only \u2014 no gate consumes it). " +
2190
- "Purely advisory: never blocks, and degrades cleanly (no fact written) when offline, the remote " +
2191
- "or default branch can't be resolved, or credentials are missing. blocking:false \u2014 every failure " +
2192
- "path logs to stderr and exits 0.", runSessionStartStaleBaseCheck);
2193
- // `harness gate` — operator escape hatch for hard-blocking hooks.
2194
- // Task 8fcddb26: the understanding-before-execution PreToolUse hook can
2195
- // lock a Claude session out of every Bash call, and the recommended
2196
- // recovery (`harness approve understanding`) is itself a Bash invocation
2197
- // and gets caught by the same gate. `gate disable` strips the
2198
- // offending hook group out of settings.json with a reversible snapshot.
2199
- const gate = program
2200
- .command("gate")
2201
- .description("Operator escape hatch: disable/restore hook groups in ~/.claude/settings.json");
2202
- gate
2203
- .command("disable")
2204
- .description("Remove hook groups from ~/.claude/settings.json whose `matcher` substring-matches " +
2205
- "`--matcher <pattern>`. Writes a snapshot of the removed groups next to settings.json " +
2206
- "(`harness.gate-disable.<ts>.json`) and backs up the original to `settings.json.bak.<ts>` " +
2207
- "so `harness gate enable` can restore them. With no `--matcher` flag, lists the candidate " +
2208
- "groups without writing.")
2209
- .option("--matcher <pattern>", "remove groups whose matcher includes this substring")
2210
- .option("--settings <path>", "override ~/.claude/settings.json")
2211
- .action(async (options) => {
2212
- const cliOpts = {};
2213
- if (options.matcher)
2214
- cliOpts.matcher = options.matcher;
2215
- if (options.settings)
2216
- cliOpts.settingsPath = options.settings;
2217
- try {
2218
- const result = gateDisable(cliOpts);
2219
- if (result.mode === "list") {
2220
- if (result.candidates.length === 0) {
2221
- stdout(`no hook groups in ${result.settingsPath}; nothing to disable.\n`);
2222
- return;
2223
- }
2224
- stdout(`candidate hook groups in ${result.settingsPath}:\n`);
2225
- for (const c of result.candidates) {
2226
- const matcherLabel = c.matcher === null ? "(no matcher)" : JSON.stringify(c.matcher);
2227
- stdout(` ${c.event}[${c.index}] matcher=${matcherLabel}: ${c.description}\n`);
2228
- }
2229
- stdout(`\nPass --matcher <substring> to remove a group. Removal is reversible via ` +
2230
- `\`harness gate enable\`.\n`);
2231
- return;
2232
- }
2233
- stdout(`disabled ${result.removed.length} hook group(s) in ${result.settingsPath}:\n`);
2234
- for (const r of result.removed) {
2235
- const matcherLabel = r.group !== null && typeof r.group === "object" && !Array.isArray(r.group)
2236
- ? JSON.stringify(r.group["matcher"] ?? null)
2237
- : "null";
2238
- stdout(` ${r.event}[${r.index}] matcher=${matcherLabel}\n`);
2239
- }
2240
- stdout(`backup: ${result.backupPath}\n`);
2241
- stdout(`snapshot: ${result.snapshotPath}\n`);
2242
- stdout(`This is reversible: run \`harness gate enable\` to restore.\n`);
2243
- }
2244
- catch (err) {
2245
- if (err instanceof GateDisableError) {
2246
- throw new HarnessExitError(err.message, EX_FAIL);
2247
- }
2248
- throw err;
2249
- }
2250
- });
2251
- gate
2252
- .command("enable")
2253
- .description("Restore hook groups from the newest `harness.gate-disable.*.json` snapshot next to " +
2254
- "settings.json. Refuses if settings.json has been edited since the snapshot was taken " +
2255
- "(use `--force` to restore anyway). Idempotent: if settings.json already matches the " +
2256
- "pre-disable state, exits 0 without writing.")
2257
- .option("--settings <path>", "override ~/.claude/settings.json")
2258
- .option("--force", "restore even when settings.json has been edited since the snapshot")
2259
- .action(async (options) => {
2260
- const cliOpts = {};
2261
- if (options.settings)
2262
- cliOpts.settingsPath = options.settings;
2263
- if (options.force)
2264
- cliOpts.force = true;
2265
- try {
2266
- const result = gateEnable(cliOpts);
2267
- if (result.mode === "no-snapshots") {
2268
- stdout(`no gate-disable snapshots found next to ${result.settingsPath}; nothing to restore.\n`);
2269
- return;
2270
- }
2271
- if (result.mode === "already-restored") {
2272
- stdout(`settings.json already matches the pre-disable state; no write needed. ` +
2273
- `(snapshot: ${result.snapshotPath})\n`);
2274
- return;
2275
- }
2276
- stdout(`restored ${result.restoredCount} hook group(s) into ${result.settingsPath} from ` +
2277
- `${result.snapshotPath}.\n`);
2278
- }
2279
- catch (err) {
2280
- if (err instanceof GateEnableError) {
2281
- throw new HarnessExitError(err.message, EX_FAIL);
2282
- }
2283
- throw err;
2284
- }
2285
- });
2286
- program
2287
- .command("gc")
2288
- .description("Retention-based cleanup of harness-owned gate state: terminal " +
2289
- "(approved/expired) understanding-gate reports, parse-error logs, " +
2290
- "approval markers, expired delegation markers, orphaned delegation " +
2291
- "adoption ledgers, and stale permission-mode observations older than " +
2292
- "the retention window, plus stale in-flight subagent records (fixed " +
2293
- "24h window, independent of --retention-days). Pending reports and " +
2294
- "anything outside the enumerated harness-owned dirs are never " +
2295
- "touched (the evidence ledger and solution-acceptance verdict dirs " +
2296
- "are owned by their producers). Dry-run by default; pass --apply " +
2297
- "to delete.")
2298
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2299
- .option("--retention-days <n>", `delete artifacts older than this many days (default: ${DEFAULT_RETENTION_DAYS})`)
2300
- .option("--apply", "delete the listed artifacts (default: dry-run listing only)")
2301
- .action((options) => {
2302
- const cliOpts = {};
2303
- if (options.config)
2304
- cliOpts.configPath = options.config;
2305
- if (options.apply)
2306
- cliOpts.apply = true;
2307
- if (options.retentionDays !== undefined) {
2308
- const parsed = Number(options.retentionDays);
2309
- if (!Number.isFinite(parsed) || parsed < 1) {
2310
- stderr(`--retention-days must be a positive number, got ${JSON.stringify(options.retentionDays)}\n`);
2311
- throw new HarnessExitError("", EX_USAGE);
2312
- }
2313
- cliOpts.retentionDays = parsed;
2314
- }
2315
- const result = gc(cliOpts);
2316
- const sweptDirs = [
2317
- result.reportsDir,
2318
- ...(result.parseErrorsDir !== null ? [result.parseErrorsDir] : []),
2319
- result.approvalsDir,
2320
- result.delegationsDir,
2321
- result.adoptionLedgerDir,
2322
- result.permissionModeObservationsDir,
2323
- result.inflightRecordsDir,
2324
- ];
2325
- if (result.parseErrorsDir === null) {
2326
- stderr("gc: skipping the parse-errors sweep (reports dir does not have the conventional .understanding-gate/reports shape)\n");
2327
- }
2328
- if (result.unparseable.length > 0) {
2329
- stderr(`gc: ${result.unparseable.length} file(s) could not be parsed and were left in place:\n` +
2330
- result.unparseable.map((u) => ` [${u.category}] ${escapeForDisplay(u.filePath)} (${u.reason})\n`).join(""));
2331
- }
2332
- if (result.candidates.length === 0) {
2333
- stdout(`gc: nothing older than ${result.retentionDays}d (cutoff ${result.cutoffIso}) under\n` +
2334
- sweptDirs.map((d) => ` ${d}\n`).join("") +
2335
- `${result.keptCount} artifact(s) inspected and kept.\n`);
2336
- return;
2337
- }
2338
- const verb = result.applied ? "removing" : "would remove";
2339
- stdout(`gc: ${verb} ${result.candidates.length} artifact(s) older than ${result.retentionDays}d (cutoff ${result.cutoffIso}); keeping ${result.keptCount}:\n`);
2340
- for (const c of result.candidates) {
2341
- stdout(` [${c.category}] ${escapeForDisplay(c.filePath)} (${c.reason})\n`);
2342
- }
2343
- if (!result.applied) {
2344
- stdout(`\nDry-run; pass --apply to delete.\n`);
2345
- return;
2346
- }
2347
- stdout(`removed ${result.removed.length} file(s).\n`);
2348
- if (result.failures.length > 0) {
2349
- for (const f of result.failures) {
2350
- stderr(`gc: failed to remove ${escapeForDisplay(f.filePath)}: ${escapeForDisplay(f.reason)}\n`);
2351
- }
2352
- throw new HarnessExitError(`${result.failures.length} deletion(s) failed`, EX_FAIL);
2353
- }
2354
- });
2355
- program
2356
- .command("uninstall")
2357
- .description("Clean teardown of a harness installation. Inventories harness-owned " +
2358
- "state (manifest, lock, harness.generated/, .understanding-gate/ under " +
2359
- "the state root; hook groups and mcpServers in ~/.claude/settings.json) " +
2360
- "and prints it. With --apply, removes it after writing a reversible " +
2361
- "settings.json backup + snapshot. " +
2362
- "settings.json.pre-harness-<TS> backups are listed but never deleted, " +
2363
- "so the operator can hand them to --restore-from <path> (atomic restore " +
2364
- "from that file) or `rm` them manually.")
2365
- .option("--apply", "execute the teardown (default: dry-run listing only)")
2366
- .option("--restore-from <path>", "atomic restore: copy this file over settings.json instead of selective removal (implies --apply)")
2367
- .option("--home <path>", "override ~/.claude/ (settings home; without --state it also overrides the state root, for tests / non-default installs)")
2368
- .option("--state <path>", "override the harness state root (default: ~/.harness/, legacy fallback ~/.claude/)")
2369
- .option("--settings <path>", "override ~/.claude/settings.json")
2370
- .action(async (options) => {
2371
- const cliOpts = {};
2372
- if (options.apply)
2373
- cliOpts.apply = true;
2374
- if (options.restoreFrom)
2375
- cliOpts.restoreFrom = options.restoreFrom;
2376
- if (options.home)
2377
- cliOpts.homeDir = options.home;
2378
- if (options.state)
2379
- cliOpts.stateDir = options.state;
2380
- if (options.settings)
2381
- cliOpts.settingsPath = options.settings;
2382
- try {
2383
- const result = await uninstall(cliOpts);
2384
- const inv = result.inventory;
2385
- if (result.mode === "list") {
2386
- const nothing = inv.manifestPath === null &&
2387
- inv.lockPath === null &&
2388
- inv.generatedDir === null &&
2389
- inv.gateStateDir === null &&
2390
- inv.hookGroups.length === 0 &&
2391
- inv.mcpServers.length === 0 &&
2392
- inv.mcpRegistryServers.length === 0 &&
2393
- inv.preHarnessBackups.length === 0;
2394
- const rootsLabel = inv.stateDir === inv.homeDir
2395
- ? inv.homeDir
2396
- : `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
2397
- if (nothing) {
2398
- stdout(`no harness install found under ${rootsLabel}; nothing to do.\n`);
2399
- for (const w of inv.warnings)
2400
- stderr(`warning: ${w}\n`);
2401
- return;
2402
- }
2403
- stdout(`harness install under ${rootsLabel}:\n`);
2404
- if (inv.manifestPath)
2405
- stdout(` manifest: ${inv.manifestPath}\n`);
2406
- if (inv.lockPath)
2407
- stdout(` lock: ${inv.lockPath}\n`);
2408
- if (inv.generatedDir)
2409
- stdout(` generated: ${inv.generatedDir}/\n`);
2410
- if (inv.gateStateDir)
2411
- stdout(` gate: ${inv.gateStateDir}/ (understanding-gate state)\n`);
2412
- if (inv.hookGroups.length > 0) {
2413
- stdout(` hook groups in ${inv.settingsPath}:\n`);
2414
- for (const g of inv.hookGroups) {
2415
- const matcherLabel = g.matcher === null ? "(no matcher)" : JSON.stringify(g.matcher);
2416
- stdout(` ${g.event}[${g.index}] matcher=${matcherLabel}: ${g.description}\n`);
2417
- }
2418
- }
2419
- if (inv.mcpServers.length > 0) {
2420
- stdout(` mcpServers in ${inv.settingsPath}: ${inv.mcpServers.join(", ")}\n`);
2421
- }
2422
- if (inv.mcpRegistryServers.length > 0) {
2423
- stdout(` mcpServers registered in the claude CLI user-scope registry (${inv.mcpRegistryPath}): ` +
2424
- `${inv.mcpRegistryServers.join(", ")}\n`);
2425
- }
2426
- if (inv.preHarnessBackups.length > 0) {
2427
- stdout(` pre-harness backups:\n`);
2428
- for (const b of inv.preHarnessBackups)
2429
- stdout(` ${b}\n`);
2430
- stdout(`\n Restore from one of these with: harness uninstall --restore-from <path>\n`);
2431
- }
2432
- stdout(`\nPass --apply to remove the above. This is a dry-run.\n`);
2433
- for (const w of inv.warnings)
2434
- stderr(`warning: ${w}\n`);
2435
- return;
2436
- }
2437
- if (result.mode === "restore") {
2438
- stdout(`restored ${inv.settingsPath} from ${result.restoredFrom}.\n`);
2439
- stdout(`backup: ${result.backupPath}\n`);
2440
- stdout(`snapshot: ${result.snapshotPath}\n`);
2441
- if (result.removedFiles.length > 0) {
2442
- stdout(`removed:\n`);
2443
- for (const f of result.removedFiles)
2444
- stdout(` ${f}\n`);
2445
- }
2446
- if (result.mcpRegistryRemovals.length > 0) {
2447
- stdout(`claude mcp remove (user scope, ${inv.mcpRegistryPath}):\n`);
2448
- for (const r of result.mcpRegistryRemovals)
2449
- stdout(` ${r.name}: ${r.status}\n`);
2450
- }
2451
- stdout(`\nTo finish: \`npm uninstall -g @lannguyensi/harness\` (uninstall does not touch the npm install).\n`);
2452
- for (const w of inv.warnings)
2453
- stderr(`warning: ${w}\n`);
2454
- return;
2455
- }
2456
- // apply
2457
- if (result.backupPath !== null && result.snapshotPath !== null) {
2458
- stdout(`mutated ${inv.settingsPath}:\n`);
2459
- if (inv.hookGroups.length > 0) {
2460
- stdout(` removed ${inv.hookGroups.length} hook group(s): `);
2461
- stdout(inv.hookGroups.map((g) => `${g.event}[${g.index}]`).join(", "));
2462
- stdout(`\n`);
2463
- }
2464
- if (inv.mcpServers.length > 0) {
2465
- stdout(` removed mcpServers: ${inv.mcpServers.join(", ")}\n`);
2466
- }
2467
- stdout(`backup: ${result.backupPath}\n`);
2468
- stdout(`snapshot: ${result.snapshotPath}\n`);
2469
- }
2470
- if (result.removedFiles.length > 0) {
2471
- stdout(`removed from disk:\n`);
2472
- for (const f of result.removedFiles)
2473
- stdout(` ${f}\n`);
2474
- // Explicit kept-list: name whatever survives under the state
2475
- // root (machines/ + projects/ override layers are
2476
- // operator-authored; foreign files are not ours to judge) so
2477
- // the operator never has to discover residue by accident.
2478
- try {
2479
- const residue = fs
2480
- .readdirSync(inv.stateDir)
2481
- .filter((name) => !name.startsWith("settings.json"));
2482
- if (residue.length > 0) {
2483
- stdout(`kept under ${inv.stateDir}: ${residue.join(", ")} (not removed; operator-authored or out of scope)\n`);
2484
- }
2485
- }
2486
- catch {
2487
- /* state root itself may be gone or unreadable; nothing to report */
2488
- }
2489
- }
2490
- if (result.mcpRegistryRemovals.length > 0) {
2491
- stdout(`claude mcp remove (user scope, ${inv.mcpRegistryPath}):\n`);
2492
- for (const r of result.mcpRegistryRemovals)
2493
- stdout(` ${r.name}: ${r.status}\n`);
2494
- }
2495
- if (result.backupPath === null &&
2496
- result.snapshotPath === null &&
2497
- result.removedFiles.length === 0 &&
2498
- result.mcpRegistryRemovals.length === 0) {
2499
- const rootsLabel = inv.stateDir === inv.homeDir
2500
- ? inv.homeDir
2501
- : `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
2502
- stdout(`no harness install found under ${rootsLabel}; nothing to remove.\n`);
2503
- }
2504
- else {
2505
- stdout(`\nTo finish: \`npm uninstall -g @lannguyensi/harness\` (uninstall does not touch the npm install).\n`);
2506
- }
2507
- for (const w of inv.warnings)
2508
- stderr(`warning: ${w}\n`);
2509
- }
2510
- catch (err) {
2511
- if (err instanceof UninstallError) {
2512
- throw new HarnessExitError(err.message, EX_FAIL);
2513
- }
2514
- throw err;
2515
- }
2516
- });
2517
- program
2518
- .command("migrate-home")
2519
- .description("Move harness operator-state from the legacy ~/.claude/ root to the runtime-neutral " +
2520
- "~/.harness/ root introduced in v0.24.0. Dry-run by default; pass --apply to perform " +
2521
- "the move. Re-running on already-migrated state is a no-op. Moves: harness.yaml, " +
2522
- "harness.generated/, .understanding-gate/, harness.lock. Does NOT touch settings.json " +
2523
- "or any other ~/.claude/ contents.")
2524
- .option("--apply", "perform the move (default: dry-run report only)")
2525
- .action(async (options) => {
2526
- const result = migrateHome({ ...(options.apply ? { apply: true } : {}) });
2527
- if (result.outcome === "target-conflict") {
2528
- throw new HarnessExitError("", EX_FAIL);
2529
- }
2530
- });
2531
- // `harness pause` / `harness resume`: operator-only kill switch.
2532
- // Sibling top-level commands: `pause` must start its own
2533
- // `program.command(...)` statement. It was previously chained onto the
2534
- // `migrate-home` block above, which (Commander's `.command()` returns
2535
- // the new subcommand) registered it as `harness migrate-home pause`
2536
- // and dropped it from top-level `harness --help`.
2537
- program
2538
- .command("pause")
2539
- .description("Temporarily make all harness hooks dormant by writing a sentinel under harness.generated/. " +
2540
- "Operator-only (refuses when $CLAUDE_CODE_SESSION_ID, $CLAUDE_SESSION_ID, or $CODEX_SESSION_ID " +
2541
- "is set, or stdin is non-TTY). Intended for lockout recovery, debug A/B-tests, and incident " +
2542
- "hotfixes. NOT for routine gate bypass: for permanent per-policy disable, edit " +
2543
- "`policies[].enabled` in the manifest.")
2544
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2545
- .option("--project <name>", "apply per-project overrides")
2546
- .option("--for <duration>", "auto-resume after this duration (e.g. 5m, 1h, PT30S; default: 15m)")
2547
- .option("--indefinite", "skip auto-expiry (requires --i-am-the-operator-and-accept-no-auto-resume)")
2548
- .option("--i-am-the-operator-and-accept-no-auto-resume", "acknowledge that --indefinite leaves harness dormant until you remember to resume")
2549
- .option("--reason <text>", "free-form reason recorded in the sentinel + announced on each hook fire")
2550
- .option("--i-am-the-operator", "acknowledge a scripted / non-TTY invocation (otherwise refused)")
2551
- .action(async (options) => {
2552
- const cliOpts = {};
2553
- if (options.config)
2554
- cliOpts.configPath = options.config;
2555
- if (options.project)
2556
- cliOpts.project = options.project;
2557
- if (options.for)
2558
- cliOpts.forDuration = options.for;
2559
- if (options.indefinite)
2560
- cliOpts.indefinite = true;
2561
- if (options.iAmTheOperatorAndAcceptNoAutoResume)
2562
- cliOpts.acceptNoAutoResume = true;
2563
- if (options.reason)
2564
- cliOpts.reason = options.reason;
2565
- if (options.iAmTheOperator)
2566
- cliOpts.iAmTheOperator = true;
2567
- const result = await pauseHarness(cliOpts);
2568
- const lines = [];
2569
- if (result.alreadyPaused) {
2570
- lines.push("note: harness was already paused; sentinel overwritten with new expiry");
2571
- }
2572
- const expiry = result.sentinel.expiresAt === null
2573
- ? "indefinite (no auto-resume)"
2574
- : `auto-resumes at ${result.sentinel.expiresAt}`;
2575
- lines.push(`paused: ✓ ${result.sentinelPath}`);
2576
- lines.push(`expiry: ${expiry}`);
2577
- if (result.sentinel.reason !== null) {
2578
- lines.push(`reason: ${result.sentinel.reason}`);
2579
- }
2580
- if (result.ledger.ok) {
2581
- lines.push(`ledger: ✓ wrote ${result.ledger.tag} (audit)`);
2582
- }
2583
- else {
2584
- lines.push(`ledger: ⚠ skipped (${result.ledger.reason ?? "unknown"}) (audit)`);
2585
- }
2586
- lines.push("");
2587
- lines.push("Hooks will allow + emit a stderr notice while paused. Run `harness resume` to re-enable.");
2588
- stdout(`${lines.join("\n")}\n`);
2589
- });
2590
- program
2591
- .command("resume")
2592
- .description("Delete the pause sentinel and re-enable harness hooks. Operator-only. Idempotent: " +
2593
- "running against an un-paused install exits 0 with a notice.")
2594
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2595
- .option("--project <name>", "apply per-project overrides")
2596
- .option("--i-am-the-operator", "acknowledge a scripted / non-TTY invocation (otherwise refused)")
2597
- .action(async (options) => {
2598
- const cliOpts = {};
2599
- if (options.config)
2600
- cliOpts.configPath = options.config;
2601
- if (options.project)
2602
- cliOpts.project = options.project;
2603
- if (options.iAmTheOperator)
2604
- cliOpts.iAmTheOperator = true;
2605
- const result = await resumeHarness(cliOpts);
2606
- const lines = [];
2607
- if (!result.wasPaused) {
2608
- lines.push(`resume: · harness was not paused (${result.sentinelPath} did not exist)`);
2609
- }
2610
- else {
2611
- lines.push(`resume: ✓ deleted ${result.sentinelPath}`);
2612
- if (result.previousSentinel) {
2613
- const prev = result.previousSentinel;
2614
- lines.push(`prev: pausedAt=${prev.pausedAt} expiresAt=${prev.expiresAt ?? "indefinite"}`);
2615
- if (prev.reason !== null)
2616
- lines.push(`reason: ${prev.reason}`);
2617
- }
2618
- if (result.ledger.ok) {
2619
- lines.push(`ledger: ✓ wrote ${result.ledger.tag} (audit)`);
2620
- }
2621
- else {
2622
- lines.push(`ledger: ⚠ skipped (${result.ledger.reason ?? "unknown"}) (audit)`);
2623
- }
2624
- }
2625
- stdout(`${lines.join("\n")}\n`);
2626
- });
2627
- const policy = program.command("policy").description("Policy runtime verbs");
2628
- policy
2629
- .command("intercept")
2630
- .description("PreToolUse hook entrypoint: read tool-event JSON from stdin, evaluate matching policies, emit Claude Code deny JSON on block. " +
2631
- "Stdin shape (per Claude Code hook protocol): " +
2632
- "{ session_id, hook_event_name, tool_name, tool_input, cwd?, transcript_path? }. " +
2633
- "hook_event_name is required for any policy to match; if missing or unmatched, a one-line diagnostic is written to stderr.")
2634
- .option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
2635
- .option("--project <name>", "apply per-project overrides")
2636
- .option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
2637
- .option("--verbose", "emit a stderr diagnostic block for each non-allow decision (also enabled by HARNESS_POLICY_VERBOSE=1)")
2638
- .option("--hook <name>", "manifest hook name (injected by `harness apply` for the Codex projection so failure logs identify which hook fired)")
2639
- .action(async (options) => {
2640
- const cliOpts = {};
2641
- if (options.config)
2642
- cliOpts.configPath = options.config;
2643
- if (options.project)
2644
- cliOpts.project = options.project;
2645
- if (options.ledgerTimeout) {
2646
- const n = Number.parseInt(options.ledgerTimeout, 10);
2647
- if (Number.isFinite(n) && n > 0)
2648
- cliOpts.ledgerTimeoutMs = n;
2649
- }
2650
- if (options.verbose)
2651
- cliOpts.verbose = options.verbose;
2652
- if (options.hook)
2653
- cliOpts.hookName = options.hook;
2654
- await runInterceptCli(cliOpts);
2655
- });
57
+ registerInspectGroup(program, { stdout, stderr }, opts);
58
+ registerSetupGroup(program, { stdout, stderr });
59
+ registerRemove(program, { stdout, stderr });
60
+ registerPackGroup(program, { stdout, stderr });
61
+ registerGateGroup(program, { stdout, stderr });
62
+ registerUninstall(program, { stdout, stderr });
63
+ registerOperatorLifecycle(program, { stdout, stderr });
2656
64
  return program;
2657
65
  }
2658
66
  export async function run(opts = {}) {