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