@bridge_gpt/mcp-server 0.2.37 → 0.2.39

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 (91) hide show
  1. package/README.md +193 -16
  2. package/build/agent-capabilities/probe-context.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +392 -0
  4. package/build/agent-launchers/executor-adapter-inspection.js +163 -0
  5. package/build/agent-launchers/executor-adapter-registry.js +90 -0
  6. package/build/agent-launchers/executor-adapter.js +136 -0
  7. package/build/agent-registry.js +28 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/claude-login.js +85 -0
  10. package/build/claude-user-config-doctor.js +59 -33
  11. package/build/commands.generated.js +12 -11
  12. package/build/conduct-epic/bridge-client.js +345 -0
  13. package/build/conduct-epic/checkpoint-store.js +423 -0
  14. package/build/conduct-epic/cli.js +1732 -0
  15. package/build/conduct-epic/lock.js +302 -0
  16. package/build/conduct-epic/pr-state.js +197 -0
  17. package/build/conduct-epic/spawn.js +101 -0
  18. package/build/conductor/bridge-api-client.js +37 -2
  19. package/build/conductor/doctor.js +11 -1
  20. package/build/conductor/install-doctor.js +184 -10
  21. package/build/conductor-bin.js +7 -7
  22. package/build/credential-store.js +10 -4
  23. package/build/credentials-cli.js +34 -19
  24. package/build/docs.generated.js +1 -1
  25. package/build/doctor.js +579 -88
  26. package/build/executor/agent-identity.js +32 -0
  27. package/build/executor/cli.js +50 -39
  28. package/build/executor/deps.js +15 -1
  29. package/build/executor/env.js +56 -45
  30. package/build/executor/index.js +9 -1
  31. package/build/executor/install-preflight.js +138 -0
  32. package/build/executor/job-errors.js +200 -0
  33. package/build/executor/job-runner.js +619 -268
  34. package/build/executor/observation.js +165 -0
  35. package/build/executor/permissions.js +163 -36
  36. package/build/executor/platform.js +54 -0
  37. package/build/executor/preflight.js +175 -67
  38. package/build/executor/process.js +39 -7
  39. package/build/executor/runner.js +19 -0
  40. package/build/executor/service-lifecycle.js +269 -0
  41. package/build/executor/service-unit.js +121 -12
  42. package/build/executor/stale-artifacts.js +70 -0
  43. package/build/executor/test-clock.js +188 -24
  44. package/build/executor/worker-command.js +22 -58
  45. package/build/executor/worker-log.js +82 -0
  46. package/build/executor/worktree-lock.js +264 -0
  47. package/build/index.js +527 -357
  48. package/build/install-bridge-conductor.js +376 -38
  49. package/build/install-bridge.js +414 -114
  50. package/build/install-doctor.js +13 -0
  51. package/build/install-reexec.js +5 -3
  52. package/build/mcp-install-state.js +130 -0
  53. package/build/mcp-profile.js +11 -2
  54. package/build/mcp-provisioning.js +15 -0
  55. package/build/merge-pull-request.js +562 -0
  56. package/build/phase-result-artifacts.js +450 -0
  57. package/build/pipeline-orchestrator.js +4 -0
  58. package/build/pipeline-utils.js +16 -0
  59. package/build/pipelines.generated.js +7 -7
  60. package/build/plane/preflight.js +18 -14
  61. package/build/plane/supervisor.js +8 -1
  62. package/build/project-root.js +34 -0
  63. package/build/readme.generated.js +1 -1
  64. package/build/run-unit-tests-launcher.js +36 -9
  65. package/build/setup-epic.js +57 -4
  66. package/build/sfcc/ocapi-shape.js +23 -4
  67. package/build/sfcc/permissions.js +25 -6
  68. package/build/sfcc/read-body.js +92 -0
  69. package/build/sfcc/read-projection.js +6 -2
  70. package/build/sfcc/reads-custom-object-def.js +33 -21
  71. package/build/sfcc/reads-site-preference.js +20 -7
  72. package/build/sfcc/reads-system-object.js +11 -5
  73. package/build/sfcc/register.js +61 -23
  74. package/build/sfcc/registration-inventory.js +89 -0
  75. package/build/sfcc/setup-status.js +18 -34
  76. package/build/sfcc/tool-wrapper.js +294 -17
  77. package/build/sfcc/write-grants.js +33 -1
  78. package/build/sfcc/write-guard.js +41 -12
  79. package/build/sfcc/write-result.js +16 -7
  80. package/build/sfcc/writes-custom-object-def.js +12 -4
  81. package/build/sfcc/writes-site-preference.js +6 -1
  82. package/build/sfcc/writes-system-object.js +11 -2
  83. package/build/sfcc/writes.js +13 -8
  84. package/build/start-tickets-prereqs.js +25 -15
  85. package/build/start-tickets.js +123 -21
  86. package/build/version.generated.js +1 -1
  87. package/build/worktree-core.js +9 -3
  88. package/docs/install/mcp-tool-integrations.md +54 -9
  89. package/docs/install/sfcc-integration.md +71 -24
  90. package/package.json +3 -3
  91. package/build/executor/worker-config-isolation.js +0 -287
