@bridge_gpt/mcp-server 0.2.16 → 0.2.18

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 (45) hide show
  1. package/build/agents.generated.js +2 -2
  2. package/build/commands.generated.js +6 -6
  3. package/build/conductor/bridge-api-client.js +191 -11
  4. package/build/conductor/claude-hook.js +22 -4
  5. package/build/conductor/cli.js +11 -13
  6. package/build/conductor/done-gate.js +5 -0
  7. package/build/conductor/epic-reconcile.js +62 -13
  8. package/build/conductor/epic-runtime.js +447 -35
  9. package/build/conductor/epic-state.js +517 -63
  10. package/build/conductor/errors.js +41 -0
  11. package/build/conductor/event-accessors.js +234 -0
  12. package/build/conductor/file-scope-guard.js +201 -0
  13. package/build/conductor/github-mergeability.js +85 -0
  14. package/build/conductor/local-merge.js +47 -1
  15. package/build/conductor/merge-identity.js +41 -0
  16. package/build/conductor/merge-ledger.js +13 -68
  17. package/build/conductor/plan.js +12 -2
  18. package/build/conductor/pr-discovery.js +11 -1
  19. package/build/conductor/supervisor-config.js +4 -39
  20. package/build/conductor/supervisor-escalation.js +10 -26
  21. package/build/conductor/supervisor-ledger.js +5 -12
  22. package/build/conductor/supervisor-message-relay.js +2 -5
  23. package/build/conductor/supervisor-notification.js +1 -1
  24. package/build/conductor/supervisor-runtime.js +12 -54
  25. package/build/conductor/supervisor-state.js +4 -18
  26. package/build/conductor/supervisor-types.js +2 -2
  27. package/build/conductor/taxonomy.js +4 -0
  28. package/build/conductor-bin.js +2333 -666
  29. package/build/conductor-claude-hook-bin.js +4 -2
  30. package/build/doctor.js +32 -0
  31. package/build/index.js +10125 -8522
  32. package/build/install-bridge.js +25 -8
  33. package/build/install-doctor.js +387 -0
  34. package/build/pipelines.generated.js +30 -5
  35. package/build/regression-check.js +53 -1
  36. package/build/review-tickets.js +175 -21
  37. package/build/start-tickets-conductor.js +22 -6
  38. package/build/start-tickets-prereqs.js +33 -3
  39. package/build/start-tickets.js +122 -22
  40. package/build/version.generated.js +1 -1
  41. package/package.json +5 -5
  42. package/pipelines/review-ticket.json +24 -2
  43. package/public/css/main.min.css +3272 -1
  44. package/public/css/main.min.css.map +1 -1
  45. package/smoke-test/SMOKE-TEST.md +4 -2
@@ -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 only — no wt, git-wt, git, or Worktrunk required):",
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
- "--dry-run works on any platform, including unsupported ones.",
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
- return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}`;
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
- const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides);
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
  }
@@ -22,6 +22,7 @@
22
22
  import { randomBytes } from "node:crypto";
23
23
  import path from "node:path";
24
24
  import { fileURLToPath } from "node:url";
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 exactly three named keys,
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
- return {
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
@@ -182,11 +190,19 @@ export function buildConductorWorkerEnv(context, worker, parentEnv) {
182
190
  // ---------------------------------------------------------------------------
183
191
  // Claude hook settings merge / provisioning
184
192
  // ---------------------------------------------------------------------------
185
- /** Claude lifecycle events always registered for the conductor hook. */
193
+ /**
194
+ * Claude lifecycle events always registered for the conductor hook.
195
+ *
196
+ * BAPI-507 (N-1): `Stop` and `SubagentStop` are deliberately NOT registered.
197
+ * They are per-turn / per-subagent events (verified against the Claude Code
198
+ * lifecycle-hook contract), so registering them emitted a `run.stopped` on every
199
+ * turn and folded the ticket to `ready_for_review` while it was still being
200
+ * implemented. `SessionEnd` is the one true session-terminal event and is the
201
+ * only session-end signal registered here.
202
+ */
186
203
  export const CONDUCTOR_HOOK_LIFECYCLE_EVENTS = [
187
204
  "SessionStart",
188
- "Stop",
189
- "SubagentStop",
205
+ "SessionEnd",
190
206
  "Notification",
191
207
  ];
192
208
  /** Broad matcher used for `PreToolUse` when no existing matcher is present. */
@@ -254,6 +254,34 @@ function uvDescriptor() {
254
254
  return commandDescriptor("uv", "uv", UV_INSTALL_HINTS);
255
255
  }
256
256
  // ---------------------------------------------------------------------------
257
+ // review-tickets git prerequisite (BAPI-474). review-tickets has its own
258
+ // terminal-launcher-only live preflight (runReviewTicketsPreflight in
259
+ // review-tickets.ts) — separate from getPreflightPrereqDescriptors above,
260
+ // which is start-tickets-specific. Since the parent-fetch-once base pin now
261
+ // requires git for review-tickets too (unless --no-refresh-base is passed),
262
+ // this doctor-only descriptor gives it its own distinctly-labeled,
263
+ // read-only-diagnosable entry rather than relying on the generic start-tickets
264
+ // "git" descriptor, so a user running review-tickets alone can self-diagnose.
265
+ // ---------------------------------------------------------------------------
266
+ const REVIEW_TICKETS_GIT_INSTALL_HINTS = {
267
+ darwin: `${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,
268
+ linux: `${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,
269
+ win32: `${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,
270
+ };
271
+ function reviewTicketsGitDescriptor() {
272
+ return {
273
+ id: "review-tickets-git",
274
+ label: "git (required by review-tickets base-branch fetch)",
275
+ installHint: REVIEW_TICKETS_GIT_INSTALL_HINTS,
276
+ probe: async (deps) => {
277
+ const found = await isCommandOnPath(deps, "git");
278
+ return found
279
+ ? { found: true, detail: "found on PATH" }
280
+ : { found: false, detail: "review-tickets' parent-fetch-once base pin needs git unless --no-refresh-base is passed" };
281
+ },
282
+ };
283
+ }
284
+ // ---------------------------------------------------------------------------
257
285
  // regression-check (BAPI-460) doctor-only tool descriptors. These probe the
258
286
  // deterministic tooling regression-check relies on (ast-grep, ripgrep,
259
287
  // lizard). They are doctor-diagnosable but NOT preflight blockers: a missing
@@ -270,10 +298,11 @@ const LIZARD_INSTALL_HINTS = {
270
298
  linux: "uv tool install lizard",
271
299
  win32: "uv tool install lizard",
272
300
  };
301
+ const RIPGREP_SHELL_FUNCTION_CAVEAT = "a shell function/alias will not satisfy this check — ripgrep must be installed as a real PATH binary.";
273
302
  const RIPGREP_INSTALL_HINTS = {
274
- darwin: "brew install ripgrep",
275
- linux: "Install ripgrep with your distro package manager, e.g. apt install ripgrep",
276
- win32: "winget install BurntSushi.ripgrep.MSVC",
303
+ darwin: `brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,
304
+ linux: `Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,
305
+ win32: `winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,
277
306
  };
278
307
  /** ast-grep ships two equivalent binary names (`ast-grep` and `sg`); either satisfies the probe. */
279
308
  function astGrepDescriptor() {
@@ -427,6 +456,7 @@ export function getDoctorOnlyPrereqDescriptors(_platform, _env, agent) {
427
456
  astGrepDescriptor(),
428
457
  lizardDescriptor(),
429
458
  ripgrepDescriptor(),
459
+ reviewTicketsGitDescriptor(),
430
460
  ];
431
461
  }
432
462
  /**