@lannguyensi/harness 0.41.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 +31 -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/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/format.js +37 -0
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +10 -0
- package/dist/cli/doctor/index.js +24 -0
- 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 +11 -0
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/index.js +253 -5
- 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 +3 -9
- 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/interactive.d.ts +33 -0
- package/dist/cli/init/interactive.js +399 -5
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +1 -1
- package/dist/cli/init/profiles.js +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +64 -5
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.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-solution-acceptance-writeguard.js +148 -2
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- 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/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 +7 -0
- package/dist/cli/session-start/index.js +133 -38
- 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/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/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/registry.d.ts +1 -1
- package/dist/policy-packs/registry.js +12 -0
- package/dist/policy-packs/registry.js.map +1 -1
- 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/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/read-only-bash.js +118 -0
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/schema/index.d.ts +41 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.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`.
|
|
@@ -40,6 +41,7 @@ import { approveBranchProtection } from "./approve/branch-protection.js";
|
|
|
40
41
|
import { approveRisk } from "./approve/risk.js";
|
|
41
42
|
import { approveUnderstanding } from "./approve/understanding.js";
|
|
42
43
|
import { readPipedStdin } from "./approve/stdin-report.js";
|
|
44
|
+
import { runRecordDogfood, runRecordReview, runRecordReviewSubagent, } from "./record/index.js";
|
|
43
45
|
import { describe, isPillar } from "./describe.js";
|
|
44
46
|
import { diff as diffRun } from "./diff/index.js";
|
|
45
47
|
import { diffSinceApply } from "./diff/since-apply.js";
|
|
@@ -64,9 +66,12 @@ import { runInterceptCli } from "./policy/intercept.js";
|
|
|
64
66
|
import { runSessionStartPreflight } from "./session-start/index.js";
|
|
65
67
|
import { writePendingApproval } from "../runtime/pending-approval.js";
|
|
66
68
|
import { runSessionStartBranchCheck } from "./session-start/branch-check.js";
|
|
69
|
+
import { runSessionStartToolchainParity } from "./session-start/toolchain-parity.js";
|
|
67
70
|
import { runPackHookBranchProtectionCli } from "./pack/hook-branch-protection.js";
|
|
68
71
|
import { runPackHookSolutionAcceptanceCli } from "./pack/hook-solution-acceptance.js";
|
|
69
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";
|
|
70
75
|
import { runPackHookRuntimeRealityCli } from "./pack/hook-runtime-reality.js";
|
|
71
76
|
import { gateDisable, GateDisableError } from "./gate/disable.js";
|
|
72
77
|
import { gateEnable, GateEnableError } from "./gate/enable.js";
|
|
@@ -503,13 +508,25 @@ export function buildProgram(opts = {}) {
|
|
|
503
508
|
});
|
|
504
509
|
program
|
|
505
510
|
.command("adopt <file>")
|
|
506
|
-
.description("Capture hand-edits
|
|
507
|
-
"
|
|
508
|
-
"
|
|
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.")
|
|
509
517
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
510
518
|
.option("--yes", "skip the confirmation prompt (for non-interactive use)")
|
|
511
519
|
.action(async (file, options) => {
|
|
512
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
|
+
}
|
|
513
530
|
if (result.outcome === "no-drift") {
|
|
514
531
|
stdout(`nothing to adopt (no drift between ${file} and ${result.manifestPath})\n`);
|
|
515
532
|
return;
|
|
@@ -1029,6 +1046,61 @@ export function buildProgram(opts = {}) {
|
|
|
1029
1046
|
throw new HarnessExitError("", result.exitCode);
|
|
1030
1047
|
}
|
|
1031
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
|
+
});
|
|
1032
1104
|
packHookCmd
|
|
1033
1105
|
.command("runtime-reality")
|
|
1034
1106
|
.description("PreToolUse drift gate: read tool-event JSON from stdin, run the operator-configured " +
|
|
@@ -1689,6 +1761,124 @@ export function buildProgram(opts = {}) {
|
|
|
1689
1761
|
cliOpts.stagePendingApproval = writePendingApproval;
|
|
1690
1762
|
await runSessionStartPreflight(cliOpts);
|
|
1691
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
|
+
});
|
|
1692
1882
|
const sessionStart = program
|
|
1693
1883
|
.command("session-start")
|
|
1694
1884
|
.description("SessionStart hook entrypoints (called by Claude Code via settings.json)");
|
|
@@ -1759,6 +1949,36 @@ export function buildProgram(opts = {}) {
|
|
|
1759
1949
|
}
|
|
1760
1950
|
await runSessionStartBranchCheck(cliOpts);
|
|
1761
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
|
+
});
|
|
1762
1982
|
// `harness gate` — operator escape hatch for hard-blocking hooks.
|
|
1763
1983
|
// Task 8fcddb26: the understanding-before-execution PreToolUse hook can
|
|
1764
1984
|
// lock a Claude session out of every Bash call, and the recommended
|
|
@@ -1937,7 +2157,7 @@ export function buildProgram(opts = {}) {
|
|
|
1937
2157
|
if (options.settings)
|
|
1938
2158
|
cliOpts.settingsPath = options.settings;
|
|
1939
2159
|
try {
|
|
1940
|
-
const result = uninstall(cliOpts);
|
|
2160
|
+
const result = await uninstall(cliOpts);
|
|
1941
2161
|
const inv = result.inventory;
|
|
1942
2162
|
if (result.mode === "list") {
|
|
1943
2163
|
const nothing = inv.manifestPath === null &&
|
|
@@ -1946,6 +2166,7 @@ export function buildProgram(opts = {}) {
|
|
|
1946
2166
|
inv.gateStateDir === null &&
|
|
1947
2167
|
inv.hookGroups.length === 0 &&
|
|
1948
2168
|
inv.mcpServers.length === 0 &&
|
|
2169
|
+
inv.mcpRegistryServers.length === 0 &&
|
|
1949
2170
|
inv.preHarnessBackups.length === 0;
|
|
1950
2171
|
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1951
2172
|
? inv.homeDir
|
|
@@ -1975,6 +2196,10 @@ export function buildProgram(opts = {}) {
|
|
|
1975
2196
|
if (inv.mcpServers.length > 0) {
|
|
1976
2197
|
stdout(` mcpServers in ${inv.settingsPath}: ${inv.mcpServers.join(", ")}\n`);
|
|
1977
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
|
+
}
|
|
1978
2203
|
if (inv.preHarnessBackups.length > 0) {
|
|
1979
2204
|
stdout(` pre-harness backups:\n`);
|
|
1980
2205
|
for (const b of inv.preHarnessBackups)
|
|
@@ -1995,6 +2220,11 @@ export function buildProgram(opts = {}) {
|
|
|
1995
2220
|
for (const f of result.removedFiles)
|
|
1996
2221
|
stdout(` ${f}\n`);
|
|
1997
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
|
+
}
|
|
1998
2228
|
stdout(`\nTo finish: \`npm uninstall -g @lannguyensi/harness\` (uninstall does not touch the npm install).\n`);
|
|
1999
2229
|
for (const w of inv.warnings)
|
|
2000
2230
|
stderr(`warning: ${w}\n`);
|
|
@@ -2034,9 +2264,15 @@ export function buildProgram(opts = {}) {
|
|
|
2034
2264
|
/* state root itself may be gone or unreadable; nothing to report */
|
|
2035
2265
|
}
|
|
2036
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
|
+
}
|
|
2037
2272
|
if (result.backupPath === null &&
|
|
2038
2273
|
result.snapshotPath === null &&
|
|
2039
|
-
result.removedFiles.length === 0
|
|
2274
|
+
result.removedFiles.length === 0 &&
|
|
2275
|
+
result.mcpRegistryRemovals.length === 0) {
|
|
2040
2276
|
const rootsLabel = inv.stateDir === inv.homeDir
|
|
2041
2277
|
? inv.homeDir
|
|
2042
2278
|
: `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
|
|
@@ -2205,6 +2441,18 @@ export async function run(opts = {}) {
|
|
|
2205
2441
|
return 0;
|
|
2206
2442
|
}
|
|
2207
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;
|
|
2208
2456
|
if (err instanceof HarnessExitError) {
|
|
2209
2457
|
if (err.exitCode !== 0 && err.message)
|
|
2210
2458
|
stderr(`${err.message}\n`);
|