@@ -26,11 +26,31 @@
26
26
  * 3. **Two independent consents.** Confirming the resolved base URL is NOT
27
27
  * consent to write, and neither is consent to overwrite a workflow file. Each
28
28
  * write surface asks for itself.
29
- * 4. **The executor leg is never claimed as provisioned by a bootstrap.** The
30
- * doctor *detects* generated service units (`executor install-service`,
31
- * BAPI-688), but starting one stays operator-managed, so the capability
32
- * matrix reports `executor_ready` only from a live server-observed claim and
33
- * the run prints `complete-with-manual-steps` until then.
29
+ * 4. **The executor leg is never claimed as provisioned by local success.**
30
+ * AMENDED BY BAPI-779 (R-1): this command can now write the service unit
31
+ * *and* start it. What did not change and must not — is where readiness
32
+ * comes from. The capability matrix reports `executor_ready` only from a
33
+ * live server-observed claim, so a written unit, a successful
34
+ * `launchctl bootstrap`, and a successful `systemctl --user enable --now`
35
+ * all still print `complete-with-manual-steps` until the SERVER says
36
+ * otherwise. Local lifecycle success and server observation are reported as
37
+ * two separate facts, and the second is never derived from the first.
38
+ *
39
+ * The executor phase (BAPI-779) is opt-in via `--executor-id` and runs behind
40
+ * TWO independent consents, per property 3: writing the unit is not consent to
41
+ * start a persistent daemon. Its shape is:
42
+ *
43
+ * preflight (platform / `wt`,`git`,`claude` / explicit base URL — plus an
44
+ * ADVISORY `claude login` line that never blocks, because BAPI-791 moved the
45
+ * definitive auth check to the first worker spawn)
46
+ * → consent 1: write the unit through the existing generator
47
+ * → consent 2: start the service, ONE attempt, never a retry loop
48
+ * → bounded server observation (~90s) — observed ⇒ `executor_ready`,
49
+ * timeout ⇒ an honest degradation naming what was and was not achieved.
50
+ *
51
+ * Declining either consent, a start failure, and an observation timeout are all
52
+ * non-fatal: the run prints the exact platform-specific start command and
53
+ * finishes at `complete-with-manual-steps`.
34
54
  *
35
55
  * The command does NOT persist a repository-level review policy: that is
36
56
  * BAPI-694's. The policy selected here is per-run and is reported as such.
@@ -38,6 +58,9 @@
38
58
  import { bootstrapConductorSupervisorDefaults, ConductorBridgeApiError, fetchConductorReadiness, } from "./conductor/bridge-api-client.js";
39
59
  import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
40
60
  import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
61
+ import { formatExecutorInstallPreflight, } from "./executor/install-preflight.js";
62
+ import { buildExecutorServiceStartPlan, formatExecutorServiceStartFailure, } from "./executor/service-lifecycle.js";
63
+ import { planExecutorServiceInstall, } from "./executor/service-unit.js";
41
64
  // ---------------------------------------------------------------------------
42
65
  // Options + parsing
43
66
  // ---------------------------------------------------------------------------
