@lannguyensi/harness 0.40.0 → 0.42.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 +68 -0
- package/README.md +9 -0
- package/dist/cli/adopt/derive.d.ts +15 -0
- package/dist/cli/adopt/derive.js +34 -7
- package/dist/cli/adopt/derive.js.map +1 -1
- package/dist/cli/adopt/index.d.ts +30 -0
- package/dist/cli/adopt/index.js +23 -3
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/generate-settings.d.ts +14 -1
- package/dist/cli/apply/generate-settings.js +7 -3
- package/dist/cli/apply/generate-settings.js.map +1 -1
- package/dist/cli/approve/stdin-report.js +2 -0
- package/dist/cli/approve/stdin-report.js.map +1 -1
- package/dist/cli/approve/understanding.js +28 -0
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/doctor/claude-mcp.d.ts +50 -0
- package/dist/cli/doctor/claude-mcp.js +170 -0
- package/dist/cli/doctor/claude-mcp.js.map +1 -0
- package/dist/cli/doctor/codex.d.ts +8 -0
- package/dist/cli/doctor/codex.js +8 -1
- package/dist/cli/doctor/codex.js.map +1 -1
- package/dist/cli/doctor/format.js +60 -11
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +46 -0
- package/dist/cli/doctor/index.js +155 -29
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/npm-bin-path.js +42 -0
- package/dist/cli/doctor/npm-bin-path.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +37 -0
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/dry-run.js +5 -0
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/explain.js +16 -3
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/index.js +328 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/agent-tasks-auth.js +32 -0
- package/dist/cli/init/agent-tasks-auth.js.map +1 -1
- package/dist/cli/init/composer.js +13 -40
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/dependencies.js +21 -0
- package/dist/cli/init/dependencies.js.map +1 -1
- package/dist/cli/init/detect.d.ts +20 -3
- package/dist/cli/init/detect.js +34 -36
- package/dist/cli/init/detect.js.map +1 -1
- package/dist/cli/init/index.d.ts +18 -0
- package/dist/cli/init/index.js +28 -2
- package/dist/cli/init/index.js.map +1 -1
- package/dist/cli/init/interactive.d.ts +41 -0
- package/dist/cli/init/interactive.js +421 -6
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +10 -1
- package/dist/cli/init/profiles.js.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +217 -8
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/list.js +3 -1
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/pack/hook-bootstrap.d.ts +24 -0
- package/dist/cli/pack/hook-bootstrap.js +47 -1
- package/dist/cli/pack/hook-bootstrap.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +9 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-codex-post-tool-use.d.ts +25 -0
- package/dist/cli/pack/hook-codex-post-tool-use.js +160 -0
- package/dist/cli/pack/hook-codex-post-tool-use.js.map +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +43 -10
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-codex-stop.js +1 -8
- package/dist/cli/pack/hook-codex-stop.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate-record.d.ts +30 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js +153 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js.map +1 -0
- package/dist/cli/pack/hook-post-merge-gate.d.ts +28 -0
- package/dist/cli/pack/hook-post-merge-gate.js +244 -0
- package/dist/cli/pack/hook-post-merge-gate.js.map +1 -0
- package/dist/cli/pack/hook-post-tool-use.js +16 -131
- package/dist/cli/pack/hook-post-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +54 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +148 -2
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-stay-in-scope.js +63 -20
- package/dist/cli/pack/hook-stay-in-scope.js.map +1 -1
- package/dist/cli/pack/hook-track-active-claim.js +33 -9
- package/dist/cli/pack/hook-track-active-claim.js.map +1 -1
- package/dist/cli/pack/index.d.ts +2 -1
- package/dist/cli/pack/index.js +2 -1
- package/dist/cli/pack/index.js.map +1 -1
- package/dist/cli/pack/mutate.d.ts +19 -0
- package/dist/cli/pack/mutate.js +38 -0
- package/dist/cli/pack/mutate.js.map +1 -1
- package/dist/cli/pack/reseed.d.ts +19 -0
- package/dist/cli/pack/reseed.js +118 -0
- package/dist/cli/pack/reseed.js.map +1 -0
- package/dist/cli/pack/understanding-report-schema-hint.js +1 -1
- package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
- package/dist/cli/pause/index.js +12 -3
- package/dist/cli/pause/index.js.map +1 -1
- package/dist/cli/record/index.d.ts +64 -0
- package/dist/cli/record/index.js +211 -0
- package/dist/cli/record/index.js.map +1 -0
- package/dist/cli/session-start/index.d.ts +21 -0
- package/dist/cli/session-start/index.js +160 -25
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/session-start/toolchain-parity.d.ts +149 -0
- package/dist/cli/session-start/toolchain-parity.js +618 -0
- package/dist/cli/session-start/toolchain-parity.js.map +1 -0
- package/dist/cli/uninstall/index.d.ts +73 -1
- package/dist/cli/uninstall/index.js +134 -2
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/cli/validate/checks.js +8 -0
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/io/claude-mcp.d.ts +258 -0
- package/dist/io/claude-mcp.js +544 -0
- package/dist/io/claude-mcp.js.map +1 -0
- package/dist/policies/ledger-client.d.ts +34 -1
- package/dist/policies/ledger-client.js +21 -16
- package/dist/policies/ledger-client.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -2
- package/dist/policy-packs/builtin/branch-protection-runtime.js +4 -2
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.d.ts +10 -1
- package/dist/policy-packs/builtin/branch-protection.js +17 -0
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +195 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js +290 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -0
- package/dist/policy-packs/builtin/post-merge-gate.d.ts +56 -0
- package/dist/policy-packs/builtin/post-merge-gate.js +291 -0
- package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +28 -13
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +197 -24
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +342 -40
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +26 -1
- package/dist/policy-packs/builtin/understanding-before-execution.js +302 -17
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/policy-packs/registry.d.ts +23 -2
- package/dist/policy-packs/registry.js +31 -2
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/policy-packs/ux-compare.d.ts +14 -0
- package/dist/policy-packs/ux-compare.js +58 -0
- package/dist/policy-packs/ux-compare.js.map +1 -0
- package/dist/policy-packs/ux-drift-check.d.ts +27 -0
- package/dist/policy-packs/ux-drift-check.js +78 -0
- package/dist/policy-packs/ux-drift-check.js.map +1 -0
- package/dist/probes/mcp.d.ts +13 -0
- package/dist/probes/mcp.js +57 -0
- package/dist/probes/mcp.js.map +1 -1
- package/dist/runtime/approval-signing.d.ts +82 -0
- package/dist/runtime/approval-signing.js +275 -0
- package/dist/runtime/approval-signing.js.map +1 -0
- package/dist/runtime/git-context.d.ts +50 -0
- package/dist/runtime/git-context.js +108 -1
- package/dist/runtime/git-context.js.map +1 -1
- package/dist/runtime/hermetic-spawn-guard.d.ts +25 -0
- package/dist/runtime/hermetic-spawn-guard.js +136 -0
- package/dist/runtime/hermetic-spawn-guard.js.map +1 -0
- package/dist/runtime/intercept.js +62 -5
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/ledger-writer.d.ts +27 -0
- package/dist/runtime/ledger-writer.js +46 -0
- package/dist/runtime/ledger-writer.js.map +1 -0
- package/dist/runtime/pause-sentinel.js +14 -3
- package/dist/runtime/pause-sentinel.js.map +1 -1
- package/dist/runtime/read-only-bash.js +118 -0
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/runtime/recovery-git-commit.d.ts +22 -0
- package/dist/runtime/recovery-git-commit.js +284 -0
- package/dist/runtime/recovery-git-commit.js.map +1 -0
- package/dist/runtime/tool-name-aliases.d.ts +1 -0
- package/dist/runtime/tool-name-aliases.js +7 -1
- package/dist/runtime/tool-name-aliases.js.map +1 -1
- package/dist/schema/index.d.ts +94 -42
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/policies.d.ts +56 -44
- package/dist/schema/policies.js +79 -9
- package/dist/schema/policies.js.map +1 -1
- package/dist/schema/toolchain-parity.d.ts +18 -0
- package/dist/schema/toolchain-parity.js +35 -0
- package/dist/schema/toolchain-parity.js.map +1 -0
- package/package.json +4 -2
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
+
import { HermeticSpawnViolationError } from "../runtime/hermetic-spawn-guard.js";
|
|
6
7
|
// Production version probe for `harness doctor`: synchronous --version
|
|
7
8
|
// invocation with a 5s timeout. Tests inject their own probe; the CLI
|
|
8
9
|
// entrypoint wires this default. Same shape as `cli/doctor/codex.ts`.
|
|
@@ -26,12 +27,13 @@ import { add } from "./add/index.js";
|
|
|
26
27
|
import { adopt } from "./adopt/index.js";
|
|
27
28
|
import { apply, DRIFT_HINT_MESSAGE, formatNextSteps, SETTINGS_BASENAME, } from "./apply/index.js";
|
|
28
29
|
import { isRemoveType, KNOWN_REMOVE_TYPES, remove } from "./remove/index.js";
|
|
29
|
-
import { packAdd, packList, packRemove } from "./pack/index.js";
|
|
30
|
+
import { packAdd, packList, packRemove, packReseed } from "./pack/index.js";
|
|
30
31
|
import { runPackHookPreToolUseCli } from "./pack/hook-pre-tool-use.js";
|
|
31
32
|
import { runPackHookPostToolUseCli } from "./pack/hook-post-tool-use.js";
|
|
32
33
|
import { runPackHookTrackActiveClaimCli } from "./pack/hook-track-active-claim.js";
|
|
33
34
|
import { runPackHookStayInScopeCli } from "./pack/hook-stay-in-scope.js";
|
|
34
35
|
import { runPackHookCodexPreToolUseCli } from "./pack/hook-codex-pre-tool-use.js";
|
|
36
|
+
import { runPackHookCodexPostToolUseCli } from "./pack/hook-codex-post-tool-use.js";
|
|
35
37
|
import { runPackHookCodexStopCli } from "./pack/hook-codex-stop.js";
|
|
36
38
|
import { runPackHookCodexUserPromptSubmitCli } from "./pack/hook-codex-user-prompt-submit.js";
|
|
37
39
|
import { isRuntime, KNOWN_RUNTIMES } from "../policy-packs/index.js";
|
|
@@ -39,6 +41,7 @@ import { approveBranchProtection } from "./approve/branch-protection.js";
|
|
|
39
41
|
import { approveRisk } from "./approve/risk.js";
|
|
40
42
|
import { approveUnderstanding } from "./approve/understanding.js";
|
|
41
43
|
import { readPipedStdin } from "./approve/stdin-report.js";
|
|
44
|
+
import { runRecordDogfood, runRecordReview, runRecordReviewSubagent, } from "./record/index.js";
|
|
42
45
|
import { describe, isPillar } from "./describe.js";
|
|
43
46
|
import { diff as diffRun } from "./diff/index.js";
|
|
44
47
|
import { diffSinceApply } from "./diff/since-apply.js";
|
|
@@ -63,9 +66,12 @@ import { runInterceptCli } from "./policy/intercept.js";
|
|
|
63
66
|
import { runSessionStartPreflight } from "./session-start/index.js";
|
|
64
67
|
import { writePendingApproval } from "../runtime/pending-approval.js";
|
|
65
68
|
import { runSessionStartBranchCheck } from "./session-start/branch-check.js";
|
|
69
|
+
import { runSessionStartToolchainParity } from "./session-start/toolchain-parity.js";
|
|
66
70
|
import { runPackHookBranchProtectionCli } from "./pack/hook-branch-protection.js";
|
|
67
71
|
import { runPackHookSolutionAcceptanceCli } from "./pack/hook-solution-acceptance.js";
|
|
68
72
|
import { runPackHookSolutionAcceptanceWriteguardCli } from "./pack/hook-solution-acceptance-writeguard.js";
|
|
73
|
+
import { runPackHookPostMergeGateRecordCli } from "./pack/hook-post-merge-gate-record.js";
|
|
74
|
+
import { runPackHookPostMergeGateCli } from "./pack/hook-post-merge-gate.js";
|
|
69
75
|
import { runPackHookRuntimeRealityCli } from "./pack/hook-runtime-reality.js";
|
|
70
76
|
import { gateDisable, GateDisableError } from "./gate/disable.js";
|
|
71
77
|
import { gateEnable, GateEnableError } from "./gate/enable.js";
|
|
@@ -190,16 +196,32 @@ export function buildProgram(opts = {}) {
|
|
|
190
196
|
? { rogueLedgerScanOptions: opts.rogueLedgerScanOptions }
|
|
191
197
|
: {}),
|
|
192
198
|
});
|
|
199
|
+
// Non-zero exit when the report carries errors (task a07b379a):
|
|
200
|
+
// callers previously had no way to gate CI/scripts on doctor
|
|
201
|
+
// health, since this action always fell through to exit 0
|
|
202
|
+
// regardless of report.errorCount. Warnings alone still exit 0.
|
|
203
|
+
// Checked before every return below (json / no --rm-rogue-ledgers /
|
|
204
|
+
// no hits found / after the delete+rescan flow) so the exit code
|
|
205
|
+
// is consistent across all output modes.
|
|
206
|
+
const failIfErrors = () => {
|
|
207
|
+
if (report.errorCount > 0) {
|
|
208
|
+
throw new HarnessExitError("", EX_FAIL);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
193
211
|
if (options.json) {
|
|
194
212
|
stdout(`${JSON.stringify(report, null, 2)}\n`);
|
|
213
|
+
failIfErrors();
|
|
195
214
|
return;
|
|
196
215
|
}
|
|
197
216
|
stdout(formatDoctor(report));
|
|
198
|
-
if (!options.rmRogueLedgers)
|
|
217
|
+
if (!options.rmRogueLedgers) {
|
|
218
|
+
failIfErrors();
|
|
199
219
|
return;
|
|
220
|
+
}
|
|
200
221
|
const hits = report.rogueLedgerDbs;
|
|
201
222
|
if (hits.length === 0) {
|
|
202
223
|
stdout("no rogue evidence-ledger DBs found; nothing to delete\n");
|
|
224
|
+
failIfErrors();
|
|
203
225
|
return;
|
|
204
226
|
}
|
|
205
227
|
const result = await deleteRogueLedgers(hits, { yes: options.yes });
|
|
@@ -221,6 +243,7 @@ export function buildProgram(opts = {}) {
|
|
|
221
243
|
: {}),
|
|
222
244
|
});
|
|
223
245
|
stdout(`rogue evidence-ledger DBs remaining: ${afterScan.length}\n`);
|
|
246
|
+
failIfErrors();
|
|
224
247
|
});
|
|
225
248
|
program
|
|
226
249
|
.command("list <category>")
|
|
@@ -328,6 +351,9 @@ export function buildProgram(opts = {}) {
|
|
|
328
351
|
if (r.validateClean === false) {
|
|
329
352
|
throw new HarnessExitError(`manifest written but failed harness validate; see stderr for diagnostics`, EX_FAIL);
|
|
330
353
|
}
|
|
354
|
+
if (r.binResolutionClean === false) {
|
|
355
|
+
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);
|
|
356
|
+
}
|
|
331
357
|
return;
|
|
332
358
|
}
|
|
333
359
|
if (options.template !== undefined && !isTemplate(options.template)) {
|
|
@@ -482,13 +508,25 @@ export function buildProgram(opts = {}) {
|
|
|
482
508
|
});
|
|
483
509
|
program
|
|
484
510
|
.command("adopt <file>")
|
|
485
|
-
.description("Capture hand-edits
|
|
486
|
-
"
|
|
487
|
-
"
|
|
511
|
+
.description("Capture hand-edits into the manifest. Hooks are diffed against <file> " +
|
|
512
|
+
"(today: ~/.claude/settings.json). MCP servers are ALWAYS diffed " +
|
|
513
|
+
"against the effective Claude Code registry (top-level mcpServers in " +
|
|
514
|
+
"~/.claude.json, respecting CLAUDE_CONFIG_DIR) regardless of <file> — " +
|
|
515
|
+
"Claude Code does not read <file>'s mcpServers block at runtime. " +
|
|
516
|
+
"Prompts y/N before writing.")
|
|
488
517
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
489
518
|
.option("--yes", "skip the confirmation prompt (for non-interactive use)")
|
|
490
519
|
.action(async (file, options) => {
|
|
491
520
|
const result = await adopt(file, { configPath: options.config, yes: options.yes });
|
|
521
|
+
if (result.deadSettingsMcpNames.length > 0) {
|
|
522
|
+
stderr(`⚠ ${file} has a dead \`mcpServers\` block (${result.deadSettingsMcpNames.join(", ")}); ` +
|
|
523
|
+
"Claude Code does not read this file for MCP registration — adopt diffed against the " +
|
|
524
|
+
"effective registry instead. Safe to remove by hand.\n");
|
|
525
|
+
}
|
|
526
|
+
if (result.registryReadError !== undefined) {
|
|
527
|
+
stderr(`⚠ could not read the Claude Code MCP registry: ${result.registryReadError}; ` +
|
|
528
|
+
"treated as empty for MCP drift purposes.\n");
|
|
529
|
+
}
|
|
492
530
|
if (result.outcome === "no-drift") {
|
|
493
531
|
stdout(`nothing to adopt (no drift between ${file} and ${result.manifestPath})\n`);
|
|
494
532
|
return;
|
|
@@ -713,7 +751,7 @@ export function buildProgram(opts = {}) {
|
|
|
713
751
|
// `harness pack` subtree (Phase 6 #3): managed CRUD over policy_packs[].
|
|
714
752
|
const packCmd = program
|
|
715
753
|
.command("pack")
|
|
716
|
-
.description("Manage policy_packs[] entries (add / remove / list)");
|
|
754
|
+
.description("Manage policy_packs[] entries (add / remove / list / reseed)");
|
|
717
755
|
packCmd
|
|
718
756
|
.command("add <name>")
|
|
719
757
|
.description("Insert a new policy_packs entry. <name> must be a known builtin (see docs/policy-packs/).")
|
|
@@ -765,6 +803,34 @@ export function buildProgram(opts = {}) {
|
|
|
765
803
|
}
|
|
766
804
|
stdout(`removed policy_packs entry ${JSON.stringify(result.name)} from ${result.path}\n`);
|
|
767
805
|
});
|
|
806
|
+
// `harness pack reseed <name>` (task 68b9ad9c): pull the shipped
|
|
807
|
+
// builtin template's config.ux (and config.producers) into an already-
|
|
808
|
+
// installed manifest. Explicit-only, mirroring add/remove — never
|
|
809
|
+
// invoked by `apply`, so an upgrade never silently rewrites an
|
|
810
|
+
// operator's deliberate ux customisation. Paired with the `harness
|
|
811
|
+
// doctor` divergence warning (src/policy-packs/ux-drift-check.ts).
|
|
812
|
+
packCmd
|
|
813
|
+
.command("reseed <name>")
|
|
814
|
+
.description("Pull the shipped builtin template's config.ux (and config.producers) for <name> into " +
|
|
815
|
+
"the manifest, preserving other config keys (mode, approval_lifecycle, ...). No-op if " +
|
|
816
|
+
"already up to date. See `harness doctor` for a divergence warning.")
|
|
817
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
818
|
+
.option("--dry-run", "print the unified diff and exit without writing")
|
|
819
|
+
.action(async (name, options) => {
|
|
820
|
+
const result = await packReseed(name, {
|
|
821
|
+
configPath: options.config,
|
|
822
|
+
dryRun: options.dryRun,
|
|
823
|
+
});
|
|
824
|
+
if (result.fieldsChanged.length === 0) {
|
|
825
|
+
stdout(`policy_packs entry ${JSON.stringify(result.name)} already matches the shipped template; nothing to reseed.\n`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (options.dryRun) {
|
|
829
|
+
stdout(result.diff);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
stdout(`reseeded ${result.fieldsChanged.map((f) => `config.${f}`).join(", ")} for policy_packs entry ${JSON.stringify(result.name)} in ${result.path}\n`);
|
|
833
|
+
});
|
|
768
834
|
// `harness pack hook` runtime sub-tree (Phase 6 #4): wired by the
|
|
769
835
|
// pack's PreToolUse hook contribution; reads PreToolUse JSON from
|
|
770
836
|
// stdin, consults ledger + persisted-report, emits Claude Code deny
|
|
@@ -873,6 +939,25 @@ export function buildProgram(opts = {}) {
|
|
|
873
939
|
throw new HarnessExitError("", result.exitCode);
|
|
874
940
|
}
|
|
875
941
|
});
|
|
942
|
+
packHookCmd
|
|
943
|
+
.command("codex-post-tool-use")
|
|
944
|
+
.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).")
|
|
945
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
946
|
+
.option("--project <name>", "apply per-project overrides")
|
|
947
|
+
.option("--pack <name>", "pack name to evaluate (default: understanding-before-execution)")
|
|
948
|
+
.option("--reports-dir <path>", "override the persisted-report directory (default: ./.understanding-gate/reports)")
|
|
949
|
+
.action(async (options) => {
|
|
950
|
+
const cliOpts = {};
|
|
951
|
+
if (options.config)
|
|
952
|
+
cliOpts.configPath = options.config;
|
|
953
|
+
if (options.project)
|
|
954
|
+
cliOpts.project = options.project;
|
|
955
|
+
if (options.pack)
|
|
956
|
+
cliOpts.pack = options.pack;
|
|
957
|
+
if (options.reportsDir)
|
|
958
|
+
cliOpts.reportsDir = options.reportsDir;
|
|
959
|
+
await runPackHookCodexPostToolUseCli(cliOpts);
|
|
960
|
+
});
|
|
876
961
|
packHookCmd
|
|
877
962
|
.command("codex-user-prompt-submit")
|
|
878
963
|
.description("Codex UserPromptSubmit injector: emit the Understanding-Gate instruction template on stdout for Codex to prepend to additional_instructions")
|
|
@@ -961,6 +1046,61 @@ export function buildProgram(opts = {}) {
|
|
|
961
1046
|
throw new HarnessExitError("", result.exitCode);
|
|
962
1047
|
}
|
|
963
1048
|
});
|
|
1049
|
+
packHookCmd
|
|
1050
|
+
.command("post-merge-gate-record")
|
|
1051
|
+
.description("PostToolUse producer for the post-merge-gate pack: read tool-event JSON from stdin and, on a " +
|
|
1052
|
+
"`gh pr merge` Bash call whose tool_output.exit_code is the number 0, record a " +
|
|
1053
|
+
"`post-merge-gate:merged:<repo>:<branch>:<sha>` fact to the evidence ledger. blocking:false; every " +
|
|
1054
|
+
"non-match / failure path is a no-op.")
|
|
1055
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1056
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1057
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1058
|
+
.option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
|
|
1059
|
+
.action(async (options) => {
|
|
1060
|
+
const cliOpts = {};
|
|
1061
|
+
if (options.config)
|
|
1062
|
+
cliOpts.configPath = options.config;
|
|
1063
|
+
if (options.project)
|
|
1064
|
+
cliOpts.project = options.project;
|
|
1065
|
+
if (options.cwd)
|
|
1066
|
+
cliOpts.cwd = options.cwd;
|
|
1067
|
+
if (options.ledgerTimeout) {
|
|
1068
|
+
const n = Number.parseInt(options.ledgerTimeout, 10);
|
|
1069
|
+
if (Number.isFinite(n) && n > 0)
|
|
1070
|
+
cliOpts.ledgerTimeoutMs = n;
|
|
1071
|
+
}
|
|
1072
|
+
await runPackHookPostMergeGateRecordCli(cliOpts);
|
|
1073
|
+
});
|
|
1074
|
+
packHookCmd
|
|
1075
|
+
.command("post-merge-gate")
|
|
1076
|
+
.description("PreToolUse blocker for the post-merge-gate pack: read tool-event JSON from stdin, and on a curated " +
|
|
1077
|
+
"history-mutating Bash command (git commit/add/push/merge/rebase/cherry-pick/revert/reset/stash " +
|
|
1078
|
+
"pop|apply, gh pr create/merge) emit a deny envelope when the current branch tip matches a recorded " +
|
|
1079
|
+
"merged-tip fact. An escape allowlist (git switch/checkout/pull/fetch, git branch -d/-D, git stash " +
|
|
1080
|
+
"list/show, any `harness ...` command) is checked first, unconditionally. Fails open when the ledger " +
|
|
1081
|
+
"is unreachable.")
|
|
1082
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1083
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1084
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1085
|
+
.option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
|
|
1086
|
+
.action(async (options) => {
|
|
1087
|
+
const cliOpts = {};
|
|
1088
|
+
if (options.config)
|
|
1089
|
+
cliOpts.configPath = options.config;
|
|
1090
|
+
if (options.project)
|
|
1091
|
+
cliOpts.project = options.project;
|
|
1092
|
+
if (options.cwd)
|
|
1093
|
+
cliOpts.cwd = options.cwd;
|
|
1094
|
+
if (options.ledgerTimeout) {
|
|
1095
|
+
const n = Number.parseInt(options.ledgerTimeout, 10);
|
|
1096
|
+
if (Number.isFinite(n) && n > 0)
|
|
1097
|
+
cliOpts.ledgerTimeoutMs = n;
|
|
1098
|
+
}
|
|
1099
|
+
const result = await runPackHookPostMergeGateCli(cliOpts);
|
|
1100
|
+
if (result.exitCode !== 0) {
|
|
1101
|
+
throw new HarnessExitError("", result.exitCode);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
964
1104
|
packHookCmd
|
|
965
1105
|
.command("runtime-reality")
|
|
966
1106
|
.description("PreToolUse drift gate: read tool-event JSON from stdin, run the operator-configured " +
|
|
@@ -1621,6 +1761,124 @@ export function buildProgram(opts = {}) {
|
|
|
1621
1761
|
cliOpts.stagePendingApproval = writePendingApproval;
|
|
1622
1762
|
await runSessionStartPreflight(cliOpts);
|
|
1623
1763
|
});
|
|
1764
|
+
// Shared by the three `record` verbs' action handlers below: parse
|
|
1765
|
+
// `--ledger-timeout <ms>` into `cliOpts.ledgerTimeoutMs`, and report a
|
|
1766
|
+
// `RecordResult` (print the recorded fact on success; on failure, throw
|
|
1767
|
+
// with the runner's own exit code and an EMPTY message, since the
|
|
1768
|
+
// runner already wrote the reason to stderr itself — throwing the same
|
|
1769
|
+
// text again would double-print it).
|
|
1770
|
+
//
|
|
1771
|
+
// An unparseable / non-positive value used to fall back to the
|
|
1772
|
+
// default silently (review finding, T-004): an operator who typo'd
|
|
1773
|
+
// `--ledger-timeout 5ooo` got the default timeout with zero
|
|
1774
|
+
// diagnostic. Now a malformed value warns once on stderr before
|
|
1775
|
+
// falling back, same "never a silent gap" convention `resolveBase`'s
|
|
1776
|
+
// own degrade path already uses below.
|
|
1777
|
+
const applyLedgerTimeout = (raw, cliOpts) => {
|
|
1778
|
+
if (!raw)
|
|
1779
|
+
return;
|
|
1780
|
+
const n = Number.parseInt(raw, 10);
|
|
1781
|
+
if (Number.isFinite(n) && n > 0) {
|
|
1782
|
+
cliOpts.ledgerTimeoutMs = n;
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
stderr(`harness record: --ledger-timeout ${JSON.stringify(raw)} is not a positive integer; using the default timeout.\n`);
|
|
1786
|
+
};
|
|
1787
|
+
const reportRecordResult = (result) => {
|
|
1788
|
+
if (result.wrote) {
|
|
1789
|
+
stdout(`recorded ${result.content} for session ${result.sessionId}\n`);
|
|
1790
|
+
}
|
|
1791
|
+
if (result.exitCode !== 0) {
|
|
1792
|
+
throw new HarnessExitError("", result.exitCode);
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
// `harness record {review,review-subagent,dogfood}` (task T-001):
|
|
1796
|
+
// evidence-ledger producers for the review-before-merge,
|
|
1797
|
+
// review-subagent-before-pr-create, and dogfood-before-release gate
|
|
1798
|
+
// families (see src/cli/init/templates.ts for the exact policies).
|
|
1799
|
+
// Unlike the `preflight` / `session-start preflight` pair above,
|
|
1800
|
+
// these are NOT hooks: they are invoked deliberately by an agent or
|
|
1801
|
+
// operator, so a failure exits non-zero with a clear stderr message
|
|
1802
|
+
// (written by the runner itself) rather than degrading silently.
|
|
1803
|
+
const recordCmd = program
|
|
1804
|
+
.command("record")
|
|
1805
|
+
.description("Evidence-ledger producers for the review / review-subagent / dogfood gate families. " +
|
|
1806
|
+
"Interactive verbs (not hooks): a failure exits non-zero.");
|
|
1807
|
+
recordCmd
|
|
1808
|
+
.command("review <summary>")
|
|
1809
|
+
.description("Record a review:${PR_NUMBER} + review:${BRANCH} (+ review:${BASE}) fact for the " +
|
|
1810
|
+
"review-before-merge / review-before-merge-bash gates.")
|
|
1811
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1812
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1813
|
+
.requiredOption("--pr <number>", "PR number the review:${PR_NUMBER} tag is namespaced by")
|
|
1814
|
+
.option("--base <branch>", "base branch for the review:${BASE} tag. Default: the remote's default branch read from " +
|
|
1815
|
+
"refs/remotes/origin/HEAD (packed-refs fallback included); omitted with a stderr warning " +
|
|
1816
|
+
"when neither resolves. No `gh` shell-out.")
|
|
1817
|
+
.option("--branch <name>", "explicit branch override for review:${BRANCH} (default: current git branch)")
|
|
1818
|
+
.option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then newest Claude Code transcript)")
|
|
1819
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1820
|
+
.action(async (summary, options) => {
|
|
1821
|
+
const cliOpts = { pr: options.pr, summary };
|
|
1822
|
+
if (options.config)
|
|
1823
|
+
cliOpts.configPath = options.config;
|
|
1824
|
+
if (options.project)
|
|
1825
|
+
cliOpts.project = options.project;
|
|
1826
|
+
if (options.base)
|
|
1827
|
+
cliOpts.base = options.base;
|
|
1828
|
+
if (options.branch)
|
|
1829
|
+
cliOpts.branch = options.branch;
|
|
1830
|
+
if (options.session)
|
|
1831
|
+
cliOpts.session = options.session;
|
|
1832
|
+
applyLedgerTimeout(options.ledgerTimeout, cliOpts);
|
|
1833
|
+
reportRecordResult(await runRecordReview(cliOpts));
|
|
1834
|
+
});
|
|
1835
|
+
recordCmd
|
|
1836
|
+
.command("review-subagent [summary]")
|
|
1837
|
+
.description("Record a review-subagent:${TASK_ID} + review-subagent:${BRANCH} fact for the " +
|
|
1838
|
+
"review-subagent-before-pr-create / review-subagent-before-pr-create-bash gates.")
|
|
1839
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1840
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1841
|
+
.requiredOption("--task <id>", "agent-tasks task id the review-subagent:${TASK_ID} tag is namespaced by")
|
|
1842
|
+
.requiredOption("--verdict <text>", "reviewer verdict recorded in the fact content")
|
|
1843
|
+
.option("--branch <name>", "explicit branch override for review-subagent:${BRANCH} (default: current git branch)")
|
|
1844
|
+
.option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then newest Claude Code transcript)")
|
|
1845
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1846
|
+
.action(async (summary, options) => {
|
|
1847
|
+
const cliOpts = {
|
|
1848
|
+
task: options.task,
|
|
1849
|
+
verdict: options.verdict,
|
|
1850
|
+
};
|
|
1851
|
+
if (options.config)
|
|
1852
|
+
cliOpts.configPath = options.config;
|
|
1853
|
+
if (options.project)
|
|
1854
|
+
cliOpts.project = options.project;
|
|
1855
|
+
if (options.branch)
|
|
1856
|
+
cliOpts.branch = options.branch;
|
|
1857
|
+
if (options.session)
|
|
1858
|
+
cliOpts.session = options.session;
|
|
1859
|
+
if (summary)
|
|
1860
|
+
cliOpts.summary = summary;
|
|
1861
|
+
applyLedgerTimeout(options.ledgerTimeout, cliOpts);
|
|
1862
|
+
reportRecordResult(await runRecordReviewSubagent(cliOpts));
|
|
1863
|
+
});
|
|
1864
|
+
recordCmd
|
|
1865
|
+
.command("dogfood <summary>")
|
|
1866
|
+
.description("Record a dogfood:${SESSION_ID} fact for the dogfood-before-release gate.")
|
|
1867
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1868
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1869
|
+
.option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then newest Claude Code transcript)")
|
|
1870
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1871
|
+
.action(async (summary, options) => {
|
|
1872
|
+
const cliOpts = { summary };
|
|
1873
|
+
if (options.config)
|
|
1874
|
+
cliOpts.configPath = options.config;
|
|
1875
|
+
if (options.project)
|
|
1876
|
+
cliOpts.project = options.project;
|
|
1877
|
+
if (options.session)
|
|
1878
|
+
cliOpts.session = options.session;
|
|
1879
|
+
applyLedgerTimeout(options.ledgerTimeout, cliOpts);
|
|
1880
|
+
reportRecordResult(await runRecordDogfood(cliOpts));
|
|
1881
|
+
});
|
|
1624
1882
|
const sessionStart = program
|
|
1625
1883
|
.command("session-start")
|
|
1626
1884
|
.description("SessionStart hook entrypoints (called by Claude Code via settings.json)");
|
|
@@ -1691,6 +1949,36 @@ export function buildProgram(opts = {}) {
|
|
|
1691
1949
|
}
|
|
1692
1950
|
await runSessionStartBranchCheck(cliOpts);
|
|
1693
1951
|
});
|
|
1952
|
+
sessionStart
|
|
1953
|
+
.command("toolchain-parity")
|
|
1954
|
+
.description("SessionStart producer (opt-in via `toolchain_parity.enabled: true`): writes THIS machine's " +
|
|
1955
|
+
"toolchain snapshot (node version, npm globals, OW-Kit version, MCP server names) to " +
|
|
1956
|
+
"`<machine_state_dir>/<profile>.json`, compares it against every OTHER snapshot file already " +
|
|
1957
|
+
"in that directory, and records a `toolchain-parity:ok` / `toolchain-parity:drift:<n>` fact " +
|
|
1958
|
+
"to the evidence ledger. Purely advisory — never blocking, and never touches a peer's file. " +
|
|
1959
|
+
"Cross-machine transport of the snapshot files is agent-memory-sync's job, not this command's.")
|
|
1960
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1961
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1962
|
+
.option("--session <id>", "explicit session id (overrides stdin event + env)")
|
|
1963
|
+
.option("--cwd <path>", "override cwd resolution (default: stdin event.cwd then process.cwd())")
|
|
1964
|
+
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
1965
|
+
.action(async (options) => {
|
|
1966
|
+
const cliOpts = {};
|
|
1967
|
+
if (options.config)
|
|
1968
|
+
cliOpts.configPath = options.config;
|
|
1969
|
+
if (options.project)
|
|
1970
|
+
cliOpts.project = options.project;
|
|
1971
|
+
if (options.session)
|
|
1972
|
+
cliOpts.session = options.session;
|
|
1973
|
+
if (options.cwd)
|
|
1974
|
+
cliOpts.cwd = options.cwd;
|
|
1975
|
+
if (options.ledgerTimeout) {
|
|
1976
|
+
const n = Number.parseInt(options.ledgerTimeout, 10);
|
|
1977
|
+
if (Number.isFinite(n) && n > 0)
|
|
1978
|
+
cliOpts.ledgerTimeoutMs = n;
|
|
1979
|
+
}
|
|
1980
|
+
await runSessionStartToolchainParity(cliOpts);
|
|
1981
|
+
});
|
|
1694
1982
|
// `harness gate` — operator escape hatch for hard-blocking hooks.
|
|
1695
1983
|
// Task 8fcddb26: the understanding-before-execution PreToolUse hook can
|
|
1696
1984
|
// lock a Claude session out of every Bash call, and the recommended
|
|
@@ -1869,7 +2157,7 @@ export function buildProgram(opts = {}) {
|
|
|
1869
2157
|
if (options.settings)
|
|
1870
2158
|
cliOpts.settingsPath = options.settings;
|
|
1871
2159
|
try {
|
|
1872
|
-
const result = uninstall(cliOpts);
|
|
2160
|
+
const result = await uninstall(cliOpts);
|
|
1873
2161
|
const inv = result.inventory;
|
|
1874
2162
|
if (result.mode === "list") {
|
|
1875
2163
|
const nothing = inv.manifestPath === null &&
|
|
@@ -1878,6 +2166,7 @@ export function buildProgram(opts = {}) {
|
|
|
1878
2166
|
inv.gateStateDir === null &&
|
|
1879
2167
|
inv.hookGroups.length === 0 &&
|
|
1880
2168
|
inv.mcpServers.length === 0 &&
|
|
2169
|
+
inv.mcpRegistryServers.length === 0 &&
|
|
1881
2170
|
inv.preHarnessBackups.length === 0;
|
|
1882
2171
|
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1883
2172
|
? inv.homeDir
|
|
@@ -1907,6 +2196,10 @@ export function buildProgram(opts = {}) {
|
|
|
1907
2196
|
if (inv.mcpServers.length > 0) {
|
|
1908
2197
|
stdout(` mcpServers in ${inv.settingsPath}: ${inv.mcpServers.join(", ")}\n`);
|
|
1909
2198
|
}
|
|
2199
|
+
if (inv.mcpRegistryServers.length > 0) {
|
|
2200
|
+
stdout(` mcpServers registered in the claude CLI user-scope registry (${inv.mcpRegistryPath}): ` +
|
|
2201
|
+
`${inv.mcpRegistryServers.join(", ")}\n`);
|
|
2202
|
+
}
|
|
1910
2203
|
if (inv.preHarnessBackups.length > 0) {
|
|
1911
2204
|
stdout(` pre-harness backups:\n`);
|
|
1912
2205
|
for (const b of inv.preHarnessBackups)
|
|
@@ -1927,6 +2220,11 @@ export function buildProgram(opts = {}) {
|
|
|
1927
2220
|
for (const f of result.removedFiles)
|
|
1928
2221
|
stdout(` ${f}\n`);
|
|
1929
2222
|
}
|
|
2223
|
+
if (result.mcpRegistryRemovals.length > 0) {
|
|
2224
|
+
stdout(`claude mcp remove (user scope, ${inv.mcpRegistryPath}):\n`);
|
|
2225
|
+
for (const r of result.mcpRegistryRemovals)
|
|
2226
|
+
stdout(` ${r.name}: ${r.status}\n`);
|
|
2227
|
+
}
|
|
1930
2228
|
stdout(`\nTo finish: \`npm uninstall -g @lannguyensi/harness\` (uninstall does not touch the npm install).\n`);
|
|
1931
2229
|
for (const w of inv.warnings)
|
|
1932
2230
|
stderr(`warning: ${w}\n`);
|
|
@@ -1966,9 +2264,15 @@ export function buildProgram(opts = {}) {
|
|
|
1966
2264
|
/* state root itself may be gone or unreadable; nothing to report */
|
|
1967
2265
|
}
|
|
1968
2266
|
}
|
|
2267
|
+
if (result.mcpRegistryRemovals.length > 0) {
|
|
2268
|
+
stdout(`claude mcp remove (user scope, ${inv.mcpRegistryPath}):\n`);
|
|
2269
|
+
for (const r of result.mcpRegistryRemovals)
|
|
2270
|
+
stdout(` ${r.name}: ${r.status}\n`);
|
|
2271
|
+
}
|
|
1969
2272
|
if (result.backupPath === null &&
|
|
1970
2273
|
result.snapshotPath === null &&
|
|
1971
|
-
result.removedFiles.length === 0
|
|
2274
|
+
result.removedFiles.length === 0 &&
|
|
2275
|
+
result.mcpRegistryRemovals.length === 0) {
|
|
1972
2276
|
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1973
2277
|
? inv.homeDir
|
|
1974
2278
|
: `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
|
|
@@ -2010,9 +2314,10 @@ export function buildProgram(opts = {}) {
|
|
|
2010
2314
|
program
|
|
2011
2315
|
.command("pause")
|
|
2012
2316
|
.description("Temporarily make all harness hooks dormant by writing a sentinel under harness.generated/. " +
|
|
2013
|
-
"Operator-only (refuses when $CLAUDE_SESSION_ID
|
|
2014
|
-
"lockout recovery, debug A/B-tests, and incident
|
|
2015
|
-
"for permanent per-policy disable, edit
|
|
2317
|
+
"Operator-only (refuses when $CLAUDE_CODE_SESSION_ID, $CLAUDE_SESSION_ID, or $CODEX_SESSION_ID " +
|
|
2318
|
+
"is set, or stdin is non-TTY). Intended for lockout recovery, debug A/B-tests, and incident " +
|
|
2319
|
+
"hotfixes. NOT for routine gate bypass: for permanent per-policy disable, edit " +
|
|
2320
|
+
"`policies[].enabled` in the manifest.")
|
|
2016
2321
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
2017
2322
|
.option("--project <name>", "apply per-project overrides")
|
|
2018
2323
|
.option("--for <duration>", "auto-resume after this duration (e.g. 5m, 1h, PT30S; default: 15m)")
|
|
@@ -2136,6 +2441,18 @@ export async function run(opts = {}) {
|
|
|
2136
2441
|
return 0;
|
|
2137
2442
|
}
|
|
2138
2443
|
catch (err) {
|
|
2444
|
+
// Defense-in-depth (task 325ace29): a hermetic-spawn-guard violation
|
|
2445
|
+
// must always propagate out of run() as a hard failure, never be
|
|
2446
|
+
// folded into the generic "return 70" branch below. Without this,
|
|
2447
|
+
// any test that merely asserts "exit code != 0" (rather than the
|
|
2448
|
+
// specific message) would mask a future real spawn slipping past a
|
|
2449
|
+
// guarded call site. This is production-neutral: the guard
|
|
2450
|
+
// (src/runtime/hermetic-spawn-guard.ts) only ever throws when
|
|
2451
|
+
// `process.env.VITEST` is set, which a real, standalone `harness`
|
|
2452
|
+
// invocation never has — so this branch is unreachable outside of
|
|
2453
|
+
// vitest and changes no production behavior.
|
|
2454
|
+
if (err instanceof HermeticSpawnViolationError)
|
|
2455
|
+
throw err;
|
|
2139
2456
|
if (err instanceof HarnessExitError) {
|
|
2140
2457
|
if (err.exitCode !== 0 && err.message)
|
|
2141
2458
|
stderr(`${err.message}\n`);
|