@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.
Files changed (189) hide show
  1. package/CHANGELOG.md +68 -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/approve/understanding.js +28 -0
  15. package/dist/cli/approve/understanding.js.map +1 -1
  16. package/dist/cli/doctor/claude-mcp.d.ts +50 -0
  17. package/dist/cli/doctor/claude-mcp.js +170 -0
  18. package/dist/cli/doctor/claude-mcp.js.map +1 -0
  19. package/dist/cli/doctor/codex.d.ts +8 -0
  20. package/dist/cli/doctor/codex.js +8 -1
  21. package/dist/cli/doctor/codex.js.map +1 -1
  22. package/dist/cli/doctor/format.js +60 -11
  23. package/dist/cli/doctor/format.js.map +1 -1
  24. package/dist/cli/doctor/index.d.ts +46 -0
  25. package/dist/cli/doctor/index.js +155 -29
  26. package/dist/cli/doctor/index.js.map +1 -1
  27. package/dist/cli/doctor/npm-bin-path.js +42 -0
  28. package/dist/cli/doctor/npm-bin-path.js.map +1 -1
  29. package/dist/cli/doctor/types.d.ts +37 -0
  30. package/dist/cli/doctor/types.js.map +1 -1
  31. package/dist/cli/dry-run.js +5 -0
  32. package/dist/cli/dry-run.js.map +1 -1
  33. package/dist/cli/explain.js +16 -3
  34. package/dist/cli/explain.js.map +1 -1
  35. package/dist/cli/index.js +328 -11
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/cli/init/agent-tasks-auth.js +32 -0
  38. package/dist/cli/init/agent-tasks-auth.js.map +1 -1
  39. package/dist/cli/init/composer.js +13 -40
  40. package/dist/cli/init/composer.js.map +1 -1
  41. package/dist/cli/init/dependencies.js +21 -0
  42. package/dist/cli/init/dependencies.js.map +1 -1
  43. package/dist/cli/init/detect.d.ts +20 -3
  44. package/dist/cli/init/detect.js +34 -36
  45. package/dist/cli/init/detect.js.map +1 -1
  46. package/dist/cli/init/index.d.ts +18 -0
  47. package/dist/cli/init/index.js +28 -2
  48. package/dist/cli/init/index.js.map +1 -1
  49. package/dist/cli/init/interactive.d.ts +41 -0
  50. package/dist/cli/init/interactive.js +421 -6
  51. package/dist/cli/init/interactive.js.map +1 -1
  52. package/dist/cli/init/profiles.d.ts +2 -2
  53. package/dist/cli/init/profiles.js +10 -1
  54. package/dist/cli/init/profiles.js.map +1 -1
  55. package/dist/cli/init/templates.d.ts +1 -1
  56. package/dist/cli/init/templates.js +217 -8
  57. package/dist/cli/init/templates.js.map +1 -1
  58. package/dist/cli/list.js +3 -1
  59. package/dist/cli/list.js.map +1 -1
  60. package/dist/cli/main.js +10 -0
  61. package/dist/cli/main.js.map +1 -1
  62. package/dist/cli/pack/hook-bootstrap.d.ts +24 -0
  63. package/dist/cli/pack/hook-bootstrap.js +47 -1
  64. package/dist/cli/pack/hook-bootstrap.js.map +1 -1
  65. package/dist/cli/pack/hook-branch-protection.js +9 -1
  66. package/dist/cli/pack/hook-branch-protection.js.map +1 -1
  67. package/dist/cli/pack/hook-codex-post-tool-use.d.ts +25 -0
  68. package/dist/cli/pack/hook-codex-post-tool-use.js +160 -0
  69. package/dist/cli/pack/hook-codex-post-tool-use.js.map +1 -0
  70. package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +1 -0
  71. package/dist/cli/pack/hook-codex-pre-tool-use.js +43 -10
  72. package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
  73. package/dist/cli/pack/hook-codex-stop.js +1 -8
  74. package/dist/cli/pack/hook-codex-stop.js.map +1 -1
  75. package/dist/cli/pack/hook-post-merge-gate-record.d.ts +30 -0
  76. package/dist/cli/pack/hook-post-merge-gate-record.js +153 -0
  77. package/dist/cli/pack/hook-post-merge-gate-record.js.map +1 -0
  78. package/dist/cli/pack/hook-post-merge-gate.d.ts +28 -0
  79. package/dist/cli/pack/hook-post-merge-gate.js +244 -0
  80. package/dist/cli/pack/hook-post-merge-gate.js.map +1 -0
  81. package/dist/cli/pack/hook-post-tool-use.js +16 -131
  82. package/dist/cli/pack/hook-post-tool-use.js.map +1 -1
  83. package/dist/cli/pack/hook-pre-tool-use.js +54 -2
  84. package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
  85. package/dist/cli/pack/hook-solution-acceptance-writeguard.js +148 -2
  86. package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
  87. package/dist/cli/pack/hook-stay-in-scope.js +63 -20
  88. package/dist/cli/pack/hook-stay-in-scope.js.map +1 -1
  89. package/dist/cli/pack/hook-track-active-claim.js +33 -9
  90. package/dist/cli/pack/hook-track-active-claim.js.map +1 -1
  91. package/dist/cli/pack/index.d.ts +2 -1
  92. package/dist/cli/pack/index.js +2 -1
  93. package/dist/cli/pack/index.js.map +1 -1
  94. package/dist/cli/pack/mutate.d.ts +19 -0
  95. package/dist/cli/pack/mutate.js +38 -0
  96. package/dist/cli/pack/mutate.js.map +1 -1
  97. package/dist/cli/pack/reseed.d.ts +19 -0
  98. package/dist/cli/pack/reseed.js +118 -0
  99. package/dist/cli/pack/reseed.js.map +1 -0
  100. package/dist/cli/pack/understanding-report-schema-hint.js +1 -1
  101. package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
  102. package/dist/cli/pause/index.js +12 -3
  103. package/dist/cli/pause/index.js.map +1 -1
  104. package/dist/cli/record/index.d.ts +64 -0
  105. package/dist/cli/record/index.js +211 -0
  106. package/dist/cli/record/index.js.map +1 -0
  107. package/dist/cli/session-start/index.d.ts +21 -0
  108. package/dist/cli/session-start/index.js +160 -25
  109. package/dist/cli/session-start/index.js.map +1 -1
  110. package/dist/cli/session-start/toolchain-parity.d.ts +149 -0
  111. package/dist/cli/session-start/toolchain-parity.js +618 -0
  112. package/dist/cli/session-start/toolchain-parity.js.map +1 -0
  113. package/dist/cli/uninstall/index.d.ts +73 -1
  114. package/dist/cli/uninstall/index.js +134 -2
  115. package/dist/cli/uninstall/index.js.map +1 -1
  116. package/dist/cli/validate/checks.js +8 -0
  117. package/dist/cli/validate/checks.js.map +1 -1
  118. package/dist/io/claude-mcp.d.ts +258 -0
  119. package/dist/io/claude-mcp.js +544 -0
  120. package/dist/io/claude-mcp.js.map +1 -0
  121. package/dist/policies/ledger-client.d.ts +34 -1
  122. package/dist/policies/ledger-client.js +21 -16
  123. package/dist/policies/ledger-client.js.map +1 -1
  124. package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -2
  125. package/dist/policy-packs/builtin/branch-protection-runtime.js +4 -2
  126. package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
  127. package/dist/policy-packs/builtin/branch-protection.d.ts +10 -1
  128. package/dist/policy-packs/builtin/branch-protection.js +17 -0
  129. package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
  130. package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +195 -0
  131. package/dist/policy-packs/builtin/post-merge-gate-runtime.js +290 -0
  132. package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -0
  133. package/dist/policy-packs/builtin/post-merge-gate.d.ts +56 -0
  134. package/dist/policy-packs/builtin/post-merge-gate.js +291 -0
  135. package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -0
  136. package/dist/policy-packs/builtin/solution-acceptance-runtime.js +28 -13
  137. package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
  138. package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +197 -24
  139. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +342 -40
  140. package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
  141. package/dist/policy-packs/builtin/understanding-before-execution.d.ts +26 -1
  142. package/dist/policy-packs/builtin/understanding-before-execution.js +302 -17
  143. package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
  144. package/dist/policy-packs/registry.d.ts +23 -2
  145. package/dist/policy-packs/registry.js +31 -2
  146. package/dist/policy-packs/registry.js.map +1 -1
  147. package/dist/policy-packs/ux-compare.d.ts +14 -0
  148. package/dist/policy-packs/ux-compare.js +58 -0
  149. package/dist/policy-packs/ux-compare.js.map +1 -0
  150. package/dist/policy-packs/ux-drift-check.d.ts +27 -0
  151. package/dist/policy-packs/ux-drift-check.js +78 -0
  152. package/dist/policy-packs/ux-drift-check.js.map +1 -0
  153. package/dist/probes/mcp.d.ts +13 -0
  154. package/dist/probes/mcp.js +57 -0
  155. package/dist/probes/mcp.js.map +1 -1
  156. package/dist/runtime/approval-signing.d.ts +82 -0
  157. package/dist/runtime/approval-signing.js +275 -0
  158. package/dist/runtime/approval-signing.js.map +1 -0
  159. package/dist/runtime/git-context.d.ts +50 -0
  160. package/dist/runtime/git-context.js +108 -1
  161. package/dist/runtime/git-context.js.map +1 -1
  162. package/dist/runtime/hermetic-spawn-guard.d.ts +25 -0
  163. package/dist/runtime/hermetic-spawn-guard.js +136 -0
  164. package/dist/runtime/hermetic-spawn-guard.js.map +1 -0
  165. package/dist/runtime/intercept.js +62 -5
  166. package/dist/runtime/intercept.js.map +1 -1
  167. package/dist/runtime/ledger-writer.d.ts +27 -0
  168. package/dist/runtime/ledger-writer.js +46 -0
  169. package/dist/runtime/ledger-writer.js.map +1 -0
  170. package/dist/runtime/pause-sentinel.js +14 -3
  171. package/dist/runtime/pause-sentinel.js.map +1 -1
  172. package/dist/runtime/read-only-bash.js +118 -0
  173. package/dist/runtime/read-only-bash.js.map +1 -1
  174. package/dist/runtime/recovery-git-commit.d.ts +22 -0
  175. package/dist/runtime/recovery-git-commit.js +284 -0
  176. package/dist/runtime/recovery-git-commit.js.map +1 -0
  177. package/dist/runtime/tool-name-aliases.d.ts +1 -0
  178. package/dist/runtime/tool-name-aliases.js +7 -1
  179. package/dist/runtime/tool-name-aliases.js.map +1 -1
  180. package/dist/schema/index.d.ts +94 -42
  181. package/dist/schema/index.js +5 -0
  182. package/dist/schema/index.js.map +1 -1
  183. package/dist/schema/policies.d.ts +56 -44
  184. package/dist/schema/policies.js +79 -9
  185. package/dist/schema/policies.js.map +1 -1
  186. package/dist/schema/toolchain-parity.d.ts +18 -0
  187. package/dist/schema/toolchain-parity.js +35 -0
  188. package/dist/schema/toolchain-parity.js.map +1 -0
  189. 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 from a runtime file (today: ~/.claude/settings.json) " +
486
- "into the manifest. Diffs against the manifest's current declarations and " +
487
- "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.")
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 is set or stdin is non-TTY). Intended for " +
2014
- "lockout recovery, debug A/B-tests, and incident hotfixes. NOT for routine gate bypass: " +
2015
- "for permanent per-policy disable, edit `policies[].enabled` in the manifest.")
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`);