@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +9 -0
  3. package/dist/cli/adopt/derive.d.ts +15 -0
  4. package/dist/cli/adopt/derive.js +34 -7
  5. package/dist/cli/adopt/derive.js.map +1 -1
  6. package/dist/cli/adopt/index.d.ts +30 -0
  7. package/dist/cli/adopt/index.js +23 -3
  8. package/dist/cli/adopt/index.js.map +1 -1
  9. package/dist/cli/apply/generate-settings.d.ts +14 -1
  10. package/dist/cli/apply/generate-settings.js +7 -3
  11. package/dist/cli/apply/generate-settings.js.map +1 -1
  12. package/dist/cli/approve/stdin-report.js +2 -0
  13. package/dist/cli/approve/stdin-report.js.map +1 -1
  14. package/dist/cli/doctor/claude-mcp.d.ts +50 -0
  15. package/dist/cli/doctor/claude-mcp.js +170 -0
  16. package/dist/cli/doctor/claude-mcp.js.map +1 -0
  17. package/dist/cli/doctor/format.js +37 -0
  18. package/dist/cli/doctor/format.js.map +1 -1
  19. package/dist/cli/doctor/index.d.ts +10 -0
  20. package/dist/cli/doctor/index.js +24 -0
  21. package/dist/cli/doctor/index.js.map +1 -1
  22. package/dist/cli/doctor/npm-bin-path.js +42 -0
  23. package/dist/cli/doctor/npm-bin-path.js.map +1 -1
  24. package/dist/cli/doctor/types.d.ts +11 -0
  25. package/dist/cli/doctor/types.js.map +1 -1
  26. package/dist/cli/index.js +253 -5
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/init/agent-tasks-auth.js +32 -0
  29. package/dist/cli/init/agent-tasks-auth.js.map +1 -1
  30. package/dist/cli/init/composer.js +3 -9
  31. package/dist/cli/init/composer.js.map +1 -1
  32. package/dist/cli/init/dependencies.js +21 -0
  33. package/dist/cli/init/dependencies.js.map +1 -1
  34. package/dist/cli/init/detect.d.ts +20 -3
  35. package/dist/cli/init/detect.js +34 -36
  36. package/dist/cli/init/detect.js.map +1 -1
  37. package/dist/cli/init/interactive.d.ts +33 -0
  38. package/dist/cli/init/interactive.js +399 -5
  39. package/dist/cli/init/interactive.js.map +1 -1
  40. package/dist/cli/init/profiles.d.ts +1 -1
  41. package/dist/cli/init/profiles.js +1 -1
  42. package/dist/cli/init/templates.d.ts +1 -1
  43. package/dist/cli/init/templates.js +64 -5
  44. package/dist/cli/init/templates.js.map +1 -1
  45. package/dist/cli/main.js +10 -0
  46. package/dist/cli/main.js.map +1 -1
  47. package/dist/cli/pack/hook-post-merge-gate-record.d.ts +30 -0
  48. package/dist/cli/pack/hook-post-merge-gate-record.js +153 -0
  49. package/dist/cli/pack/hook-post-merge-gate-record.js.map +1 -0
  50. package/dist/cli/pack/hook-post-merge-gate.d.ts +28 -0
  51. package/dist/cli/pack/hook-post-merge-gate.js +244 -0
  52. package/dist/cli/pack/hook-post-merge-gate.js.map +1 -0
  53. package/dist/cli/pack/hook-solution-acceptance-writeguard.js +148 -2
  54. package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
  55. package/dist/cli/pack/understanding-report-schema-hint.js +1 -1
  56. package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
  57. package/dist/cli/record/index.d.ts +64 -0
  58. package/dist/cli/record/index.js +211 -0
  59. package/dist/cli/record/index.js.map +1 -0
  60. package/dist/cli/session-start/index.d.ts +7 -0
  61. package/dist/cli/session-start/index.js +133 -38
  62. package/dist/cli/session-start/index.js.map +1 -1
  63. package/dist/cli/session-start/toolchain-parity.d.ts +149 -0
  64. package/dist/cli/session-start/toolchain-parity.js +618 -0
  65. package/dist/cli/session-start/toolchain-parity.js.map +1 -0
  66. package/dist/cli/uninstall/index.d.ts +73 -1
  67. package/dist/cli/uninstall/index.js +134 -2
  68. package/dist/cli/uninstall/index.js.map +1 -1
  69. package/dist/io/claude-mcp.d.ts +258 -0
  70. package/dist/io/claude-mcp.js +544 -0
  71. package/dist/io/claude-mcp.js.map +1 -0
  72. package/dist/policies/ledger-client.d.ts +34 -1
  73. package/dist/policies/ledger-client.js +21 -16
  74. package/dist/policies/ledger-client.js.map +1 -1
  75. package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +195 -0
  76. package/dist/policy-packs/builtin/post-merge-gate-runtime.js +290 -0
  77. package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -0
  78. package/dist/policy-packs/builtin/post-merge-gate.d.ts +56 -0
  79. package/dist/policy-packs/builtin/post-merge-gate.js +291 -0
  80. package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -0
  81. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +28 -13
  82. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
  83. package/dist/policy-packs/registry.d.ts +1 -1
  84. package/dist/policy-packs/registry.js +12 -0
  85. package/dist/policy-packs/registry.js.map +1 -1
  86. package/dist/runtime/git-context.d.ts +50 -0
  87. package/dist/runtime/git-context.js +108 -1
  88. package/dist/runtime/git-context.js.map +1 -1
  89. package/dist/runtime/hermetic-spawn-guard.d.ts +25 -0
  90. package/dist/runtime/hermetic-spawn-guard.js +136 -0
  91. package/dist/runtime/hermetic-spawn-guard.js.map +1 -0
  92. package/dist/runtime/ledger-writer.d.ts +27 -0
  93. package/dist/runtime/ledger-writer.js +46 -0
  94. package/dist/runtime/ledger-writer.js.map +1 -0
  95. package/dist/runtime/read-only-bash.js +118 -0
  96. package/dist/runtime/read-only-bash.js.map +1 -1
  97. package/dist/schema/index.d.ts +41 -0
  98. package/dist/schema/index.js +5 -0
  99. package/dist/schema/index.js.map +1 -1
  100. package/dist/schema/toolchain-parity.d.ts +18 -0
  101. package/dist/schema/toolchain-parity.js +35 -0
  102. package/dist/schema/toolchain-parity.js.map +1 -0
  103. 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 from a runtime file (today: ~/.claude/settings.json) " +
507
- "into the manifest. Diffs against the manifest's current declarations and " +
508
- "prompts y/N before writing.")
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`);