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