@lannguyensi/harness 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +9 -0
- package/dist/cli/adopt/derive.d.ts +15 -0
- package/dist/cli/adopt/derive.js +34 -7
- package/dist/cli/adopt/derive.js.map +1 -1
- package/dist/cli/adopt/index.d.ts +30 -0
- package/dist/cli/adopt/index.js +23 -3
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/generate-settings.d.ts +14 -1
- package/dist/cli/apply/generate-settings.js +7 -3
- package/dist/cli/apply/generate-settings.js.map +1 -1
- package/dist/cli/approve/stdin-report.js +2 -0
- package/dist/cli/approve/stdin-report.js.map +1 -1
- package/dist/cli/approve/understanding.js +28 -0
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/doctor/claude-mcp.d.ts +50 -0
- package/dist/cli/doctor/claude-mcp.js +170 -0
- package/dist/cli/doctor/claude-mcp.js.map +1 -0
- package/dist/cli/doctor/codex.d.ts +8 -0
- package/dist/cli/doctor/codex.js +8 -1
- package/dist/cli/doctor/codex.js.map +1 -1
- package/dist/cli/doctor/format.js +60 -11
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +46 -0
- package/dist/cli/doctor/index.js +155 -29
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/npm-bin-path.js +42 -0
- package/dist/cli/doctor/npm-bin-path.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +37 -0
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/dry-run.js +5 -0
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/explain.js +16 -3
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/index.js +328 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/agent-tasks-auth.js +32 -0
- package/dist/cli/init/agent-tasks-auth.js.map +1 -1
- package/dist/cli/init/composer.js +13 -40
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/dependencies.js +21 -0
- package/dist/cli/init/dependencies.js.map +1 -1
- package/dist/cli/init/detect.d.ts +20 -3
- package/dist/cli/init/detect.js +34 -36
- package/dist/cli/init/detect.js.map +1 -1
- package/dist/cli/init/index.d.ts +18 -0
- package/dist/cli/init/index.js +28 -2
- package/dist/cli/init/index.js.map +1 -1
- package/dist/cli/init/interactive.d.ts +41 -0
- package/dist/cli/init/interactive.js +421 -6
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +10 -1
- package/dist/cli/init/profiles.js.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +217 -8
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/list.js +3 -1
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/pack/hook-bootstrap.d.ts +24 -0
- package/dist/cli/pack/hook-bootstrap.js +47 -1
- package/dist/cli/pack/hook-bootstrap.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +9 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-codex-post-tool-use.d.ts +25 -0
- package/dist/cli/pack/hook-codex-post-tool-use.js +160 -0
- package/dist/cli/pack/hook-codex-post-tool-use.js.map +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +43 -10
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-codex-stop.js +1 -8
- package/dist/cli/pack/hook-codex-stop.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate-record.d.ts +30 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js +153 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js.map +1 -0
- package/dist/cli/pack/hook-post-merge-gate.d.ts +28 -0
- package/dist/cli/pack/hook-post-merge-gate.js +244 -0
- package/dist/cli/pack/hook-post-merge-gate.js.map +1 -0
- package/dist/cli/pack/hook-post-tool-use.js +16 -131
- package/dist/cli/pack/hook-post-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +54 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +148 -2
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-stay-in-scope.js +63 -20
- package/dist/cli/pack/hook-stay-in-scope.js.map +1 -1
- package/dist/cli/pack/hook-track-active-claim.js +33 -9
- package/dist/cli/pack/hook-track-active-claim.js.map +1 -1
- package/dist/cli/pack/index.d.ts +2 -1
- package/dist/cli/pack/index.js +2 -1
- package/dist/cli/pack/index.js.map +1 -1
- package/dist/cli/pack/mutate.d.ts +19 -0
- package/dist/cli/pack/mutate.js +38 -0
- package/dist/cli/pack/mutate.js.map +1 -1
- package/dist/cli/pack/reseed.d.ts +19 -0
- package/dist/cli/pack/reseed.js +118 -0
- package/dist/cli/pack/reseed.js.map +1 -0
- package/dist/cli/pack/understanding-report-schema-hint.js +1 -1
- package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
- package/dist/cli/pause/index.js +12 -3
- package/dist/cli/pause/index.js.map +1 -1
- package/dist/cli/record/index.d.ts +64 -0
- package/dist/cli/record/index.js +211 -0
- package/dist/cli/record/index.js.map +1 -0
- package/dist/cli/session-start/index.d.ts +21 -0
- package/dist/cli/session-start/index.js +160 -25
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/session-start/toolchain-parity.d.ts +149 -0
- package/dist/cli/session-start/toolchain-parity.js +618 -0
- package/dist/cli/session-start/toolchain-parity.js.map +1 -0
- package/dist/cli/uninstall/index.d.ts +73 -1
- package/dist/cli/uninstall/index.js +134 -2
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/cli/validate/checks.js +8 -0
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/io/claude-mcp.d.ts +258 -0
- package/dist/io/claude-mcp.js +544 -0
- package/dist/io/claude-mcp.js.map +1 -0
- package/dist/policies/ledger-client.d.ts +34 -1
- package/dist/policies/ledger-client.js +21 -16
- package/dist/policies/ledger-client.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -2
- package/dist/policy-packs/builtin/branch-protection-runtime.js +4 -2
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.d.ts +10 -1
- package/dist/policy-packs/builtin/branch-protection.js +17 -0
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +195 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js +290 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -0
- package/dist/policy-packs/builtin/post-merge-gate.d.ts +56 -0
- package/dist/policy-packs/builtin/post-merge-gate.js +291 -0
- package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +28 -13
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +197 -24
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +342 -40
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +26 -1
- package/dist/policy-packs/builtin/understanding-before-execution.js +302 -17
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/policy-packs/registry.d.ts +23 -2
- package/dist/policy-packs/registry.js +31 -2
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/policy-packs/ux-compare.d.ts +14 -0
- package/dist/policy-packs/ux-compare.js +58 -0
- package/dist/policy-packs/ux-compare.js.map +1 -0
- package/dist/policy-packs/ux-drift-check.d.ts +27 -0
- package/dist/policy-packs/ux-drift-check.js +78 -0
- package/dist/policy-packs/ux-drift-check.js.map +1 -0
- package/dist/probes/mcp.d.ts +13 -0
- package/dist/probes/mcp.js +57 -0
- package/dist/probes/mcp.js.map +1 -1
- package/dist/runtime/approval-signing.d.ts +82 -0
- package/dist/runtime/approval-signing.js +275 -0
- package/dist/runtime/approval-signing.js.map +1 -0
- package/dist/runtime/git-context.d.ts +50 -0
- package/dist/runtime/git-context.js +108 -1
- package/dist/runtime/git-context.js.map +1 -1
- package/dist/runtime/hermetic-spawn-guard.d.ts +25 -0
- package/dist/runtime/hermetic-spawn-guard.js +136 -0
- package/dist/runtime/hermetic-spawn-guard.js.map +1 -0
- package/dist/runtime/intercept.js +62 -5
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/ledger-writer.d.ts +27 -0
- package/dist/runtime/ledger-writer.js +46 -0
- package/dist/runtime/ledger-writer.js.map +1 -0
- package/dist/runtime/pause-sentinel.js +14 -3
- package/dist/runtime/pause-sentinel.js.map +1 -1
- package/dist/runtime/read-only-bash.js +118 -0
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/runtime/recovery-git-commit.d.ts +22 -0
- package/dist/runtime/recovery-git-commit.js +284 -0
- package/dist/runtime/recovery-git-commit.js.map +1 -0
- package/dist/runtime/tool-name-aliases.d.ts +1 -0
- package/dist/runtime/tool-name-aliases.js +7 -1
- package/dist/runtime/tool-name-aliases.js.map +1 -1
- package/dist/schema/index.d.ts +94 -42
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/policies.d.ts +56 -44
- package/dist/schema/policies.js +79 -9
- package/dist/schema/policies.js.map +1 -1
- package/dist/schema/toolchain-parity.d.ts +18 -0
- package/dist/schema/toolchain-parity.js +35 -0
- package/dist/schema/toolchain-parity.js.map +1 -0
- package/package.json +4 -2
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
// write + file-lock + post-write validate path is reused. The wizard
|
|
22
22
|
// is essentially a UI for picking the `--template` value.
|
|
23
23
|
import { select, confirm, input, checkbox } from "@inquirer/prompts";
|
|
24
|
+
import * as fs from "node:fs";
|
|
24
25
|
import * as path from "node:path";
|
|
25
26
|
import { spawn } from "node:child_process";
|
|
26
27
|
import { resolveHomeDir } from "../../runtime/home-dir.js";
|
|
28
|
+
import { assertNoRealSpawnInTests, HermeticSpawnViolationError } from "../../runtime/hermetic-spawn-guard.js";
|
|
27
29
|
import { EX_FAIL, HarnessExitError } from "../exit-codes.js";
|
|
28
30
|
import { detect, } from "./detect.js";
|
|
29
31
|
import { init } from "./index.js";
|
|
30
32
|
import { validate } from "../validate/index.js";
|
|
31
|
-
import { apply, CODEX_CONFIG_BASENAME } from "../apply/index.js";
|
|
33
|
+
import { apply, CODEX_CONFIG_BASENAME, SETTINGS_BASENAME } from "../apply/index.js";
|
|
34
|
+
import { buildMcpServers, projectGroundingEnv, } from "../apply/generate-settings.js";
|
|
35
|
+
import { loadManifest } from "../loader.js";
|
|
36
|
+
import { ensureMcpServers, manualRemoveLines, posixSingleQuote, stripOwnedMcpServers, } from "../../io/claude-mcp.js";
|
|
37
|
+
import { atomicWriteFile } from "../../io/atomic-write.js";
|
|
38
|
+
import { resolveGeneratedDir } from "../../io/generated-dir.js";
|
|
39
|
+
import { readLastApply } from "../../io/last-apply.js";
|
|
40
|
+
import { DEFAULT_OWNED_MCP_SERVERS } from "../uninstall/index.js";
|
|
32
41
|
import { checkDependencies, checkDependencyList, dependenciesForCustom, formatDependencyTable, installPackagesGlobally, } from "./dependencies.js";
|
|
33
42
|
import { probeAgentTasksAuth, runBridgeLogin, } from "./agent-tasks-auth.js";
|
|
34
43
|
import { COMPOSABLE_MCPS, COMPOSABLE_PACKS, COMPOSABLE_POLICIES, composeCustom, } from "./composer.js";
|
|
@@ -68,7 +77,7 @@ function summariseDetection(d) {
|
|
|
68
77
|
lines.push(` MCP wired ${d.mcpServers.map((s) => s.name).join(", ")}`);
|
|
69
78
|
}
|
|
70
79
|
else {
|
|
71
|
-
lines.push(` MCP wired (none detected in Claude
|
|
80
|
+
lines.push(` MCP wired (none detected in the Claude Code user-scope registry)`);
|
|
72
81
|
}
|
|
73
82
|
lines.push(` harness v${d.harness.version}`);
|
|
74
83
|
return lines.join("\n");
|
|
@@ -87,6 +96,33 @@ function runtimeIsConfigured(r) {
|
|
|
87
96
|
return false;
|
|
88
97
|
return r.homeExists || r.settingsExists;
|
|
89
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Read `.last-apply` BEFORE `apply()` gets a chance to re-stamp it (task
|
|
101
|
+
* 363a6de0, Reviewer-Finding 2 / D-107). `apply()` overwrites `.last-apply`
|
|
102
|
+
* with the CURRENT manifest's snapshot whenever anything changed —
|
|
103
|
+
* including a combined edit that both removes a `tools.mcp[]` entry AND
|
|
104
|
+
* touches something else (e.g. a hook via a pack) — which would erase the
|
|
105
|
+
* PRE-edit provenance GC's ownership union needs. Malformed JSON or a
|
|
106
|
+
* schema-invalid record (`readLastApply`'s own validation throws for
|
|
107
|
+
* both) degrades to `null` — the same conservative "no snapshot" fallback
|
|
108
|
+
* D-107 already specifies — rather than letting a corrupt file crash
|
|
109
|
+
* wire-now (previously this was read late and only reachable after
|
|
110
|
+
* Ensure succeeded; hoisting it here means a throw would otherwise
|
|
111
|
+
* surface twice, once from the try branch and again from the catch
|
|
112
|
+
* branch's own `wireClaudeMcp` call).
|
|
113
|
+
*/
|
|
114
|
+
function readPriorLastApply(o) {
|
|
115
|
+
const generatedDir = resolveGeneratedDir({
|
|
116
|
+
...(o.homeDir !== undefined ? { homeDir: o.homeDir } : {}),
|
|
117
|
+
manifestPath: o.configPath,
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
return readLastApply(generatedDir);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
90
126
|
async function wireRuntime(o) {
|
|
91
127
|
// Defensive: only claude-code and codex have apply paths in v1. The
|
|
92
128
|
// checkbox UI disables "opencode" until task f34eb233 lands, so this
|
|
@@ -97,6 +133,15 @@ async function wireRuntime(o) {
|
|
|
97
133
|
throw new HarnessExitError(`wireRuntime: ${o.runtime} is not a wirable runtime in this harness build`, EX_FAIL);
|
|
98
134
|
}
|
|
99
135
|
if (o.runtime === "claude-code") {
|
|
136
|
+
// D-107 / Reviewer-Finding 2 (task 363a6de0): capture `.last-apply`'s
|
|
137
|
+
// manifest snapshot BEFORE apply() runs. apply() re-stamps
|
|
138
|
+
// `.last-apply` with the CURRENT (already-edited) manifest whenever
|
|
139
|
+
// anything changed — including a combined edit that both removes a
|
|
140
|
+
// `tools.mcp[]` entry AND touches something else (e.g. a hook via a
|
|
141
|
+
// pack). Reading it only after apply() (the pre-fix ordering) would
|
|
142
|
+
// silently erase the very provenance GC's ownership union needs to
|
|
143
|
+
// see a manifest entry that existed just before THIS run.
|
|
144
|
+
const priorLastApply = readPriorLastApply(o);
|
|
100
145
|
// init's wire-now intent is "wire this freshly written manifest"
|
|
101
146
|
// — the operator already confirmed by ticking claude-code in the
|
|
102
147
|
// wire-now checkbox. A pre-existing drift in
|
|
@@ -147,14 +192,30 @@ async function wireRuntime(o) {
|
|
|
147
192
|
if (!r.targetWritten && !r.targetInSync) {
|
|
148
193
|
outcome.recoveryHint = `harness apply --target ${o.claudeSettingsPath} --merge --overwrite-drift`;
|
|
149
194
|
}
|
|
195
|
+
// T-002: MCP registration below is independent of this hooks/
|
|
196
|
+
// settings.json merge — it goes through the `claude mcp` CLI, not
|
|
197
|
+
// through settings.json. Run it regardless of the merge outcome so
|
|
198
|
+
// a hooks-merge hiccup doesn't also withhold MCP wiring.
|
|
199
|
+
await wireClaudeMcp(o, outcome, priorLastApply);
|
|
150
200
|
return outcome;
|
|
151
201
|
}
|
|
152
202
|
catch (err) {
|
|
203
|
+
// Hermetic guard (task 0d80e969): a real-spawn violation from
|
|
204
|
+
// `wireClaudeMcp`'s FIRST call above (inside this `try`) must
|
|
205
|
+
// never be treated as an ordinary `apply()` failure — this catch
|
|
206
|
+
// otherwise degrades any thrown error to a "Failed to wire ..."
|
|
207
|
+
// warning AND calls `wireClaudeMcp` a SECOND time below, which
|
|
208
|
+
// would both print a misleading message and retry a spawn that
|
|
209
|
+
// must never happen under vitest. Re-throw before any of that.
|
|
210
|
+
if (err instanceof HermeticSpawnViolationError)
|
|
211
|
+
throw err;
|
|
153
212
|
const message = err instanceof Error ? err.message : String(err);
|
|
154
213
|
const recoveryHint = `harness apply --target ${o.claudeSettingsPath} --merge --overwrite-drift`;
|
|
155
214
|
o.stderr(`\nFailed to wire ${o.claudeSettingsPath}: ${message}\n`);
|
|
156
215
|
o.stderr(`Manifest is on disk. To retry the merge manually:\n ${recoveryHint}\n`);
|
|
157
|
-
|
|
216
|
+
const outcome = { runtime: "claude-code", recoveryHint };
|
|
217
|
+
await wireClaudeMcp(o, outcome, priorLastApply);
|
|
218
|
+
return outcome;
|
|
158
219
|
}
|
|
159
220
|
}
|
|
160
221
|
// Codex path: apply --runtime codex emits harness.generated/codex/config.toml.
|
|
@@ -199,6 +260,289 @@ async function wireRuntime(o) {
|
|
|
199
260
|
return { runtime: "codex", recoveryHint };
|
|
200
261
|
}
|
|
201
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Reload the effective manifest and translate `tools.mcp[]` into the
|
|
265
|
+
* server-spec shape the `claude mcp` CLI wants (task
|
|
266
|
+
* init-mcp-wiring-claude-code/T-002). Reuses the exact same
|
|
267
|
+
* `buildMcpServers` + `projectGroundingEnv` functions the settings.json
|
|
268
|
+
* projection used to feed into the (now dead) `mcpServers` block —
|
|
269
|
+
* see generate-settings.ts's `GenerateSettingsResult.mcpServers` doc.
|
|
270
|
+
* `harnessHomeDir` is the harness STATE root (`harnessHomeArg(opts)`),
|
|
271
|
+
* not the operator's `$HOME`; `projectGroundingEnv`'s tilde-expansion is
|
|
272
|
+
* deliberately left on its default (`os.homedir()`) here, mirroring
|
|
273
|
+
* `apply.ts`'s own `buildExpectedFiles` call, which never overrides it
|
|
274
|
+
* either.
|
|
275
|
+
*
|
|
276
|
+
* Returns `null` when the manifest can't be reloaded (should not happen
|
|
277
|
+
* right after a successful `init()`, but a hand-edited/deleted manifest
|
|
278
|
+
* between write and this read is possible); callers degrade gracefully.
|
|
279
|
+
*/
|
|
280
|
+
function loadDesiredMcpServers(configPath, harnessHomeDir) {
|
|
281
|
+
let manifest;
|
|
282
|
+
try {
|
|
283
|
+
manifest = loadManifest({
|
|
284
|
+
configPath,
|
|
285
|
+
...(harnessHomeDir !== undefined ? { homeDir: harnessHomeDir } : {}),
|
|
286
|
+
}).manifest;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const warnings = [];
|
|
292
|
+
const desired = buildMcpServers(manifest.tools.mcp, warnings);
|
|
293
|
+
projectGroundingEnv(manifest, desired);
|
|
294
|
+
return { manifest, desired, warnings };
|
|
295
|
+
}
|
|
296
|
+
/** One `claude mcp add-json --scope user <name> '<json>'` line per desired server, sorted by name. */
|
|
297
|
+
function manualAddJsonLines(desired) {
|
|
298
|
+
return Object.keys(desired)
|
|
299
|
+
.sort()
|
|
300
|
+
.map((name) => `claude mcp add-json --scope user ${name} ${posixSingleQuote(JSON.stringify(desired[name]))}`);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Extract the mcpServers names a PREVIOUS apply wrote into
|
|
304
|
+
* `harness.generated/settings.json`, from `.last-apply`. Pre-T-002
|
|
305
|
+
* harness versions projected `mcpServers` into that file; a leftover
|
|
306
|
+
* record from one of those versions is the provenance signal the
|
|
307
|
+
* migration step (below) needs to safely strip the matching names out of
|
|
308
|
+
* the live (dead) settings.json block without guessing. Mirrors the
|
|
309
|
+
* equivalent snippet in `apply.ts`'s `--target --merge` provenance
|
|
310
|
+
* handling (kept separate rather than shared: apply.ts's version is
|
|
311
|
+
* private to that module and the two call sites read a different record
|
|
312
|
+
* shape's field, not worth a shared export for ~10 lines).
|
|
313
|
+
*/
|
|
314
|
+
function priorGeneratedMcpNames(lastApply) {
|
|
315
|
+
const content = lastApply?.files[SETTINGS_BASENAME]?.content;
|
|
316
|
+
if (content === undefined)
|
|
317
|
+
return [];
|
|
318
|
+
try {
|
|
319
|
+
const prior = JSON.parse(content);
|
|
320
|
+
const mcp = prior["mcpServers"];
|
|
321
|
+
if (mcp !== null && typeof mcp === "object" && !Array.isArray(mcp)) {
|
|
322
|
+
return Object.keys(mcp);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Corrupt .last-apply record: no provenance names from it; the
|
|
327
|
+
// manifest + DEFAULT_OWNED_MCP_SERVERS sets still apply.
|
|
328
|
+
}
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Extract `tools.mcp[]` names from the manifest snapshot `.last-apply`
|
|
333
|
+
* stores (`LastApplyRecord.manifest`, Phase 3 #1) — the effective manifest
|
|
334
|
+
* as of the apply BEFORE this run (see `readPriorLastApply` — the record
|
|
335
|
+
* passed in here is captured before this run's `apply()` re-stamps the
|
|
336
|
+
* file), not necessarily the manifest on disk right now. GC ownership
|
|
337
|
+
* (task 363a6de0, D-107) needs this: a server whose `tools.mcp[]` entry
|
|
338
|
+
* was removed or disabled just before THIS run is still harness-owned,
|
|
339
|
+
* and the CURRENT manifest alone can never surface it — by definition
|
|
340
|
+
* it's gone from there. An older `.last-apply` record (pre-manifest-
|
|
341
|
+
* snapshot, so `.manifest` is absent) or a snapshot that fails to parse
|
|
342
|
+
* both degrade to an EMPTY result — the conservative fallback D-107
|
|
343
|
+
* calls for is to fall back to JUST the current manifest's names (NOT
|
|
344
|
+
* `DEFAULT_OWNED_MCP_SERVERS` — see the D-107 note on `wireClaudeMcp`)
|
|
345
|
+
* rather than guess at this one.
|
|
346
|
+
*/
|
|
347
|
+
function priorManifestMcpNames(lastApply) {
|
|
348
|
+
const content = lastApply?.manifest?.content;
|
|
349
|
+
if (content === undefined)
|
|
350
|
+
return [];
|
|
351
|
+
try {
|
|
352
|
+
const prior = JSON.parse(content);
|
|
353
|
+
const mcp = prior.tools?.mcp;
|
|
354
|
+
if (!Array.isArray(mcp))
|
|
355
|
+
return [];
|
|
356
|
+
const names = [];
|
|
357
|
+
for (const entry of mcp) {
|
|
358
|
+
if (entry !== null &&
|
|
359
|
+
typeof entry === "object" &&
|
|
360
|
+
typeof entry.name === "string") {
|
|
361
|
+
names.push(entry.name);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return names;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Corrupt .last-apply record: no provenance names from it; the
|
|
368
|
+
// current manifest's names still apply.
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Strip harness-owned names from the dead `mcpServers` block in
|
|
374
|
+
* `o.claudeSettingsPath` (D-002/D-003: only ever called AFTER Ensure has
|
|
375
|
+
* confirmed every desired server is correctly registered via the `claude`
|
|
376
|
+
* CLI). Owned = the current manifest's `tools.mcp[]` names, UNION any
|
|
377
|
+
* names a pre-T-002 harness version generated into settings.json
|
|
378
|
+
* (`.last-apply` provenance), UNION the uninstall module's default
|
|
379
|
+
* ownership set (covers a manifest-less/first-run migration the same way
|
|
380
|
+
* `harness uninstall` already does). Foreign entries (anything outside
|
|
381
|
+
* that union — an operator hand-add, or another tool's MCP server) are
|
|
382
|
+
* left untouched. Writes only when something actually changes.
|
|
383
|
+
*
|
|
384
|
+
* `lastApply` is read once, BEFORE `apply()` runs, by `wireRuntime`
|
|
385
|
+
* (`readPriorLastApply`, task 363a6de0 / D-107) and threaded through
|
|
386
|
+
* `wireClaudeMcp` rather than re-read here — both this function's
|
|
387
|
+
* pre-T-002 provenance check and GC's ownership-union construction need
|
|
388
|
+
* the SAME pre-apply snapshot (apply() re-stamps `.last-apply` with the
|
|
389
|
+
* current manifest whenever anything changed, which would otherwise be
|
|
390
|
+
* read back here as if it were "prior").
|
|
391
|
+
*/
|
|
392
|
+
function migrateDeadSettingsMcpBlock(o, manifest, lastApply) {
|
|
393
|
+
let raw;
|
|
394
|
+
try {
|
|
395
|
+
raw = fs.readFileSync(o.claudeSettingsPath, "utf8");
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return []; // No settings.json (yet) — nothing to migrate.
|
|
399
|
+
}
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = JSON.parse(raw);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
o.stderr(`\n⚠ ${o.claudeSettingsPath} is not valid JSON; skipped the dead mcpServers cleanup. Fix the file and re-run \`harness init --interactive\`.\n`);
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
const ownedNames = new Set([
|
|
412
|
+
...manifest.tools.mcp.map((m) => m.name),
|
|
413
|
+
...priorGeneratedMcpNames(lastApply),
|
|
414
|
+
...DEFAULT_OWNED_MCP_SERVERS,
|
|
415
|
+
]);
|
|
416
|
+
const { settings, removedNames } = stripOwnedMcpServers(parsed, [...ownedNames]);
|
|
417
|
+
if (removedNames.length === 0)
|
|
418
|
+
return [];
|
|
419
|
+
atomicWriteFile(o.claudeSettingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
420
|
+
return removedNames;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Register the manifest's `tools.mcp[]` servers with Claude Code's
|
|
424
|
+
* user-scope registry via the `claude mcp` CLI (task
|
|
425
|
+
* init-mcp-wiring-claude-code/T-001/T-002), then — only once every
|
|
426
|
+
* desired server is confirmed correctly registered (D-002) — strip the
|
|
427
|
+
* dead settings.json `mcpServers` block. Mutates `outcome` in place so
|
|
428
|
+
* both the try and catch branches of the caller (the settings.json/hooks
|
|
429
|
+
* apply above) get the same MCP wiring regardless of whether that apply
|
|
430
|
+
* itself succeeded.
|
|
431
|
+
*
|
|
432
|
+
* `claude` CLI missing (`cli-missing`) is NOT a hard failure: the wizard
|
|
433
|
+
* warns, prints copy-pasteable `claude mcp add-json --scope user <name>
|
|
434
|
+
* '<json>'` commands per server, and continues — the manifest is on disk
|
|
435
|
+
* and hooks may already be wired even if MCP isn't yet. Migration is
|
|
436
|
+
* skipped in that case (and for any other incomplete registration) so a
|
|
437
|
+
* still-effective legacy settings.json entry is never removed out from
|
|
438
|
+
* under a server that couldn't be re-registered.
|
|
439
|
+
*
|
|
440
|
+
* Also drives GC (task 363a6de0): `ensureMcpServers`'s `gc` option is
|
|
441
|
+
* given the D-107 ownership union — the current manifest's `tools.mcp[]`
|
|
442
|
+
* names UNION the `tools.mcp[]` names of `priorLastApply` (the
|
|
443
|
+
* `.last-apply` manifest snapshot captured by the caller BEFORE this
|
|
444
|
+
* run's `apply()` ran, see `readPriorLastApply`) — so a server whose
|
|
445
|
+
* manifest entry was removed or disabled since it was last registered
|
|
446
|
+
* gets `claude mcp remove`d. `DEFAULT_OWNED_MCP_SERVERS` is DELIBERATELY
|
|
447
|
+
* NOT part of this union (D-107, reviewer HIGH finding on the original
|
|
448
|
+
* D-103): a server sharing a name with a harness default (grounding-mcp,
|
|
449
|
+
* agent-tasks, codebase-oracle) that the operator registered themselves
|
|
450
|
+
* — outside of, or before, any harness manifest on this machine — must
|
|
451
|
+
* never be GC'd just because it never appears in `desired`; "never in
|
|
452
|
+
* the manifest" is not the same ownership claim as "removed from the
|
|
453
|
+
* manifest". GC reporting is independent of the add/replace gate below —
|
|
454
|
+
* it runs, and is reported, regardless of whether the add/replace pass
|
|
455
|
+
* for `desired` succeeded.
|
|
456
|
+
*/
|
|
457
|
+
async function wireClaudeMcp(o, outcome, priorLastApply) {
|
|
458
|
+
const loaded = loadDesiredMcpServers(o.configPath, o.homeDir);
|
|
459
|
+
if (loaded === null) {
|
|
460
|
+
o.stderr(`\n⚠ Could not reload the manifest to register MCP servers with the claude CLI; skipped.\n`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
for (const w of loaded.warnings)
|
|
464
|
+
o.stderr(`mcp warning: ${w}\n`);
|
|
465
|
+
// D-107: GC eligibility is manifest-provenance-based ONLY — current
|
|
466
|
+
// manifest names ∪ the pre-apply `.last-apply` snapshot's names.
|
|
467
|
+
// DEFAULT_OWNED_MCP_SERVERS is intentionally excluded here (see the
|
|
468
|
+
// function doc above); it remains the ownership source for the
|
|
469
|
+
// read-only paths below (dead-settings-block migration, detection).
|
|
470
|
+
const gcOwnedNames = [
|
|
471
|
+
...new Set([
|
|
472
|
+
...loaded.manifest.tools.mcp.map((m) => m.name),
|
|
473
|
+
...priorManifestMcpNames(priorLastApply),
|
|
474
|
+
]),
|
|
475
|
+
];
|
|
476
|
+
const claudeHomeDir = path.dirname(o.claudeSettingsPath);
|
|
477
|
+
const ensureResult = await ensureMcpServers({
|
|
478
|
+
desired: loaded.desired,
|
|
479
|
+
homeDir: claudeHomeDir,
|
|
480
|
+
gc: { ownedNames: gcOwnedNames },
|
|
481
|
+
...(o.mcpExec ? { exec: o.mcpExec } : {}),
|
|
482
|
+
});
|
|
483
|
+
outcome.mcpEnsure = ensureResult;
|
|
484
|
+
const gcResults = ensureResult.gc?.results ?? [];
|
|
485
|
+
const gcRemoved = gcResults.filter((r) => r.action === "removed");
|
|
486
|
+
const gcSkipped = gcResults.filter((r) => r.action === "skipped");
|
|
487
|
+
if (gcRemoved.length > 0) {
|
|
488
|
+
o.stderr(`deregistered ${gcRemoved.length} stale MCP server(s) no longer in the manifest ` +
|
|
489
|
+
`(user scope, claude CLI): ${gcRemoved.map((r) => r.name).join(", ")}\n`);
|
|
490
|
+
}
|
|
491
|
+
if (gcSkipped.length > 0) {
|
|
492
|
+
o.stderr([
|
|
493
|
+
"",
|
|
494
|
+
"⚠ Could not deregister one or more stale MCP server(s) via the `claude` CLI:",
|
|
495
|
+
...gcSkipped.map((r) => ` ${r.name}: ${r.reason ?? r.remove.message}`),
|
|
496
|
+
" Remove them yourself:",
|
|
497
|
+
...manualRemoveLines(gcSkipped.map((r) => r.name)).map((l) => ` ${l}`),
|
|
498
|
+
"",
|
|
499
|
+
].join("\n"));
|
|
500
|
+
}
|
|
501
|
+
if (ensureResult.gc?.registryReadError) {
|
|
502
|
+
o.stderr("⚠ Could not read the claude CLI user-scope registry to garbage-collect stale MCP " +
|
|
503
|
+
`servers: ${ensureResult.gc.registryReadError}\n`);
|
|
504
|
+
}
|
|
505
|
+
const actionable = ensureResult.results.filter((r) => r.action !== "noop");
|
|
506
|
+
const cliMissing = actionable.some((r) => r.add?.status === "cli-missing" || r.remove?.status === "cli-missing");
|
|
507
|
+
const allOk = ensureResult.results.every((r) => r.action === "noop" ||
|
|
508
|
+
((r.action === "add" || r.action === "replace") && r.add?.status === "added"));
|
|
509
|
+
if (!allOk) {
|
|
510
|
+
if (cliMissing) {
|
|
511
|
+
o.stderr([
|
|
512
|
+
"",
|
|
513
|
+
"⚠ The `claude` CLI is not on PATH — MCP servers were not registered automatically.",
|
|
514
|
+
" Install Claude Code, then register the manifest's MCP servers yourself:",
|
|
515
|
+
"",
|
|
516
|
+
...manualAddJsonLines(Object.fromEntries(actionable.map((r) => [r.name, loaded.desired[r.name]]))).map((l) => ` ${l}`),
|
|
517
|
+
"",
|
|
518
|
+
].join("\n"));
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
o.stderr([
|
|
522
|
+
"",
|
|
523
|
+
"⚠ Registering one or more MCP servers with the `claude` CLI failed:",
|
|
524
|
+
...actionable.map((r) => ` ${r.name}: ${r.add?.message ?? r.remove?.message ?? r.reason ?? r.action}`),
|
|
525
|
+
"",
|
|
526
|
+
].join("\n"));
|
|
527
|
+
}
|
|
528
|
+
o.stderr(" Re-run `harness init --interactive` after fixing the issue to finish MCP registration and the settings.json cleanup.\n");
|
|
529
|
+
if (outcome.recoveryHint === undefined) {
|
|
530
|
+
outcome.recoveryHint =
|
|
531
|
+
"re-run `harness init --interactive` to finish registering MCP servers with the claude CLI";
|
|
532
|
+
}
|
|
533
|
+
return; // D-002: migration runs only after every desired server registers successfully.
|
|
534
|
+
}
|
|
535
|
+
if (actionable.length > 0) {
|
|
536
|
+
o.stderr(`\nregistered ${actionable.length} MCP server(s) with the claude CLI (user scope): ${actionable
|
|
537
|
+
.map((r) => r.name)
|
|
538
|
+
.join(", ")}\n`);
|
|
539
|
+
}
|
|
540
|
+
const removedNames = migrateDeadSettingsMcpBlock(o, loaded.manifest, priorLastApply);
|
|
541
|
+
if (removedNames.length > 0) {
|
|
542
|
+
outcome.mcpMigrationRemovedNames = removedNames;
|
|
543
|
+
o.stderr(`removed ${removedNames.length} dead mcpServers entr${removedNames.length === 1 ? "y" : "ies"} from ${o.claudeSettingsPath}: ${removedNames.join(", ")}\n`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
202
546
|
/**
|
|
203
547
|
* Post-install auth check for the agent-tasks bridge. Probes the
|
|
204
548
|
* bridge's `status` verb and, when no token is configured, offers the
|
|
@@ -322,8 +666,17 @@ async function ensureAgentTasksAuth(o) {
|
|
|
322
666
|
* PATH) resolves `code: 1` rather than throwing, so the caller's
|
|
323
667
|
* graceful-failure path handles a missing toolchain the same as a
|
|
324
668
|
* non-zero exit.
|
|
669
|
+
*
|
|
670
|
+
* Hermetic guard (task 54739002): asserts BEFORE touching
|
|
671
|
+
* `child_process` that we are not running under vitest without a test
|
|
672
|
+
* having injected `owInitSpawn`. See
|
|
673
|
+
* src/runtime/hermetic-spawn-guard.ts for why and the env signal used.
|
|
674
|
+
* The thrown `HermeticSpawnViolationError` is re-thrown past the
|
|
675
|
+
* caller's try/catch (which otherwise degrades a thrown runner to a
|
|
676
|
+
* warning) — see the catch in offerOrchestratorWorkflow below.
|
|
325
677
|
*/
|
|
326
678
|
function realOwInitSpawn(cmd, args) {
|
|
679
|
+
assertNoRealSpawnInTests("npx orchestrator-workflow init", "Inject a fake `owInitSpawn` runner in the test instead of exercising the real spawn path.");
|
|
327
680
|
return new Promise((resolve) => {
|
|
328
681
|
const child = spawn(cmd, args, { stdio: ["ignore", "inherit", "pipe"] });
|
|
329
682
|
let stderr = "";
|
|
@@ -414,6 +767,12 @@ async function offerOrchestratorWorkflow(o) {
|
|
|
414
767
|
result = await run("npx", ["orchestrator-workflow", "init", "--yes", o.repoDir]);
|
|
415
768
|
}
|
|
416
769
|
catch (err) {
|
|
770
|
+
// Hermetic guard (task 54739002): a real-spawn violation under
|
|
771
|
+
// vitest is NOT an ordinary runner failure — it must propagate past
|
|
772
|
+
// this optional-and-warn handling and fail the test hard, so it is
|
|
773
|
+
// re-thrown here before the generic degrade-to-warning below runs.
|
|
774
|
+
if (err instanceof HermeticSpawnViolationError)
|
|
775
|
+
throw err;
|
|
417
776
|
// A thrown runner (an injected spawn that rejects, or an unexpected
|
|
418
777
|
// throw) is treated exactly like a non-zero exit: OW is optional, so
|
|
419
778
|
// we warn and continue rather than failing harness init.
|
|
@@ -498,7 +857,7 @@ export async function runInteractive(opts = {}) {
|
|
|
498
857
|
}
|
|
499
858
|
if (profileNeedsAgentTasks(profile) && !detectionHasAgentTasks(detection)) {
|
|
500
859
|
const proceed = await prompts.confirm({
|
|
501
|
-
message: "The Team profile wires the agent-tasks MCP via the `agent-tasks-mcp-bridge` binary AND assumes you have an agent-tasks account (hosted or self-hosted).
|
|
860
|
+
message: "The Team profile wires the agent-tasks MCP via the `agent-tasks-mcp-bridge` binary AND assumes you have an agent-tasks account (hosted or self-hosted). It is not registered with Claude Code yet; the wizard will offer to install missing packages and register it in a moment via the `claude mcp` CLI (user scope). Proceed?",
|
|
502
861
|
default: true,
|
|
503
862
|
});
|
|
504
863
|
if (!proceed) {
|
|
@@ -590,6 +949,17 @@ export async function runInteractive(opts = {}) {
|
|
|
590
949
|
if (homeArg !== undefined) {
|
|
591
950
|
initOpts.homeDir = homeArg;
|
|
592
951
|
}
|
|
952
|
+
// Task 7f8fb4bc: reuse the dependency-check PATH override (when a
|
|
953
|
+
// test supplies one) for `init()`'s own post-write bin-resolution
|
|
954
|
+
// check, so the two checks agree on what's "installed" for this run.
|
|
955
|
+
if (opts.dependencyPathEnv !== undefined) {
|
|
956
|
+
initOpts.pathEnv = opts.dependencyPathEnv;
|
|
957
|
+
}
|
|
958
|
+
// Task 325ace29: same forwarding for the `npm prefix -g` runner, so a
|
|
959
|
+
// test can keep `init()`'s bin-resolution check off the real npm.
|
|
960
|
+
if (opts.npmBinExec !== undefined) {
|
|
961
|
+
initOpts.npmBinExec = opts.npmBinExec;
|
|
962
|
+
}
|
|
593
963
|
const initResult = await init(initOpts);
|
|
594
964
|
stdout(initResult.stdout);
|
|
595
965
|
const tailResult = await runPostInitTail({
|
|
@@ -617,6 +987,14 @@ export async function runInteractive(opts = {}) {
|
|
|
617
987
|
return tailResult;
|
|
618
988
|
}
|
|
619
989
|
catch (err) {
|
|
990
|
+
// Defense-in-depth (task 54739002): a hermetic-spawn-guard violation
|
|
991
|
+
// must always propagate out of runInteractive as a hard failure.
|
|
992
|
+
// `isAbortError` below already wouldn't match a
|
|
993
|
+
// HermeticSpawnViolationError (it checks for an ExitPromptError/
|
|
994
|
+
// abort-shaped name), so this is belt-and-suspenders against a
|
|
995
|
+
// future change to isAbortError narrowing that behavior by name.
|
|
996
|
+
if (err instanceof HermeticSpawnViolationError)
|
|
997
|
+
throw err;
|
|
620
998
|
if (isAbortError(err)) {
|
|
621
999
|
stderr("Aborted: Ctrl-C received during prompt; no manifest written.\n");
|
|
622
1000
|
return { aborted: true };
|
|
@@ -645,6 +1023,17 @@ async function runPostInitTail(t) {
|
|
|
645
1023
|
stderr(`\nValidate reported errors. Fix the manifest before running \`harness apply\`.\n`);
|
|
646
1024
|
return { aborted: false, profile, init: initResult, validateClean };
|
|
647
1025
|
}
|
|
1026
|
+
// Bin-resolution check (task 7f8fb4bc): validate() only checks the
|
|
1027
|
+
// manifest's shape, not whether the binaries it declares actually
|
|
1028
|
+
// resolve. A binary that installed successfully but landed under a npm
|
|
1029
|
+
// global bin dir NOT on PATH (the dogfood incident this task fixes)
|
|
1030
|
+
// passes validate cleanly and only used to surface as an opaque
|
|
1031
|
+
// `harness doctor` crash later. `init()` itself already ran this check
|
|
1032
|
+
// against the just-written manifest (threaded through `pathEnv` above)
|
|
1033
|
+
// and folded any findings into `initResult.stderr`, printed at the top
|
|
1034
|
+
// of this function — read its verdict here rather than re-running the
|
|
1035
|
+
// check a second time with different (test-injection) plumbing.
|
|
1036
|
+
const binResolutionClean = initResult.binResolutionErrorCount === 0;
|
|
648
1037
|
if (profile === "team" || profile === "full") {
|
|
649
1038
|
// Reminder splits at the "Not using agent-tasks?" paragraph because
|
|
650
1039
|
// Full ships review-before-merge-bash + review-subagent-before-pr-create-bash
|
|
@@ -711,14 +1100,31 @@ async function runPostInitTail(t) {
|
|
|
711
1100
|
],
|
|
712
1101
|
}));
|
|
713
1102
|
if (selectedRuntimes.length === 0) {
|
|
1103
|
+
// T-002: claude-code wiring is now two independent steps — the
|
|
1104
|
+
// settings.json/hooks merge (still `harness apply --target ... --merge`)
|
|
1105
|
+
// and MCP registration (via the `claude mcp` CLI, not settings.json;
|
|
1106
|
+
// see io/claude-mcp.ts). There is no single non-interactive command for
|
|
1107
|
+
// the latter yet, so print the manifest's per-server add-json commands
|
|
1108
|
+
// directly — the same manual fallback the wizard itself prints when
|
|
1109
|
+
// the `claude` CLI is missing.
|
|
1110
|
+
const desiredMcp = loadDesiredMcpServers(initResult.path, harnessHomeArg(opts));
|
|
1111
|
+
const mcpLines = desiredMcp !== null && Object.keys(desiredMcp.desired).length > 0
|
|
1112
|
+
? manualAddJsonLines(desiredMcp.desired)
|
|
1113
|
+
: [];
|
|
714
1114
|
stderr([
|
|
715
1115
|
"",
|
|
716
1116
|
"Manifest written; no runtimes selected for wiring. To wire later:",
|
|
717
|
-
` claude-code: harness apply --target ${claudeSettingsPath} --merge`,
|
|
1117
|
+
` claude-code hooks: harness apply --target ${claudeSettingsPath} --merge`,
|
|
1118
|
+
...(mcpLines.length > 0
|
|
1119
|
+
? [
|
|
1120
|
+
" claude-code MCP: re-run `harness init --interactive` and select claude-code, or run:",
|
|
1121
|
+
...mcpLines.map((l) => ` ${l}`),
|
|
1122
|
+
]
|
|
1123
|
+
: []),
|
|
718
1124
|
` codex: harness apply --runtime codex --install --codex-config ${codexConfigPath}`,
|
|
719
1125
|
"",
|
|
720
1126
|
].join("\n"));
|
|
721
|
-
return { aborted: false, profile, init: initResult, validateClean, applies: [] };
|
|
1127
|
+
return { aborted: false, profile, init: initResult, validateClean, binResolutionClean, applies: [] };
|
|
722
1128
|
}
|
|
723
1129
|
if (selectedRuntimes.length > 1) {
|
|
724
1130
|
stderr("\nMulti-runtime wiring: harness.lock will reflect the last-applied runtime; re-run `harness apply --runtime <name>` to refresh drift baselines.\n");
|
|
@@ -735,6 +1141,8 @@ async function runPostInitTail(t) {
|
|
|
735
1141
|
const homeArg = harnessHomeArg(opts);
|
|
736
1142
|
if (homeArg !== undefined)
|
|
737
1143
|
wireOpts.homeDir = homeArg;
|
|
1144
|
+
if (opts.mcpExec)
|
|
1145
|
+
wireOpts.mcpExec = opts.mcpExec;
|
|
738
1146
|
const outcome = await wireRuntime(wireOpts);
|
|
739
1147
|
applies.push(outcome);
|
|
740
1148
|
}
|
|
@@ -744,6 +1152,7 @@ async function runPostInitTail(t) {
|
|
|
744
1152
|
profile,
|
|
745
1153
|
init: initResult,
|
|
746
1154
|
validateClean,
|
|
1155
|
+
binResolutionClean,
|
|
747
1156
|
applies,
|
|
748
1157
|
};
|
|
749
1158
|
if (legacyApply !== undefined)
|
|
@@ -883,6 +1292,12 @@ async function runCustomProfile(rc) {
|
|
|
883
1292
|
const homeArg = harnessHomeArg(opts);
|
|
884
1293
|
if (homeArg !== undefined)
|
|
885
1294
|
initOpts.homeDir = homeArg;
|
|
1295
|
+
// Task 7f8fb4bc: see the named-profile path above for rationale.
|
|
1296
|
+
if (opts.dependencyPathEnv !== undefined)
|
|
1297
|
+
initOpts.pathEnv = opts.dependencyPathEnv;
|
|
1298
|
+
// Task 325ace29: see the named-profile path above for rationale.
|
|
1299
|
+
if (opts.npmBinExec !== undefined)
|
|
1300
|
+
initOpts.npmBinExec = opts.npmBinExec;
|
|
886
1301
|
const initResult = await init(initOpts);
|
|
887
1302
|
stdout(initResult.stdout);
|
|
888
1303
|
return await runPostInitTail({
|