@@ -63,19 +86,26 @@ export function getInstallBridgeConductorUsage() {
63
86
  " --required-check <name> Required CI check for the done gate (repeatable, REQUIRED)",
64
87
  " --required-checks a,b Comma-separated form of --required-check",
65
88
  ` --review-policy <source> One of: ${CONDUCTOR_REVIEW_POLICY_SOURCES.join(", ")}`,
66
- ` (default: ${DEFAULT_CONDUCTOR_REVIEW_POLICY}; PER-RUN only —`,
67
- " repository defaults are not stored until BAPI-694)",
89
+ ` (default: ${DEFAULT_CONDUCTOR_REVIEW_POLICY}). Supplying it`,
90
+ " EXPLICITLY also persists it as the supervisor",
91
+ " project default, stamped into a new run that",
92
+ " carries no review policy of its own. Omitting it",
93
+ " stores no default at all.",
68
94
  " --epic-key <KEY> Epic key for the setup-epic verification phase",
69
95
  " --plan-file <path> Plan sidecar for the setup-epic verification phase",
96
+ " --executor-id <id> Stable executor id. Supplying it OPTS IN to the",
97
+ " consented executor-provisioning phase below; the",
98
+ " phase is skipped entirely without it.",
70
99
  " --allowed-bot <slug> claude-review workflow: allowed bot login",
71
100
  " --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
72
101
  " --branches a,b claude-review workflow: PR branch triggers",
73
102
  " --skip-workflow Never offer the claude-review workflow",
74
103
  " --dry-run Diagnose and preview only. Performs NO writes:",
75
104
  " no POST, no file write, no install-state write,",
76
- " and no write-consent prompt. Still asks the",
77
- " base-URL confirmation below first its doctor",
78
- " readiness GET carries the repo API key.",
105
+ " no service-unit write, no launchctl/systemctl",
106
+ " command, and no write- or start-consent prompt.",
107
+ " Still asks the base-URL confirmation below first —",
108
+ " its doctor readiness GET carries the repo API key.",
79
109
  " -h, --help Show this help",
80
110
  "",
81
111
  "The resolved BAPI_BASE_URL is displayed and confirmed BEFORE any network call",
@@ -83,9 +113,27 @@ export function getInstallBridgeConductorUsage() {
83
113
  "production when unset). The bootstrap POST then asks its own, separate write",
84
114
  "consent. Without a TTY there is no consent, so the command aborts.",
85
115
  "",
86
- "Executor provisioning: generate a persistent service unit with",
87
- "`executor install-service` (BAPI-688). Starting/stopping it stays",
88
- `operator-managed; see ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
116
+ "Executor provisioning: `executor install-service` still generates a unit and",
117
+ "nothing else, and remains available for operators who want the file only.",
118
+ "",
119
+ "With --executor-id, this command does the whole leg. A preflight checks the platform,",
120
+ "`wt` / `git` / `claude` on PATH, and an explicit base URL, then reports whether",
121
+ "this host has a `claude login` marker — that line is ADVISORY and never blocks,",
122
+ "and no API key, token, or other credential is requested, stored, or checked.",
123
+ "",
124
+ "Two independent consents follow. The first writes the service unit; the second,",
125
+ `asked separately, starts it (${"`launchctl bootstrap`"} on darwin,`,
126
+ "`systemctl --user daemon-reload && systemctl --user enable --now` on linux) with",
127
+ "exactly one attempt and no retry loop. Declining either one, or a start that",
128
+ "fails, prints the exact start command and finishes at complete-with-manual-steps.",
129
+ "",
130
+ "After a successful start the command polls the server for roughly 90 seconds for",
131
+ "the executor to be observed. Only that live server observation can report",
132
+ "executor_ready: a written unit and a successful local start never can, so an",
133
+ "observation timeout finishes at complete-with-manual-steps and says exactly what",
134
+ "was and was not achieved.",
135
+ "",
136
+ `See ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}.`,
89
137
  ].join("\n");
90
138
  }
91
139
  function splitList(raw) {
@@ -109,8 +157,10 @@ export function parseInstallBridgeConductorArgs(argv) {
109
157
  let dryRun = false;
110
158
  let skipWorkflow = false;
111
159
  let reviewPolicy = DEFAULT_CONDUCTOR_REVIEW_POLICY;
160
+ let reviewPolicyExplicit = false;
112
161
  let epicKey;
113
162
  let planFile;
163
+ let executorId;
114
164
  let allowedBot;
115
165
  let secretName;
116
166
  let branches;
@@ -148,6 +198,7 @@ export function parseInstallBridgeConductorArgs(argv) {
148
198
  case "--review-policy":
149
199
  case "--epic-key":
150
200
  case "--plan-file":
201
+ case "--executor-id":
151
202
  case "--allowed-bot":
152
203
  case "--secret-name":
153
204
  case "--branches": {
@@ -167,6 +218,8 @@ export function parseInstallBridgeConductorArgs(argv) {
167
218
  epicKey = value;
168
219
  else if (flag === "--plan-file")
169
220
  planFile = value;
221
+ else if (flag === "--executor-id")
222
+ executorId = value;
170
223
  else if (flag === "--allowed-bot")
171
224
  allowedBot = value;
172
225
  else if (flag === "--secret-name")
@@ -185,6 +238,7 @@ export function parseInstallBridgeConductorArgs(argv) {
185
238
  };
186
239
  }
187
240
  reviewPolicy = value;
241
+ reviewPolicyExplicit = true;
188
242
  }
189
243
  break;
190
244
  }
@@ -218,16 +272,85 @@ export function parseInstallBridgeConductorArgs(argv) {
218
272
  dryRun,
219
273
  requiredChecks: normalizedChecks,
220
274
  reviewPolicy,
275
+ reviewPolicyExplicit,
221
276
  epicKey,
222
277
  planFile,
223
278
  allowedBot,
224
279
  secretName,
225
280
  branches,
226
281
  skipWorkflow,
282
+ executorId,
227
283
  },
228
284
  };
229
285
  }
230
286
  // ---------------------------------------------------------------------------
287
+ // Bounded readiness observation (BAPI-779)
288
+ // ---------------------------------------------------------------------------
289
+ /** Total wall-clock budget for observing the executor server-side. */
290
+ export const EXECUTOR_OBSERVATION_BUDGET_MS = 90_000;
291
+ /** Fixed gap between readiness reads, clamped by the remaining budget. */
292
+ export const EXECUTOR_OBSERVATION_INTERVAL_MS = 5_000;
293
+ /**
294
+ * Poll `GET /jira/epic-runs/conductor-readiness` until the SERVER reports the
295
+ * executor ready, or the monotonic budget expires.
296
+ *
297
+ * Three properties matter here and are pinned by tests:
298
+ *
299
+ * - **Only `executor.ready === true` is readiness.** `false`, `null`, a
300
+ * truthy non-boolean, a payload that fails shape validation, and a transport
301
+ * error are all non-ready. This is the single place the `executor_ready`
302
+ * claim can originate, so a loose truthiness check here would silently
303
+ * reintroduce the dishonest-claim failure mode property 4 exists to prevent.
304
+ * - **The deadline bounds fetches AND sleeps.** No request is issued after the
305
+ * deadline, and a sleep is clamped to the remaining time, so the phase can
306
+ * never overrun its budget by up to one interval.
307
+ * - **Validation is centralized.** Every read goes through
308
+ * `fetchConductorReadiness`, so the authenticated URL construction and the
309
+ * response shape check are the same ones the doctor uses.
310
+ */
311
+ export async function observeExecutorReadiness(access, fetchJson, deps, options = {}) {
312
+ const budgetMs = options.budgetMs ?? EXECUTOR_OBSERVATION_BUDGET_MS;
313
+ const intervalMs = options.intervalMs ?? EXECUTOR_OBSERVATION_INTERVAL_MS;
314
+ const deadline = deps.now() + budgetMs;
315
+ let lastReady = null;
316
+ let lastSeenAt = null;
317
+ let lastSeenAgeSeconds = null;
318
+ let readFailures = 0;
319
+ let attempts = 0;
320
+ while (deps.now() < deadline) {
321
+ attempts++;
322
+ try {
323
+ const readiness = await fetchConductorReadiness(access, fetchJson);
324
+ if (readiness.executor.ready === true) {
325
+ return { status: "observed", readiness };
326
+ }
327
+ // A validated non-ready read: remember it so the timeout can report what
328
+ // the server actually said, not merely that nothing happened.
329
+ lastReady = readiness.executor.ready;
330
+ lastSeenAt = readiness.executor.last_seen_at;
331
+ lastSeenAgeSeconds = readiness.executor.last_seen_age_seconds;
332
+ }
333
+ catch {
334
+ // Shape-validation failures and transport errors are indistinguishable to
335
+ // an operator waiting for an executor, and both are retryable within the
336
+ // budget. The thrown value is discarded — it can carry a URL.
337
+ readFailures++;
338
+ }
339
+ const remaining = deadline - deps.now();
340
+ if (remaining <= 0)
341
+ break;
342
+ await deps.sleep(Math.min(intervalMs, remaining));
343
+ }
344
+ return {
345
+ status: "timeout",
346
+ lastReady,
347
+ lastSeenAt,
348
+ lastSeenAgeSeconds,
349
+ readFailures,
350
+ attempts,
351
+ };
352
+ }
353
+ // ---------------------------------------------------------------------------
231
354
  // Runner
232
355
  // ---------------------------------------------------------------------------
233
356
  const CONSENT_YES = new Set(["y", "yes"]);
@@ -260,6 +383,126 @@ function renderCapabilityMatrix(deps, readiness) {
260
383
  deps.log(` runbook: ${CONDUCTOR_OPERATOR_RUNBOOK_POINTER}`);
261
384
  return { apiReady, configurationReady, executorReady };
262
385
  }
386
+ /**
387
+ * Build both plans, or explain why the host cannot be provisioned.
388
+ *
389
+ * PURE apart from `resolveInvocation`, which reads this process's own launcher
390
+ * path. Computing the plans BEFORE any consent is what lets a dry run print the
391
+ * real target path and the real start command instead of a guess.
392
+ */
393
+ function buildExecutorProvisioningPlans(executor, executorId, baseUrl,
394
+ /**
395
+ * The repositories the generated unit serves. Taken from the RESOLVED Bridge
396
+ * access rather than from a flag, so the unit can only ever serve the
397
+ * repository this install actually authenticated for.
398
+ */
399
+ repos) {
400
+ if (executor.platform !== "darwin" && executor.platform !== "linux") {
401
+ return {
402
+ ok: false,
403
+ error: `platform '${executor.platform}' has no supported executor service format.`,
404
+ };
405
+ }
406
+ const platform = executor.platform;
407
+ const invocation = executor.resolveInvocation();
408
+ if (!invocation.ok)
409
+ return { ok: false, error: invocation.error };
410
+ let install;
411
+ try {
412
+ install = planExecutorServiceInstall({
413
+ invocation: invocation.invocation,
414
+ executorId,
415
+ repos,
416
+ baseUrl,
417
+ homeDir: executor.homeDir,
418
+ envPath: executor.envPath,
419
+ }, platform);
420
+ }
421
+ catch (err) {
422
+ // The generator's validation errors are already secret-free sentences.
423
+ return { ok: false, error: err instanceof Error ? err.message : "invalid unit input" };
424
+ }
425
+ const start = buildExecutorServiceStartPlan({
426
+ platform,
427
+ unitPath: install.unitPath,
428
+ serviceIdentifier: install.serviceIdentifier,
429
+ uid: executor.uid,
430
+ });
431
+ if (!start.ok)
432
+ return { ok: false, error: start.error };
433
+ return { ok: true, plans: { install, start: start.plan } };
434
+ }
435
+ /**
436
+ * Write the unit, start the service, and watch for a server observation — each
437
+ * step behind its own gate, and every failure a reported degradation.
438
+ *
439
+ * Returns the readiness payload ONLY when the server actually observed the
440
+ * executor. Returning `null` for every other path is what keeps the capability
441
+ * matrix honest: there is no code path here that can hand the matrix a
442
+ * fabricated ready response derived from a written file or a zero exit code.
443
+ */
444
+ async function runExecutorProvisioningPhase(deps, executor, plans, access) {
445
+ // ---- Consent A: write the unit ----------------------------------------
446
+ const writeOk = await confirm(deps, `Write the executor service unit ${plans.install.unitPath}? [y/N]: `);
447
+ if (!writeOk) {
448
+ deps.log(" declined — no unit was written and nothing was started.");
449
+ deps.log(` to provision by hand later: ${plans.start.manualCommand}`);
450
+ return null;
451
+ }
452
+ let writeResult;
453
+ try {
454
+ writeResult = await executor.writeUnit(plans.install);
455
+ }
456
+ catch {
457
+ deps.errorLog(" the service unit could not be written; nothing was started.");
458
+ return null;
459
+ }
460
+ deps.log(` ${writeResult.outcome} ${writeResult.unitPath}`);
461
+ // Install-state is a diagnostic aid, not a correctness dependency: a failure
462
+ // here does not invalidate the unit that was just written, so it warns.
463
+ const recorded = await executor.recordUnit(writeResult.unitPath);
464
+ if (!recorded.ok) {
465
+ deps.errorLog(` note: install-state not updated (${recorded.error ?? "unknown"})`);
466
+ }
467
+ // ---- Consent B: start the service -------------------------------------
468
+ // Deliberately separate from consent A. Writing a file and starting a
469
+ // persistent daemon that will claim work are different decisions, and the
470
+ // exact command is printed BEFORE the question so the operator is consenting
471
+ // to something specific.
472
+ deps.log("");
473
+ deps.log(` start command: ${plans.start.manualCommand}`);
474
+ const startOk = await confirm(deps, "Start the executor service now? [y/N]: ");
475
+ if (!startOk) {
476
+ deps.log(" declined — the unit is written but the service was not started.");
477
+ deps.log(` start it by hand with: ${plans.start.manualCommand}`);
478
+ return null;
479
+ }
480
+ const startResult = await executor.startService(plans.start);
481
+ if (!startResult.ok) {
482
+ deps.errorLog(` ${formatExecutorServiceStartFailure(startResult)}`);
483
+ deps.errorLog(` start it by hand with: ${startResult.manualCommand}`);
484
+ return null;
485
+ }
486
+ deps.log(" the service manager accepted the unit.");
487
+ // ---- Bounded observation ----------------------------------------------
488
+ // Only reached after a SUCCESSFUL start: polling for an executor that was
489
+ // never started would burn the whole budget to learn what we already know.
490
+ deps.log(` watching the server for an executor claim (up to ${Math.round(EXECUTOR_OBSERVATION_BUDGET_MS / 1000)}s)…`);
491
+ const observation = await observeExecutorReadiness(access, deps.fetchJson, executor);
492
+ if (observation.status === "observed") {
493
+ deps.log(" the server observed this executor.");
494
+ return observation.readiness;
495
+ }
496
+ // The honest degradation: name what WAS achieved and what was not, separately.
497
+ deps.log(" the unit was written and the service manager started it,");
498
+ deps.log(" but the server did not observe an executor claim within the budget " +
499
+ `(${observation.attempts} read(s), ${observation.readFailures} unreadable).`);
500
+ deps.log(observation.lastReady === false
501
+ ? " the server last reported this repository's executor as stale/not ready."
502
+ : " the server has not yet seen any executor claim for this repository.");
503
+ deps.log(" executor_ready is NOT claimed — only a live server observation can produce it.");
504
+ return null;
505
+ }
263
506
  /**
264
507
  * Run the nested conductor installer. Returns the process exit code.
265
508
  */
@@ -299,7 +542,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
299
542
  const access = accessResult.ok ? accessResult.access : null;
300
543
  const accessError = accessResult.ok ? undefined : accessResult.error;
301
544
  // ---- Phase 1: unified doctor, BEFORE every write -----------------------
302
- deps.log("Phase 1/5 — running the unified conductor doctor (read-only)…");
545
+ deps.log("Phase 1/6 — running the unified conductor doctor (read-only)…");
303
546
  const firstReport = await deps.runDoctor({
304
547
  access,
305
548
  accessError,
@@ -312,11 +555,63 @@ export async function runInstallBridgeConductorCli(argv, deps) {
312
555
  "install-state, or verification step was attempted.");
313
556
  return 1;
314
557
  }
315
- // ---- Review policy: per-run, explicitly NOT a repository default -------
558
+ // ---- Review policy: per-run, and a repository default when explicit ----
316
559
  deps.log("");
317
- deps.log(`Review policy (per-run): ${options.reviewPolicy}`);
318
- deps.log(" This selection applies to runs created with it. Repository-level review-policy " +
319
- "defaults are not stored yet that is BAPI-694.");
560
+ deps.log(`Review policy: ${options.reviewPolicy}`);
561
+ deps.log(options.reviewPolicyExplicit
562
+ ? " Explicitly selected, so it is ALSO persisted as the supervisor project " +
563
+ "default and stamped into a new run that sets no review policy of its own. " +
564
+ "An explicit per-run policy always wins, and changing this later never " +
565
+ "alters an already-approved run."
566
+ : " Applied to runs created with this selection. No repository default is " +
567
+ "persisted — pass --review-policy explicitly to store one.");
568
+ // ---- Executor preflight: AFTER the URL consent, BEFORE any mutation ----
569
+ // Placed here so an unsupported host or a missing `wt`/`git`/`claude` refuses
570
+ // before the bootstrap POST, the workflow write, or a single filesystem
571
+ // effect — provisioning a service that cannot run is worse than refusing —
572
+ // and before the dry-run return, so `--dry-run` reports the same refusal.
573
+ const executor = deps.executorProvisioning;
574
+ const executorRequested = Boolean(executor && options.executorId);
575
+ let executorPlans = null;
576
+ if (executor && executorRequested) {
577
+ deps.log("");
578
+ deps.log("Executor provisioning — preflight (read-only)…");
579
+ const preflight = await executor.runPreflight({
580
+ platform: executor.platform,
581
+ baseUrl,
582
+ });
583
+ deps.log(formatExecutorInstallPreflight(preflight));
584
+ if (!preflight.ok) {
585
+ // A plain refusal with remediation — never a silent skip, and never a
586
+ // credential complaint: the only auth-related output above is the
587
+ // advisory login line.
588
+ deps.errorLog("");
589
+ deps.errorLog("Executor provisioning refused — this host cannot run a conductor executor.");
590
+ for (const remediation of preflight.remediations)
591
+ deps.errorLog(` → ${remediation}`);
592
+ deps.log("");
593
+ deps.log("complete-with-manual-steps");
594
+ return 1;
595
+ }
596
+ // The unit serves the repository this run resolved access for. When access
597
+ // could not be resolved the doctor above has already produced a fatal
598
+ // section, so this fallback only matters for the narrow window where a
599
+ // caller injected a non-fatal doctor.
600
+ const planned = buildExecutorProvisioningPlans(executor, options.executorId, baseUrl, access ? [access.repoName] : []);
601
+ if (!planned.ok) {
602
+ deps.errorLog("");
603
+ deps.errorLog(`Executor provisioning refused — ${planned.error}`);
604
+ deps.log("");
605
+ deps.log("complete-with-manual-steps");
606
+ return 1;
607
+ }
608
+ executorPlans = planned.plans;
609
+ }
610
+ else if (executor) {
611
+ deps.log("");
612
+ deps.log(" executor provisioning skipped — pass --executor-id <id> to write and start a " +
613
+ "service unit for this host.");
614
+ }
320
615
  // ---- Dry run: preview and RETURN before every write/prompt -------------
321
616
  if (options.dryRun) {
322
617
  deps.log("");
@@ -327,6 +622,8 @@ export async function runInstallBridgeConductorCli(argv, deps) {
327
622
  deps.log(" - done_gate_config.required_checks " +
328
623
  `(${options.requiredChecks.length} check(s))`);
329
624
  deps.log(" - review_source");
625
+ if (options.reviewPolicyExplicit)
626
+ deps.log(" - review_policy");
330
627
  deps.log(" - auto_merge_enabled");
331
628
  deps.log(" - merge_approval_required");
332
629
  deps.log(` Would target base URL: ${baseUrl}`);
@@ -336,6 +633,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
336
633
  deps.log(options.epicKey && options.planFile
337
634
  ? ` Would verify with: setup-epic --dry-run --epic-key ${options.epicKey}`
338
635
  : " setup-epic verification: skipped (no --epic-key/--plan-file supplied)");
636
+ // The REAL planned path and the REAL start command — computed by the same
637
+ // pure planners the live run uses, so the preview cannot drift from what
638
+ // would actually happen. Still zero writes: no unit writer, no
639
+ // install-state recorder, no lifecycle runner, and no consent prompt has
640
+ // been reached on this branch.
641
+ if (executorPlans) {
642
+ deps.log(` Would write executor unit: ${executorPlans.install.unitPath}`);
643
+ deps.log(` Would start it with: ${executorPlans.start.manualCommand}`);
644
+ deps.log(" Executor provisioning: two separate consents (write, then start) — neither was asked.");
645
+ }
646
+ else if (executor) {
647
+ deps.log(" Executor provisioning: skipped (no --executor-id supplied)");
648
+ }
339
649
  renderCapabilityMatrix(deps, firstReport.readiness);
340
650
  deps.log("");
341
651
  deps.log("complete-with-manual-steps (dry run)");
@@ -358,12 +668,19 @@ export async function runInstallBridgeConductorCli(argv, deps) {
358
668
  }
359
669
  // ---- Phase 2: bootstrap ------------------------------------------------
360
670
  deps.log("");
361
- deps.log("Phase 2/5 — bootstrapping supervisor project defaults…");
671
+ deps.log("Phase 2/6 — bootstrapping supervisor project defaults…");
362
672
  let bootstrapResponse;
363
673
  try {
364
674
  bootstrapResponse = await bootstrapConductorSupervisorDefaults(access, {
365
675
  required_checks: options.requiredChecks,
366
676
  review_source: "verdict_protocol",
677
+ // BAPI-694 — the narrow ReviewPolicy object, sent ONLY on an explicit
678
+ // selection. `review_source` above is deliberately left alone: it is the
679
+ // WIDER done-gate vocabulary and the two are not interchangeable, so
680
+ // neither value is coerced into the other.
681
+ ...(options.reviewPolicyExplicit
682
+ ? { review_policy: { source: options.reviewPolicy } }
683
+ : {}),
367
684
  merge_approval_required: "true",
368
685
  }, deps.postJson);
369
686
  }
@@ -375,7 +692,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
375
692
  deps.log(` wrote supervisor setup + config (fields: ${bootstrapResponse.audited_field_names.join(", ")})`);
376
693
  // ---- Phase 3: optional workflow scaffold -------------------------------
377
694
  deps.log("");
378
- deps.log("Phase 3/5 — claude-review workflow…");
695
+ deps.log("Phase 3/6 — claude-review workflow…");
379
696
  if (options.skipWorkflow || options.reviewPolicy !== "verdict_protocol") {
380
697
  deps.log(` skipped — the '${options.reviewPolicy}' review policy does not consume the ` +
381
698
  "sticky verdict this workflow emits.");
@@ -432,25 +749,13 @@ export async function runInstallBridgeConductorCli(argv, deps) {
432
749
  }
433
750
  }
434
751
  }
435
- // ---- Phase 4: capability matrix from POST-bootstrap readiness ----------
436
- deps.log("");
437
- deps.log("Phase 4/5 — capability report…");
438
- let postReadiness = null;
439
- try {
440
- postReadiness = await fetchConductorReadiness(access, deps.fetchJson);
441
- }
442
- catch {
443
- // Degraded, not fatal: the write already succeeded, and the matrix simply
444
- // reports what it can observe.
445
- deps.errorLog(" readiness could not be re-read; capability matrix is best-effort.");
446
- }
447
- const matrix = renderCapabilityMatrix(deps, postReadiness);
448
- // ---- setup-epic verification (conditional) -----------------------------
752
+ // ---- Phase 4: setup-epic verification (conditional) --------------------
449
753
  // `parseSetupEpicArgs` hard-requires --epic-key AND --plan-file before any
450
754
  // dry-run branch, so this phase can only run when both are supplied. Fresh
451
755
  // repositories skip it with an explicit message rather than hard-failing.
756
+ deps.log("");
757
+ deps.log("Phase 4/6 — setup-epic verification…");
452
758
  if (options.epicKey && options.planFile) {
453
- deps.log("");
454
759
  deps.log(" verifying with `setup-epic --dry-run`…");
455
760
  const verification = await deps.runSetupEpicDryRun([
456
761
  "--dry-run",
@@ -471,12 +776,45 @@ export async function runInstallBridgeConductorCli(argv, deps) {
471
776
  deps.log(" setup-epic verification passed.");
472
777
  }
473
778
  else {
474
- deps.log("");
475
779
  deps.log(" setup-epic verification skipped — pass --epic-key and --plan-file once a plan exists.");
476
780
  }
477
- // ---- Phase 5: final doctor ---------------------------------------------
781
+ // ---- Phase 5: consented executor provisioning (BAPI-779) ---------------
782
+ // Runs AFTER the pre-existing phases so a bootstrap failure never leaves a
783
+ // started daemon behind, and BEFORE the capability report so the matrix can
784
+ // use the readiness this phase actually observed.
785
+ deps.log("");
786
+ deps.log("Phase 5/6 — executor provisioning…");
787
+ let observedReadiness = null;
788
+ if (executor && executorPlans) {
789
+ observedReadiness = await runExecutorProvisioningPhase(deps, executor, executorPlans, access);
790
+ }
791
+ else {
792
+ deps.log(" skipped — " +
793
+ (executor
794
+ ? "no --executor-id was supplied."
795
+ : "this caller supplied no executor-provisioning seams."));
796
+ }
797
+ // ---- Phase 6: capability matrix + final doctor -------------------------
798
+ deps.log("");
799
+ deps.log("Phase 6/6 — capability report…");
800
+ // The observed payload WINS when the executor phase actually saw the server
801
+ // report readiness. Otherwise the matrix re-reads readiness itself — it is
802
+ // never handed a synthesized response, so `executor_ready` still originates
803
+ // from exactly one place: a live server observation.
804
+ let postReadiness = observedReadiness;
805
+ if (postReadiness === null) {
806
+ try {
807
+ postReadiness = await fetchConductorReadiness(access, deps.fetchJson);
808
+ }
809
+ catch {
810
+ // Degraded, not fatal: the write already succeeded, and the matrix simply
811
+ // reports what it can observe.
812
+ deps.errorLog(" readiness could not be re-read; capability matrix is best-effort.");
813
+ }
814
+ }
815
+ const matrix = renderCapabilityMatrix(deps, postReadiness);
478
816
  deps.log("");
479
- deps.log("Phase 5/5 — re-running the unified conductor doctor (read-only)…");
817
+ deps.log(" re-running the unified conductor doctor (read-only)…");
480
818
  const finalReport = await deps.runDoctor({
481
819
  access,
482
820
  reviewPolicySource: options.reviewPolicy,