@bridge_gpt/mcp-server 0.2.16 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +3 -3
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +198 -18
- package/build/conductor/claude-hook.js +22 -4
- package/build/conductor/cli.js +76 -25
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/done-gate.js +5 -0
- package/build/conductor/epic-reconcile.js +71 -14
- package/build/conductor/epic-runtime.js +839 -67
- package/build/conductor/epic-state.js +524 -63
- package/build/conductor/errors.js +156 -3
- package/build/conductor/event-accessors.js +252 -0
- package/build/conductor/file-scope-guard.js +201 -0
- package/build/conductor/github-mergeability.js +85 -0
- package/build/conductor/local-merge.js +47 -1
- package/build/conductor/merge-identity.js +41 -0
- package/build/conductor/merge-ledger.js +19 -72
- package/build/conductor/plan.js +12 -2
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/pr-discovery.js +11 -1
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-config.js +4 -39
- package/build/conductor/supervisor-escalation.js +10 -26
- package/build/conductor/supervisor-ledger.js +5 -12
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/supervisor-message-relay.js +2 -5
- package/build/conductor/supervisor-notification.js +1 -1
- package/build/conductor/supervisor-runtime.js +12 -54
- package/build/conductor/supervisor-state.js +4 -18
- package/build/conductor/supervisor-types.js +2 -2
- package/build/conductor/taxonomy.js +12 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1800 -5166
- package/build/conductor-claude-hook-bin.js +4 -2
- package/build/doctor.js +40 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +13950 -9669
- package/build/install-bridge.js +25 -8
- package/build/install-doctor.js +387 -0
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +31 -6
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +53 -1
- package/build/review-tickets.js +175 -21
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +47 -99
- package/build/start-tickets-prereqs.js +185 -4
- package/build/start-tickets.js +218 -180
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +10 -6
- package/pipelines/review-ticket.json +24 -2
- package/public/css/main.min.css +3311 -1
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +5 -2
package/build/review-tickets.js
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* This path is worktree-free and base-branch-free; the human CLI path leaves
|
|
16
16
|
* `epic` unset and retains byte-for-byte identical behavior.
|
|
17
17
|
*/
|
|
18
|
-
import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, } from "./start-tickets.js";
|
|
18
|
+
import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, validateBranchName, fetchAndResolveBaseSha, fetchStartTicketsConfigField, resolveStartTicketsBridgeApiAccess, } from "./start-tickets.js";
|
|
19
19
|
import { WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, TMUX_COMMAND, isSupportedStartTicketsPlatform, unsupportedPlatformMessage, } from "./start-tickets-prereqs.js";
|
|
20
20
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, isValidModelAlias, } from "./agent-registry.js";
|
|
21
21
|
import { injectConductorEnvIntoShellCommand, buildEpicIdentityEnv, mintStartTicketsRunId, } from "./start-tickets-conductor.js";
|
|
22
|
+
/** Default base context: no flags threaded through (matches pre-BAPI-474 behavior). */
|
|
23
|
+
const NO_BASE_CONTEXT = { noRefreshBase: false };
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
// Usage
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
@@ -35,6 +37,8 @@ export function getReviewTicketsUsage() {
|
|
|
35
37
|
" --model VALUE Model alias to pass as --model to the agent (optional passthrough)",
|
|
36
38
|
" --max-parallel N Max tabs to spawn concurrently (default: 3)",
|
|
37
39
|
" --dry-run Print intended commands; creates no tabs and opens no sessions",
|
|
40
|
+
" --base-branch BRANCH Base branch to fetch+pin for grounding (default: config base_branch, else \"main\")",
|
|
41
|
+
" --no-refresh-base Skip the parent-fetch-once base pin; restores git-free, in-place-grounded behavior",
|
|
38
42
|
" -h, --help Show this help",
|
|
39
43
|
"",
|
|
40
44
|
"Examples:",
|
|
@@ -42,19 +46,107 @@ export function getReviewTicketsUsage() {
|
|
|
42
46
|
" npx -y @bridge_gpt/mcp-server review-tickets --auto --rounds=1 BAPI-1 BAPI-2",
|
|
43
47
|
" npx -y @bridge_gpt/mcp-server review-tickets --review BAPI-1=auto,rounds=1 --review BAPI-2=rounds=2 BAPI-1 BAPI-2",
|
|
44
48
|
" npx -y @bridge_gpt/mcp-server review-tickets --dry-run BAPI-1 BAPI-2",
|
|
49
|
+
" npx -y @bridge_gpt/mcp-server review-tickets --base-branch=develop BAPI-1 BAPI-2",
|
|
50
|
+
" npx -y @bridge_gpt/mcp-server review-tickets --no-refresh-base BAPI-1 BAPI-2",
|
|
45
51
|
"",
|
|
46
52
|
"Each KEY must match [A-Z]+-[0-9]+ (e.g., BAPI-248).",
|
|
47
53
|
"",
|
|
48
|
-
"Prerequisites (terminal launcher
|
|
49
|
-
" macOS osascript",
|
|
50
|
-
" Windows wt.exe or PowerShell",
|
|
51
|
-
" Linux tmux",
|
|
54
|
+
"Prerequisites (terminal launcher, plus git — no wt, git-wt, or Worktrunk required):",
|
|
55
|
+
" macOS osascript, git",
|
|
56
|
+
" Windows wt.exe or PowerShell, git",
|
|
57
|
+
" Linux tmux, git",
|
|
52
58
|
"",
|
|
53
|
-
"review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees.",
|
|
54
|
-
"
|
|
59
|
+
"review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees. It now",
|
|
60
|
+
"requires git on PATH (BAPI-474): before spawning tabs it fetches origin/<base_branch> once and pins a",
|
|
61
|
+
"single base_sha for the whole batch, so every spawned review grounds against the same base tree even if",
|
|
62
|
+
"origin advances mid-batch. Pass --no-refresh-base to skip this and restore the prior git-free behavior.",
|
|
63
|
+
"--dry-run works on any platform, including unsupported ones, and never performs the base-branch fetch.",
|
|
55
64
|
].join("\n");
|
|
56
65
|
}
|
|
57
66
|
// ---------------------------------------------------------------------------
|
|
67
|
+
// Fail-loud base-fetch diagnostic (BAPI-474 D-4)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
const ANSI_BOLD = "\x1b[1m";
|
|
70
|
+
// Warm Coral — a high-contrast 256-color red/coral used exclusively for this
|
|
71
|
+
// fail-loud alert box so a base-branch fetch failure is unmistakable in a scan
|
|
72
|
+
// of scrollback, without hijacking the terminal's general error-red semantics.
|
|
73
|
+
const ANSI_WARM_CORAL = "\x1b[38;5;203m";
|
|
74
|
+
const ANSI_RESET = "\x1b[0m";
|
|
75
|
+
/**
|
|
76
|
+
* Render a bordered, high-contrast fail-loud alert box for a parent-fetch-once
|
|
77
|
+
* failure (BAPI-474 D-4 "Design Direction Fail-Loud"): an operation-failed
|
|
78
|
+
* header, the target base branch, and an actionable remediation line. Pure
|
|
79
|
+
* formatting (no I/O) so it stays unit-testable; the caller is responsible for
|
|
80
|
+
* writing it to stderr.
|
|
81
|
+
*/
|
|
82
|
+
export function formatBaseFetchFailureBox(baseBranch, error) {
|
|
83
|
+
const title = "review-tickets: base-branch fetch failed";
|
|
84
|
+
// The box always shows a dedicated Remediation line below. fetchAndResolveBaseSha's
|
|
85
|
+
// own error text embeds the same remediation clause because it also has to stand
|
|
86
|
+
// alone when surfaced outside a box (e.g. raw JSON from materialize_fresh_base) —
|
|
87
|
+
// strip it here so the boxed rendering doesn't say it twice.
|
|
88
|
+
const reason = error
|
|
89
|
+
.replace(/\s*Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip\.?\s*$/i, "")
|
|
90
|
+
.trim();
|
|
91
|
+
const bodyLines = [
|
|
92
|
+
`Target base branch: origin/${baseBranch}`,
|
|
93
|
+
`Reason: ${reason}`,
|
|
94
|
+
"Remediation: check your network and 'git remote get-url origin', or rerun with --no-refresh-base to evaluate in-place.",
|
|
95
|
+
];
|
|
96
|
+
const contentWidth = Math.max(title.length + 2, ...bodyLines.map((l) => l.length)) + 2;
|
|
97
|
+
const horizontal = "─".repeat(contentWidth);
|
|
98
|
+
const pad = (s) => `│ ${s}${" ".repeat(contentWidth - s.length - 1)}│`;
|
|
99
|
+
const lines = [
|
|
100
|
+
`╭${horizontal}╮`,
|
|
101
|
+
pad(`✖ ${title}`),
|
|
102
|
+
`├${horizontal}┤`,
|
|
103
|
+
...bodyLines.map((l) => pad(l)),
|
|
104
|
+
`╰${horizontal}╯`,
|
|
105
|
+
];
|
|
106
|
+
return `${ANSI_BOLD}${ANSI_WARM_CORAL}${lines.join("\n")}${ANSI_RESET}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve ONE base_branch/base_sha pin for the whole review-tickets batch
|
|
110
|
+
* BEFORE any tab is spawned (D-3), so a mid-batch `origin` advance can never
|
|
111
|
+
* mix bases within one run. Skipped entirely (never fetches) when
|
|
112
|
+
* `--no-refresh-base` was passed. base_branch precedence: `--base-branch` flag
|
|
113
|
+
* > config field `base_branch` (BAPI-313) > `"main"`. The config-field lookup
|
|
114
|
+
* is best-effort — any failure there falls through to `"main"`; only the git
|
|
115
|
+
* fetch/resolve itself is a hard failure (the caller applies the D-4 fail-loud
|
|
116
|
+
* response to that).
|
|
117
|
+
*/
|
|
118
|
+
export async function resolveBatchBaseContext(deps, options) {
|
|
119
|
+
if (options.noRefreshBase) {
|
|
120
|
+
return { ok: true, context: { noRefreshBase: true } };
|
|
121
|
+
}
|
|
122
|
+
let effectiveBaseBranch = (options.baseBranch ?? "").trim();
|
|
123
|
+
if (effectiveBaseBranch.length === 0) {
|
|
124
|
+
try {
|
|
125
|
+
const accessResult = await resolveStartTicketsBridgeApiAccess(deps);
|
|
126
|
+
if (accessResult.ok) {
|
|
127
|
+
const configValue = await fetchStartTicketsConfigField(accessResult.access, "base_branch");
|
|
128
|
+
if (typeof configValue === "string" && configValue.trim().length > 0) {
|
|
129
|
+
effectiveBaseBranch = configValue.trim();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* config-field lookup is best-effort; fall through to the "main" default */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (effectiveBaseBranch.length === 0) {
|
|
138
|
+
effectiveBaseBranch = "main";
|
|
139
|
+
}
|
|
140
|
+
const fetchResult = await fetchAndResolveBaseSha(deps, effectiveBaseBranch);
|
|
141
|
+
if (!fetchResult.ok) {
|
|
142
|
+
return { ok: false, baseBranch: effectiveBaseBranch, error: fetchResult.error };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
context: { baseBranch: effectiveBaseBranch, baseSha: fetchResult.base_sha, noRefreshBase: false },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
58
150
|
// Argument parsing
|
|
59
151
|
// ---------------------------------------------------------------------------
|
|
60
152
|
export function parseReviewOverrideEntry(entry, requestedKeys) {
|
|
@@ -105,6 +197,8 @@ export function parseReviewTicketsArgs(argv) {
|
|
|
105
197
|
let maxParallelRaw;
|
|
106
198
|
let agentName = DEFAULT_AGENT_NAME;
|
|
107
199
|
let modelAlias;
|
|
200
|
+
let noRefreshBase = false;
|
|
201
|
+
let baseBranch;
|
|
108
202
|
const keys = [];
|
|
109
203
|
const rawReviewEntries = [];
|
|
110
204
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -123,6 +217,30 @@ export function parseReviewTicketsArgs(argv) {
|
|
|
123
217
|
globalAuto = true;
|
|
124
218
|
continue;
|
|
125
219
|
}
|
|
220
|
+
if (arg === "--no-refresh-base") {
|
|
221
|
+
noRefreshBase = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (arg === "--base-branch" || arg.startsWith("--base-branch=")) {
|
|
225
|
+
let value;
|
|
226
|
+
if (arg.startsWith("--base-branch=")) {
|
|
227
|
+
value = arg.slice("--base-branch=".length);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const next = i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
231
|
+
if (next === undefined || next.startsWith("-")) {
|
|
232
|
+
return { status: "error", message: "--base-branch requires a value (a branch name)." };
|
|
233
|
+
}
|
|
234
|
+
value = takeValue();
|
|
235
|
+
}
|
|
236
|
+
const trimmed = (value ?? "").trim();
|
|
237
|
+
const error = validateBranchName(trimmed);
|
|
238
|
+
if (error) {
|
|
239
|
+
return { status: "error", message: `Invalid --base-branch value: ${error}` };
|
|
240
|
+
}
|
|
241
|
+
baseBranch = trimmed;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
126
244
|
if (arg === "--rounds" || arg.startsWith("--rounds=")) {
|
|
127
245
|
let value;
|
|
128
246
|
if (arg.startsWith("--rounds=")) {
|
|
@@ -267,6 +385,8 @@ export function parseReviewTicketsArgs(argv) {
|
|
|
267
385
|
auto: globalAuto,
|
|
268
386
|
rounds: globalRounds,
|
|
269
387
|
reviewOverrides,
|
|
388
|
+
noRefreshBase,
|
|
389
|
+
baseBranch,
|
|
270
390
|
},
|
|
271
391
|
};
|
|
272
392
|
}
|
|
@@ -289,24 +409,27 @@ export function resolveEffectiveReviewMode(key, options) {
|
|
|
289
409
|
// hard-wired to `/implement-ticket`. Review-aware builders are used instead so
|
|
290
410
|
// the prompt uses `/review-ticket` and no directory change is prepended.
|
|
291
411
|
// ---------------------------------------------------------------------------
|
|
292
|
-
export function buildReviewTicketPrompt(key, mode) {
|
|
412
|
+
export function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT) {
|
|
293
413
|
const autoFlag = mode.auto ? " --auto" : "";
|
|
294
|
-
|
|
414
|
+
const baseBranchFlag = base.baseBranch ? ` --base-branch=${base.baseBranch}` : "";
|
|
415
|
+
const baseShaFlag = base.baseSha ? ` --base-sha=${base.baseSha}` : "";
|
|
416
|
+
const noRefreshFlag = base.noRefreshBase ? " --no-refresh-base" : "";
|
|
417
|
+
return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
|
|
295
418
|
}
|
|
296
|
-
export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias) {
|
|
297
|
-
const prompt = buildReviewTicketPrompt(key, mode);
|
|
419
|
+
export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base = NO_BASE_CONTEXT) {
|
|
420
|
+
const prompt = buildReviewTicketPrompt(key, mode, base);
|
|
298
421
|
const invocation = buildAgentInvocation(agent, prompt, (p) => `'${shSquoteInner(p)}'`, modelAlias);
|
|
299
422
|
return `cd '${shSquoteInner(cwd)}' && ${invocation}`;
|
|
300
423
|
}
|
|
301
|
-
export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias) {
|
|
302
|
-
const prompt = buildReviewTicketPrompt(key, mode);
|
|
424
|
+
export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base = NO_BASE_CONTEXT) {
|
|
425
|
+
const prompt = buildReviewTicketPrompt(key, mode, base);
|
|
303
426
|
return buildAgentInvocation(agent, prompt, powershellSquote, modelAlias);
|
|
304
427
|
}
|
|
305
|
-
export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias) {
|
|
428
|
+
export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias, base = NO_BASE_CONTEXT) {
|
|
306
429
|
if (platform === "win32") {
|
|
307
|
-
return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias);
|
|
430
|
+
return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base);
|
|
308
431
|
}
|
|
309
|
-
return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias);
|
|
432
|
+
return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base);
|
|
310
433
|
}
|
|
311
434
|
// ---------------------------------------------------------------------------
|
|
312
435
|
// Unsupported platform message for review-tickets
|
|
@@ -325,6 +448,16 @@ export async function runReviewTicketsPreflight(deps, options) {
|
|
|
325
448
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
326
449
|
return { ok: false, error: unsupportedReviewTicketsPlatformMessage(deps.platform) };
|
|
327
450
|
}
|
|
451
|
+
// BAPI-474: the parent-fetch-once base pin adds a git prerequisite unless the
|
|
452
|
+
// caller opted out via --no-refresh-base. Checked here (before any tab is
|
|
453
|
+
// spawned) so a missing git fails loud with a clear, actionable message.
|
|
454
|
+
if (!options.noRefreshBase && !(await isCommandOnPath(deps, "git"))) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
error: "git is required for review-tickets' parent-fetch-once base-branch pin (BAPI-474) but was not " +
|
|
458
|
+
"found on PATH. Install git, or rerun with --no-refresh-base to skip the fetch and evaluate in-place.",
|
|
459
|
+
};
|
|
460
|
+
}
|
|
328
461
|
if (deps.platform === "darwin") {
|
|
329
462
|
if (await isCommandOnPath(deps, "osascript"))
|
|
330
463
|
return { ok: true };
|
|
@@ -355,10 +488,10 @@ export async function runReviewTicketsPreflight(deps, options) {
|
|
|
355
488
|
// ---------------------------------------------------------------------------
|
|
356
489
|
// Plan rows + orchestration
|
|
357
490
|
// ---------------------------------------------------------------------------
|
|
358
|
-
export function buildReviewPlanRows(deps, options, agent, status, overrides = {}) {
|
|
491
|
+
export function buildReviewPlanRows(deps, options, agent, status, overrides = {}, baseContext = NO_BASE_CONTEXT) {
|
|
359
492
|
return options.keys.map((key) => {
|
|
360
493
|
const mode = resolveEffectiveReviewMode(key, options);
|
|
361
|
-
const baseCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
|
|
494
|
+
const baseCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias, baseContext);
|
|
362
495
|
let command = baseCommand;
|
|
363
496
|
let runId;
|
|
364
497
|
if (options.epic) {
|
|
@@ -379,6 +512,14 @@ export function buildReviewPlanRows(deps, options, agent, status, overrides = {}
|
|
|
379
512
|
};
|
|
380
513
|
});
|
|
381
514
|
}
|
|
515
|
+
/** Build a dry-run preview base context from parsed CLI options only — never fetches. */
|
|
516
|
+
function previewBaseContextFromOptions(options) {
|
|
517
|
+
if (options.noRefreshBase)
|
|
518
|
+
return { noRefreshBase: true };
|
|
519
|
+
if (options.baseBranch)
|
|
520
|
+
return { baseBranch: options.baseBranch, noRefreshBase: false };
|
|
521
|
+
return NO_BASE_CONTEXT;
|
|
522
|
+
}
|
|
382
523
|
export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
383
524
|
const agent = resolveAgentSpec(options.agentName);
|
|
384
525
|
if (!agent) {
|
|
@@ -395,7 +536,9 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
|
395
536
|
"and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
|
|
396
537
|
};
|
|
397
538
|
}
|
|
398
|
-
|
|
539
|
+
// --dry-run never performs the base-branch fetch (side-effect-free preview);
|
|
540
|
+
// it reflects only what the user explicitly passed on the CLI.
|
|
541
|
+
const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides, previewBaseContextFromOptions(options));
|
|
399
542
|
return { ok: true, rows };
|
|
400
543
|
}
|
|
401
544
|
const preflight = await runReviewTicketsPreflight(deps, options);
|
|
@@ -411,10 +554,21 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
|
411
554
|
"and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
|
|
412
555
|
};
|
|
413
556
|
}
|
|
557
|
+
// BAPI-474 D-3: parent-fetch-once — resolve ONE base_branch/base_sha pin for
|
|
558
|
+
// the whole batch before any tab is spawned. D-4: on failure (and no
|
|
559
|
+
// --no-refresh-base), fail loud with a bordered diagnostic and halt the
|
|
560
|
+
// fan-out entirely rather than silently grounding in place.
|
|
561
|
+
const resolveContext = overrides.resolveBaseContext ?? resolveBatchBaseContext;
|
|
562
|
+
const baseContextResult = await resolveContext(deps, options);
|
|
563
|
+
if (!baseContextResult.ok) {
|
|
564
|
+
console.error(formatBaseFetchFailureBox(baseContextResult.baseBranch, baseContextResult.error));
|
|
565
|
+
return { ok: false, error: baseContextResult.error };
|
|
566
|
+
}
|
|
567
|
+
const baseContext = baseContextResult.context;
|
|
414
568
|
const terminal = detectTerminal(undefined, deps.env);
|
|
415
569
|
const rows = await runWithConcurrency(options.keys, options.maxParallel, async (key) => {
|
|
416
570
|
const mode = resolveEffectiveReviewMode(key, options);
|
|
417
|
-
const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
|
|
571
|
+
const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias, baseContext);
|
|
418
572
|
let shellCommand = baseShellCommand;
|
|
419
573
|
let runId;
|
|
420
574
|
if (options.epic) {
|
|
@@ -565,7 +719,7 @@ export async function runReviewTicketsCli(argv, overrides = {}) {
|
|
|
565
719
|
errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`);
|
|
566
720
|
return 1;
|
|
567
721
|
}
|
|
568
|
-
const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides.orchestrateOverrides);
|
|
722
|
+
const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides.orchestrateOverrides, previewBaseContextFromOptions(options));
|
|
569
723
|
for (const row of rows) {
|
|
570
724
|
log(`DRY-RUN: ${row.key} -> ${row.command}`);
|
|
571
725
|
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* SFCC site-preference introspection read tools (BAPI-403, T6 — optional).
|
|
3
3
|
*
|
|
4
4
|
* Implements:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* site_preference_group_list — GET /system_object_definitions/SitePreferences/attribute_groups
|
|
6
|
+
* site_preference_get — match-all POST /site_preferences/preference_groups/{group}/{instance}/preference_search
|
|
7
|
+
* site_preference_search — POST /site_preferences/preference_groups/{group}/{instance}/preference_search
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* All three tools are read-only, run behind the T1 call-time gate, and route
|
|
9
10
|
* oversized payloads through the sfcc/output.ts truncate-and-save seam.
|
|
10
11
|
*
|
|
11
12
|
* NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
|
|
@@ -35,20 +36,24 @@ const INSTANCE_ENUM = z.enum(["staging", "development", "sandbox", "production"]
|
|
|
35
36
|
const INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; " +
|
|
36
37
|
"any other value is rejected with a validation error. Defaults to 'sandbox'.";
|
|
37
38
|
const sitePreferenceGetInput = z.object({
|
|
38
|
-
group: z.string().describe("
|
|
39
|
+
group: z.string().describe("Custom site preference group ID, e.g. 'LLMIntegration'."),
|
|
39
40
|
instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
|
|
40
41
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
41
42
|
count: z.number().optional().describe("Maximum number of preferences to return."),
|
|
42
43
|
});
|
|
43
44
|
const sitePreferenceSearchInput = z.object({
|
|
44
|
-
group: z.string().describe("
|
|
45
|
+
group: z.string().describe("Custom site preference group ID to search within, e.g. 'LLMIntegration'."),
|
|
45
46
|
instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
|
|
46
|
-
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for text search across preference ids
|
|
47
|
+
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for text search across preference ids, " +
|
|
47
48
|
"or a structured OCAPI query object (term_query, filtered_query, etc.)."),
|
|
48
49
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
49
50
|
count: z.number().optional().describe("Maximum number of results to return."),
|
|
50
51
|
sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
|
|
51
52
|
});
|
|
53
|
+
const sitePreferenceGroupListInput = z.object({
|
|
54
|
+
count: z.number().optional().describe("Maximum number of site preference groups to return."),
|
|
55
|
+
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
56
|
+
});
|
|
52
57
|
// ---------------------------------------------------------------------------
|
|
53
58
|
// Helpers
|
|
54
59
|
// ---------------------------------------------------------------------------
|
|
@@ -86,15 +91,14 @@ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
|
|
|
86
91
|
if (guard)
|
|
87
92
|
return guard;
|
|
88
93
|
const encodedGroup = encodeURIComponent(group);
|
|
89
|
-
const
|
|
94
|
+
const postBody = {
|
|
95
|
+
query: { match_all_query: {} },
|
|
96
|
+
};
|
|
90
97
|
if (start !== undefined)
|
|
91
|
-
|
|
98
|
+
postBody.start = start;
|
|
92
99
|
if (count !== undefined)
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
? "?" + new URLSearchParams(queryParams).toString()
|
|
96
|
-
: "";
|
|
97
|
-
const result = await ocapiGet(`/site_preferences/preference_groups/${encodedGroup}/${instance}${queryStr}`, credentials);
|
|
100
|
+
postBody.count = count;
|
|
101
|
+
const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
|
|
98
102
|
if (!result.ok) {
|
|
99
103
|
return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
|
|
100
104
|
}
|
|
@@ -113,7 +117,7 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
|
|
|
113
117
|
const encodedGroup = encodeURIComponent(group);
|
|
114
118
|
// Coerce plain string queries into OCAPI text_query shape
|
|
115
119
|
const resolvedQuery = typeof query === "string"
|
|
116
|
-
? { text_query: { fields: ["id"
|
|
120
|
+
? { text_query: { fields: ["id"], search_phrase: query } }
|
|
117
121
|
: query;
|
|
118
122
|
const postBody = { query: resolvedQuery };
|
|
119
123
|
if (start !== undefined)
|
|
@@ -132,25 +136,54 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
|
|
|
132
136
|
return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`);
|
|
133
137
|
});
|
|
134
138
|
}
|
|
139
|
+
export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
|
|
140
|
+
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
141
|
+
const { count, start } = sitePreferenceGroupListInput.parse(args);
|
|
142
|
+
const queryParams = {};
|
|
143
|
+
if (count !== undefined)
|
|
144
|
+
queryParams.count = String(count);
|
|
145
|
+
if (start !== undefined)
|
|
146
|
+
queryParams.start = String(start);
|
|
147
|
+
const queryStr = Object.keys(queryParams).length > 0
|
|
148
|
+
? "?" + new URLSearchParams(queryParams).toString()
|
|
149
|
+
: "";
|
|
150
|
+
const result = await ocapiGet(`/system_object_definitions/SitePreferences/attribute_groups${queryStr}`, credentials);
|
|
151
|
+
if (!result.ok) {
|
|
152
|
+
return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
|
|
153
|
+
}
|
|
154
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
155
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
156
|
+
const dir = path.join(await getDocsDir(), "sfcc");
|
|
157
|
+
return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
135
160
|
/**
|
|
136
|
-
* Register the
|
|
161
|
+
* Register the three SFCC site-preference introspection read tools.
|
|
137
162
|
*
|
|
138
163
|
* Called from `registerSfccTools` — no direct `index.ts` edits needed.
|
|
139
164
|
*/
|
|
140
165
|
export function registerSitePreferenceTools(registerTool, deps) {
|
|
141
166
|
const { gateDeps, getDocsDir } = deps;
|
|
167
|
+
registerTool("site_preference_group_list", {
|
|
168
|
+
description: "List custom site preference group ids from the developer sandbox via " +
|
|
169
|
+
"GET /system_object_definitions/SitePreferences/attribute_groups. Read-only; " +
|
|
170
|
+
"use this to discover group ids before site_preference_get or site_preference_search. " +
|
|
171
|
+
"Accepts optional `count` and `start` for paging. Oversized outputs are auto-saved locally.",
|
|
172
|
+
inputSchema: sitePreferenceGroupListInput,
|
|
173
|
+
annotations: READ_ANNOTATIONS,
|
|
174
|
+
}, buildSitePreferenceGroupListHandler(gateDeps, getDocsDir));
|
|
142
175
|
registerTool("site_preference_get", {
|
|
143
176
|
description: "Read effective preferences for a site preference group from the developer sandbox " +
|
|
144
|
-
"via
|
|
145
|
-
"
|
|
146
|
-
"
|
|
177
|
+
"via a match-all POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
|
|
178
|
+
"Read-only, sandbox-only. Accepts optional `start`/`count` paging in the POST body. " +
|
|
179
|
+
"Oversized payloads are auto-saved locally.",
|
|
147
180
|
inputSchema: sitePreferenceGetInput,
|
|
148
181
|
annotations: READ_ANNOTATIONS,
|
|
149
182
|
}, buildSitePreferenceGetHandler(gateDeps, getDocsDir));
|
|
150
183
|
registerTool("site_preference_search", {
|
|
151
184
|
description: "Search/filter preferences within a site preference group from the developer sandbox " +
|
|
152
185
|
"via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
|
|
153
|
-
"Read-only; v1 sandbox only. Pass a plain string for text search or a structured " +
|
|
186
|
+
"Read-only; v1 sandbox only. Pass a plain string for id-only text search or a structured " +
|
|
154
187
|
"OCAPI query object. Oversized results are auto-saved locally.",
|
|
155
188
|
inputSchema: sitePreferenceSearchInput,
|
|
156
189
|
annotations: READ_ANNOTATIONS,
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* metadata, or the shell command string.
|
|
21
21
|
*/
|
|
22
22
|
import { randomBytes } from "node:crypto";
|
|
23
|
-
import path from "node:path";
|
|
24
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { mergeClaudeSettingsWithCommandHook, detectExistingPreToolUseMatcher, provisionClaudeSettingsForWorktree, DEFAULT_PRE_TOOL_USE_MATCHER, } from "./claude-settings.js";
|
|
25
|
+
import { normalizeDeclaredTouchedFiles } from "./conductor/file-scope-guard.js";
|
|
25
26
|
import { resolveProfiles } from "./mcp-profile.js";
|
|
26
27
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -54,15 +55,22 @@ export function mintStartTicketsWorkerId(ticketKey, agentName, fragment = random
|
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Build the secret-free epic identity env keys from an {@link EpicDispatchIdentity}.
|
|
57
|
-
* Strict allowlist — constructs a FRESH object with
|
|
58
|
-
* never copies arbitrary parent env so credentials cannot leak.
|
|
58
|
+
* Strict allowlist — constructs a FRESH object with only named conductor keys,
|
|
59
|
+
* never copies arbitrary parent env so credentials cannot leak. The declared
|
|
60
|
+
* touched-file set (BAPI-507 N-2) is included as compact JSON ONLY when it
|
|
61
|
+
* normalizes to a non-empty repo-relative set.
|
|
59
62
|
*/
|
|
60
63
|
export function buildEpicIdentityEnv(epic) {
|
|
61
|
-
|
|
64
|
+
const env = {
|
|
62
65
|
BAPI_CONDUCTOR_EPIC_KEY: epic.epic_key,
|
|
63
66
|
BAPI_CONDUCTOR_EPIC_RUN_ID: epic.epic_run_id,
|
|
64
67
|
BAPI_CONDUCTOR_PLAN_VERSION: String(epic.plan_version),
|
|
65
68
|
};
|
|
69
|
+
const declared = normalizeDeclaredTouchedFiles(epic.declared_touched_files);
|
|
70
|
+
if (declared.length > 0) {
|
|
71
|
+
env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON = JSON.stringify(declared);
|
|
72
|
+
}
|
|
73
|
+
return env;
|
|
66
74
|
}
|
|
67
75
|
// ---------------------------------------------------------------------------
|
|
68
76
|
// Conductor context
|
|
@@ -108,6 +116,10 @@ export async function createStartTicketsConductorContext(options, agent, deps) {
|
|
|
108
116
|
supervisorMode,
|
|
109
117
|
agentName: agent.name,
|
|
110
118
|
cliFile: resolveBinPath("conductor-bin.js"),
|
|
119
|
+
// Capture the conductor process's Node executable so dispatched workers run
|
|
120
|
+
// conductor-bin.js under THIS runtime (which owns the ledger), never the
|
|
121
|
+
// worker's own Node (BAPI-527).
|
|
122
|
+
conductorNodePath: deps.execPath ?? process.execPath,
|
|
111
123
|
hookBinPath: resolveBinPath("conductor-claude-hook-bin.js"),
|
|
112
124
|
};
|
|
113
125
|
if (options.epic) {
|
|
@@ -159,6 +171,10 @@ export function buildConductorWorkerEnv(context, worker, parentEnv) {
|
|
|
159
171
|
BAPI_CONDUCTOR_GATE_NAME: context.gateName,
|
|
160
172
|
BAPI_CONDUCTOR_SUPERVISOR_MODE: context.supervisorMode,
|
|
161
173
|
BAPI_CONDUCTOR_CLI_FILE: context.cliFile,
|
|
174
|
+
// BAPI-527: the CLI file + Node runtime are a PAIR. The worker runs
|
|
175
|
+
// conductor-bin.js under this captured conductor Node for every worker-side
|
|
176
|
+
// ledger op, so the worker Node never loads better-sqlite3. Non-secret.
|
|
177
|
+
CONDUCTOR_NODE_PATH: context.conductorNodePath,
|
|
162
178
|
};
|
|
163
179
|
if (context.repoName) {
|
|
164
180
|
env.BAPI_CONDUCTOR_REPO_NAME = context.repoName;
|
|
@@ -182,15 +198,25 @@ export function buildConductorWorkerEnv(context, worker, parentEnv) {
|
|
|
182
198
|
// ---------------------------------------------------------------------------
|
|
183
199
|
// Claude hook settings merge / provisioning
|
|
184
200
|
// ---------------------------------------------------------------------------
|
|
185
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Claude lifecycle events always registered for the conductor hook.
|
|
203
|
+
*
|
|
204
|
+
* BAPI-507 (N-1): `Stop` and `SubagentStop` are deliberately NOT registered.
|
|
205
|
+
* They are per-turn / per-subagent events (verified against the Claude Code
|
|
206
|
+
* lifecycle-hook contract), so registering them emitted a `run.stopped` on every
|
|
207
|
+
* turn and folded the ticket to `ready_for_review` while it was still being
|
|
208
|
+
* implemented. `SessionEnd` is the one true session-terminal event and is the
|
|
209
|
+
* only session-end signal registered here.
|
|
210
|
+
*/
|
|
186
211
|
export const CONDUCTOR_HOOK_LIFECYCLE_EVENTS = [
|
|
187
212
|
"SessionStart",
|
|
188
|
-
"
|
|
189
|
-
"SubagentStop",
|
|
213
|
+
"SessionEnd",
|
|
190
214
|
"Notification",
|
|
191
215
|
];
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
// BAPI-534: the broad PreToolUse matcher and generic hook-merge machinery now
|
|
217
|
+
// live in `claude-settings.ts` (shared with the executor deny-layer path).
|
|
218
|
+
// Re-exported so existing importers of this name keep resolving.
|
|
219
|
+
export { DEFAULT_PRE_TOOL_USE_MATCHER };
|
|
194
220
|
/** Shell-quote a path for embedding in a hook command string. */
|
|
195
221
|
function shellQuotePath(value) {
|
|
196
222
|
// Single-quote for POSIX safety; embedded single quotes are escaped. Hook
|
|
@@ -208,55 +234,15 @@ export function resolveConductorHookCommand(env, hookBinPath, execPath = process
|
|
|
208
234
|
}
|
|
209
235
|
return `${shellQuotePath(execPath)} ${shellQuotePath(hookBinPath)}`;
|
|
210
236
|
}
|
|
211
|
-
function asHookEntries(value) {
|
|
212
|
-
return Array.isArray(value) ? value : [];
|
|
213
|
-
}
|
|
214
|
-
/** Does this event already register `command` as a command hook? */
|
|
215
|
-
function entriesContainCommand(entries, command) {
|
|
216
|
-
return entries.some((entry) => Array.isArray(entry?.hooks) &&
|
|
217
|
-
entry.hooks.some((h) => h && h.type === "command" && h.command === command));
|
|
218
|
-
}
|
|
219
237
|
/**
|
|
220
|
-
* Merge conductor lifecycle hooks into an existing Claude settings object
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* `
|
|
238
|
+
* Merge conductor lifecycle hooks into an existing Claude settings object.
|
|
239
|
+
* BAPI-534: a thin wrapper over the generic {@link mergeClaudeSettingsWithCommandHook}
|
|
240
|
+
* bound to the conductor lifecycle events — preserving the exported signature and
|
|
241
|
+
* behavior (idempotent; `PreToolUse` only when `enablePreToolUse`; mirrors any
|
|
242
|
+
* existing matcher style; never removes existing settings/hooks).
|
|
224
243
|
*/
|
|
225
244
|
export function mergeClaudeSettingsWithConductorHook(settings, command, options = {}) {
|
|
226
|
-
|
|
227
|
-
? settings.hooks
|
|
228
|
-
: {};
|
|
229
|
-
const hooks = { ...existingHooks };
|
|
230
|
-
const events = [...CONDUCTOR_HOOK_LIFECYCLE_EVENTS];
|
|
231
|
-
if (options.enablePreToolUse) {
|
|
232
|
-
events.push("PreToolUse");
|
|
233
|
-
}
|
|
234
|
-
for (const event of events) {
|
|
235
|
-
const entries = asHookEntries(hooks[event]);
|
|
236
|
-
if (entriesContainCommand(entries, command)) {
|
|
237
|
-
// Idempotent: already registered for this event.
|
|
238
|
-
hooks[event] = entries;
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
const newEntry = { hooks: [{ type: "command", command }] };
|
|
242
|
-
if (event === "PreToolUse") {
|
|
243
|
-
newEntry.matcher = options.preToolUseMatcher ?? DEFAULT_PRE_TOOL_USE_MATCHER;
|
|
244
|
-
}
|
|
245
|
-
hooks[event] = [...entries, newEntry];
|
|
246
|
-
}
|
|
247
|
-
return { ...settings, hooks };
|
|
248
|
-
}
|
|
249
|
-
/** Detect an existing PreToolUse matcher to mirror, if any. */
|
|
250
|
-
function detectExistingPreToolUseMatcher(settings) {
|
|
251
|
-
const hooks = settings.hooks;
|
|
252
|
-
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks))
|
|
253
|
-
return undefined;
|
|
254
|
-
const entries = asHookEntries(hooks.PreToolUse);
|
|
255
|
-
for (const entry of entries) {
|
|
256
|
-
if (typeof entry?.matcher === "string")
|
|
257
|
-
return entry.matcher;
|
|
258
|
-
}
|
|
259
|
-
return undefined;
|
|
245
|
+
return mergeClaudeSettingsWithCommandHook(settings, command, CONDUCTOR_HOOK_LIFECYCLE_EVENTS, options);
|
|
260
246
|
}
|
|
261
247
|
/**
|
|
262
248
|
* Provision the conductor Claude hook into ONE worktree's
|
|
@@ -265,52 +251,14 @@ function detectExistingPreToolUseMatcher(settings) {
|
|
|
265
251
|
* pretty-printed JSON, mirroring any existing PreToolUse matcher.
|
|
266
252
|
*/
|
|
267
253
|
export async function provisionConductorHookForWorktree(worktreePath, command, options, deps) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
raw = await deps.readFile(settingsPath);
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
// Missing file -> treat as empty settings.
|
|
277
|
-
raw = null;
|
|
278
|
-
}
|
|
279
|
-
if (raw !== null) {
|
|
280
|
-
try {
|
|
281
|
-
const parsed = JSON.parse(raw);
|
|
282
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
283
|
-
return {
|
|
284
|
-
ok: false,
|
|
285
|
-
reason: "malformed",
|
|
286
|
-
error: "existing .claude/settings.local.json is not a JSON object",
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
existing = parsed;
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
// Malformed JSON: fail the row rather than clobber the user's file. The
|
|
293
|
-
// error is generic — it never echoes the file contents.
|
|
294
|
-
return {
|
|
295
|
-
ok: false,
|
|
296
|
-
reason: "malformed",
|
|
297
|
-
error: "existing .claude/settings.local.json contains invalid JSON",
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const merged = mergeClaudeSettingsWithConductorHook(existing, command, {
|
|
254
|
+
// BAPI-534: delegate the malformed-safe read/merge/write to the shared
|
|
255
|
+
// `provisionClaudeSettingsForWorktree`, supplying a conductor-hook merge
|
|
256
|
+
// closure. The returned `{ ok, reason, error }` shape is identical to
|
|
257
|
+
// ConductorHookProvisionResult, so the mapping is a pass-through.
|
|
258
|
+
return provisionClaudeSettingsForWorktree(worktreePath, (existing) => mergeClaudeSettingsWithConductorHook(existing, command, {
|
|
302
259
|
enablePreToolUse: options.enablePreToolUse,
|
|
303
260
|
preToolUseMatcher: options.preToolUseMatcher ?? detectExistingPreToolUseMatcher(existing),
|
|
304
|
-
});
|
|
305
|
-
try {
|
|
306
|
-
await deps.mkdir(claudeDir, { recursive: true });
|
|
307
|
-
await deps.writeFile(settingsPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
// Best-effort: a write/mkdir failure must not block the actual work.
|
|
311
|
-
return { ok: false, reason: "io", error: "failed to write .claude/settings.local.json" };
|
|
312
|
-
}
|
|
313
|
-
return { ok: true };
|
|
261
|
+
}), deps);
|
|
314
262
|
}
|
|
315
263
|
/**
|
|
316
264
|
* Provision conductor identity across rows. Attaches `runId` to EVERY row (so
|