@bridge_gpt/mcp-server 0.2.65 → 0.2.66

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 (71) hide show
  1. package/README.md +25 -13
  2. package/build/agent-launchers/claude-executor-adapter.js +12 -18
  3. package/build/agent-launchers/executor-adapter.js +27 -0
  4. package/build/bridge-config.js +104 -0
  5. package/build/codex-bridge-profile.js +10 -1
  6. package/build/commands.generated.js +4 -4
  7. package/build/conductor/bridge-api-client.js +230 -10
  8. package/build/conductor/epic-child-continuation.js +61 -34
  9. package/build/conductor/epic-implementer-cli.js +12 -0
  10. package/build/conductor/guided-repair.js +323 -0
  11. package/build/conductor/readiness-cli.js +30 -9
  12. package/build/conductor/readiness.js +23 -1
  13. package/build/conductor/recovery-cli.js +507 -16
  14. package/build/conductor/recovery-operations.js +325 -2
  15. package/build/conductor/repair-contract.js +61 -3
  16. package/build/credential-store.js +5 -4
  17. package/build/diagnostic-text.js +36 -0
  18. package/build/docs.generated.js +1 -1
  19. package/build/doctor.js +70 -10
  20. package/build/drive-epic.js +395 -78
  21. package/build/epic-implementer-bin.js +24 -22
  22. package/build/epic-launch-diagnostic.js +31 -0
  23. package/build/epic-launch-profile.js +44 -14
  24. package/build/executor/completion-supervision.js +5 -0
  25. package/build/executor/correction-dispatch.js +60 -0
  26. package/build/executor/env.js +6 -0
  27. package/build/executor/http-client.js +15 -0
  28. package/build/executor/integration-job.js +6 -1
  29. package/build/executor/job-errors.js +13 -4
  30. package/build/executor/job-log-registry.js +340 -73
  31. package/build/executor/job-runner.js +260 -33
  32. package/build/executor/job-types.js +8 -13
  33. package/build/executor/preflight.js +91 -2
  34. package/build/executor/prompt-spec.js +113 -9
  35. package/build/executor/runner.js +23 -0
  36. package/build/executor/stale-artifacts.js +5 -4
  37. package/build/executor/types.js +1 -0
  38. package/build/executor/watch-cli.js +35 -8
  39. package/build/executor/worker-command.js +43 -43
  40. package/build/executor/worker-finalization.js +80 -15
  41. package/build/executor/worker-guard-hook.js +46 -10
  42. package/build/executor/worker-log.js +137 -70
  43. package/build/executor/worktree-gc.js +30 -6
  44. package/build/index.js +177 -329
  45. package/build/init.js +107 -4
  46. package/build/install-bridge.js +284 -20
  47. package/build/install-doctor.js +172 -30
  48. package/build/install-reexec.js +69 -7
  49. package/build/macos-terminal-permission-guidance.js +16 -0
  50. package/build/model-thresholds-tool.js +185 -0
  51. package/build/pipelines.generated.js +1 -1
  52. package/build/plane/status.js +4 -0
  53. package/build/plane/supervisor.js +4 -0
  54. package/build/plane/worker-exit-code.js +2 -0
  55. package/build/project-root.js +99 -0
  56. package/build/readme.generated.js +1 -1
  57. package/build/review-tickets.js +156 -13
  58. package/build/setup-epic.js +180 -108
  59. package/build/sfcc/setup-status.js +2 -0
  60. package/build/sfcc/tool-wrapper.js +2 -0
  61. package/build/sfcc/tools-configured.js +96 -0
  62. package/build/start-tickets.js +163 -58
  63. package/build/upgrade-cli.js +109 -24
  64. package/build/version.generated.js +3 -3
  65. package/build/windows-npm-entry.js +103 -0
  66. package/build/worker-guard-hook-bin.js +4 -3
  67. package/build/worktree-core.js +6 -5
  68. package/docs/install/mcp-tool-integrations.md +1 -0
  69. package/docs/install/sfcc-integration.md +12 -7
  70. package/package.json +1 -1
  71. package/pipelines/implement-ticket.json +1 -1
@@ -56,10 +56,10 @@
56
56
  import { readdir, readFile } from "node:fs/promises";
57
57
  import path, { dirname, join } from "node:path";
58
58
  import { CONDUCTOR_DEFAULT_BASE_URL, ConductorBridgeApiError, resolveEpicLaunchBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
59
- import { EPIC_LAUNCH_PROFILE_NAME_PATTERN, renderEpicLaunchCommand, shellQuote, } from "./epic-launch-profile.js";
59
+ import { EPIC_LAUNCH_PROFILE_NAME_PATTERN, epicLaunchPolicyDigest, pinEpicLaunchPolicy, renderEpicLaunchCommand, resolveEpicLaunchSelection, shellQuote, } from "./epic-launch-profile.js";
60
60
  import { validateHttpBaseUrl, describeBaseUrlRejection } from "./base-url.js";
61
61
  import { BRING_UP_REFUSAL_MESSAGES, BRING_UP_RUNBOOK, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
62
- import { policyDeclaresNotifyWebhook, readSetupEpicPolicyFile, runSetupEpicWorkflow, } from "./setup-epic.js";
62
+ import { policyDeclaresNotifyWebhook, effectivePolicyFacts, readSetupEpicPolicyFile, runSetupEpicWorkflow, } from "./setup-epic.js";
63
63
  import { EPIC_KEY_PATTERN } from "./drive-epic-key.js";
64
64
  import { discoverEpicPlan, } from "./plan-discovery.js";
65
65
  import { formatPlaneDiagnostic, launchPlaneControlPlane, requestPlaneScopedLanes, runDefaultPlanePreflight, runPlaneCli, } from "./plane/cli.js";
@@ -120,8 +120,9 @@ export const V2_READINESS_REQUIREMENTS = [
120
120
  // The repository-scoped `reconciler` section ("is this repository's work
121
121
  // moving?", from `epic_runs.last_tick_at`) is only the fallback for a server
122
122
  // too old to report `bring_up`.
123
- satisfied: (r) => r.bring_up !== null && r.bring_up !== undefined
124
- ? r.bring_up.heartbeat_readable && r.bring_up.worker_present
123
+ satisfied: (r) => r.bring_up
124
+ ? r.bring_up.heartbeat_readable && r.bring_up.worker_present &&
125
+ r.bring_up.single_transition_authority
125
126
  : r.reconciler.liveness_readable && !r.reconciler.stale,
126
127
  },
127
128
  {
@@ -238,6 +239,24 @@ export const V2_RUNTIME_READINESS_IDS = ["reconciler_live"];
238
239
  * already encodes that (setup, then config, then credentials).
239
240
  */
240
241
  export function classifyV2Readiness(readiness, options = {}) {
242
+ for (const diagnostic of readiness.diagnostics ?? []) {
243
+ if (diagnostic.severity === null || diagnostic.applicability === null) {
244
+ return {
245
+ kind: "operator_blocked",
246
+ id: "diagnostic-metadata-unknown",
247
+ describe: "declared diagnostic severity and applicability",
248
+ remedy: "Upgrade or repair the readiness producer, then retry the selected target.",
249
+ };
250
+ }
251
+ if (diagnostic.applicability === "applicable" && diagnostic.severity === "mandatory" && diagnostic.verdict !== "passed") {
252
+ return {
253
+ kind: "operator_blocked",
254
+ id: "mandatory-diagnostic-unverified",
255
+ describe: `mandatory diagnostic ${diagnostic.id}`,
256
+ remedy: "Resolve the mandatory diagnostic at the selected Bridge API target and retry.",
257
+ };
258
+ }
259
+ }
241
260
  // BAPI-1102 — the explicit `--attended` opt-out makes the unattended-only
242
261
  // requirements INAPPLICABLE, not merely tolerated. They authorize unattended
243
262
  // operation and automatic merge behavior; an attended run asks for neither, so
@@ -253,7 +272,9 @@ export function classifyV2Readiness(readiness, options = {}) {
253
272
  // which reads the policy and refuses if it declares none. Nothing is weakened:
254
273
  // `setup-epic` gates before the create, and the server re-checks the predicate
255
274
  // at approval and probes the URL there.
256
- const deferred = options.hasPolicyFile
275
+ const deferred = options.approvedStoredPolicy ||
276
+ options.effectivePolicy?.escalationChannel === "configured webhook" ||
277
+ (options.effectivePolicy === undefined && options.hasPolicyFile)
257
278
  ? V2_READINESS_REQUIREMENTS.filter((req) => req.id !== "unattended_notify_default")
258
279
  : V2_READINESS_REQUIREMENTS;
259
280
  // `--into-base` is the second opt-out, and it is structural rather than
@@ -270,8 +291,28 @@ export function classifyV2Readiness(readiness, options = {}) {
270
291
  ? gating.filter((req) => req.unattendedOnly !== true)
271
292
  : gating;
272
293
  const unmet = applicable.filter((req) => !req.satisfied(readiness));
273
- if (unmet.length === 0)
294
+ if (unmet.length === 0) {
295
+ // Older deployments have no bring-up block; their existing reconciler
296
+ // liveness predicate remains the available evidence. For deployments that
297
+ // do report process facts, an unreadable owner never becomes a green launch.
298
+ if (readiness.bring_up && !readiness.bring_up.heartbeat_readable) {
299
+ return {
300
+ kind: "operator_blocked",
301
+ id: "process-owner-unverified",
302
+ describe: "a readable reconciler process-owner heartbeat",
303
+ remedy: "Restore the process heartbeat source before launch.",
304
+ };
305
+ }
306
+ if (readiness.bring_up?.worker_present && !readiness.bring_up.single_transition_authority) {
307
+ return {
308
+ kind: "operator_blocked",
309
+ id: "process-owner-conflict",
310
+ describe: "one verified reconciler process owner",
311
+ remedy: "Resolve the missing or duplicate reconciler process before launch.",
312
+ };
313
+ }
274
314
  return { kind: "green" };
315
+ }
275
316
  const blocker = unmet.find((req) => !V2_RUNTIME_READINESS_IDS.includes(req.id));
276
317
  if (blocker) {
277
318
  return {
@@ -406,6 +447,10 @@ export function getDriveEpicUsage() {
406
447
  " non-production server and for durable hosts.",
407
448
  " --base-url <url> Explicit Bridge API target. With a profile it must",
408
449
  " agree with it — an override never rebinds a profile.",
450
+ "BAPI_EPIC_LAUNCH_PROFILE selects a host-local launch profile for epic",
451
+ "launches. --launch-profile takes precedence. For an environment-selected",
452
+ "profile, BAPI_BASE_URL remains the general CLI endpoint and does not",
453
+ "conflict with the epic launch target; --base-url still must agree.",
409
454
  "Without either, BAPI_BASE_URL is used. With none of the three, drive-epic",
410
455
  `targets production (${CONDUCTOR_DEFAULT_BASE_URL}) and uses the`,
411
456
  "stored bapi:<repo> key when one is available. A stored key approved for a",
@@ -1077,7 +1122,14 @@ export async function readLiveEpicRunByKey(access, epicKey, fetchImpl = globalTh
1077
1122
  typeof run.status !== "string") {
1078
1123
  return { kind: "unknown", reason: "the run state response was malformed" };
1079
1124
  }
1080
- return { kind: "live", epicRunId: run.epic_run_id, status: run.status };
1125
+ return {
1126
+ kind: "live",
1127
+ epicRunId: run.epic_run_id,
1128
+ status: run.status,
1129
+ ...(run.policy_json !== undefined
1130
+ ? { effectivePolicy: effectivePolicyFacts(run.policy_json, "stored") }
1131
+ : {}),
1132
+ };
1081
1133
  }
1082
1134
  catch (err) {
1083
1135
  if (err instanceof ConductorBridgeApiError) {
@@ -1104,8 +1156,8 @@ export function createDefaultDriveEpicDeps() {
1104
1156
  ...(cwd ? { cwd } : {}),
1105
1157
  });
1106
1158
  return result.ok
1107
- ? { ok: true, access: result.access, profile: result.profile }
1108
- : { ok: false, error: result.error };
1159
+ ? { ok: true, access: result.access, profile: result.profile, targetSource: result.targetSource }
1160
+ : { ok: false, error: result.error, kind: result.kind, ...(result.baseUrl ? { baseUrl: result.baseUrl } : {}) };
1109
1161
  },
1110
1162
  // Non-secret, and only ever a value the resolver just agreed with the
1111
1163
  // environment on (a conflicting BAPI_BASE_URL was refused above), so the
@@ -1114,7 +1166,7 @@ export function createDefaultDriveEpicDeps() {
1114
1166
  process.env.BAPI_BASE_URL = baseUrl;
1115
1167
  },
1116
1168
  readReadiness: (access) => fetchConductorReadiness(access, globalThis.fetch),
1117
- runSetupEpic: (argv) => runSetupEpicWorkflow(argv),
1169
+ runSetupEpic: (argv, handoff) => runSetupEpicWorkflow(argv, {}, handoff),
1118
1170
  readEligibilityArtifact: (path) => readFile(path, "utf8"),
1119
1171
  runPlanePreflight: (repoRoot) => runDefaultPlanePreflight(repoRoot),
1120
1172
  runPlane: (argv, overrides) => runPlaneCli(argv, overrides ?? {}),
@@ -1167,7 +1219,7 @@ export function createDefaultDriveEpicDeps() {
1167
1219
  readPlanDag: (filePath) => readFile(filePath, "utf8"),
1168
1220
  readPolicyFile: (filePath) => readFile(filePath, "utf8"),
1169
1221
  readLiveEpicRun: (access, epicKey) => readLiveEpicRunByKey(access, epicKey),
1170
- previewSetupEpic: async ({ argv, baseUrl }) => {
1222
+ previewSetupEpic: async ({ argv, baseUrl, handoff }) => {
1171
1223
  const stdout = [];
1172
1224
  const stderr = [];
1173
1225
  const result = await runSetupEpicWorkflow(argv, {
@@ -1176,7 +1228,7 @@ export function createDefaultDriveEpicDeps() {
1176
1228
  env: { ...process.env, BAPI_BASE_URL: baseUrl },
1177
1229
  log: (message) => stdout.push(message),
1178
1230
  errorLog: (message) => stderr.push(message),
1179
- });
1231
+ }, handoff);
1180
1232
  return { exitCode: result.exitCode, stdout, stderr };
1181
1233
  },
1182
1234
  // Read-only by construction: `prepareRunExecutor` runs `git rev-parse` and
@@ -1251,6 +1303,7 @@ export async function runDriveEpicCli(argv, overrides = {}) {
1251
1303
  options: parsed.options,
1252
1304
  access: accessResult.access,
1253
1305
  profile: launchProfile,
1306
+ targetSource: accessResult.targetSource ?? "default",
1254
1307
  };
1255
1308
  // BAPI-1285 — with no `--plan-file`, look for the plan BEFORE readiness. A
1256
1309
  // discovered plan must count as a supplied one for the BAPI-1153 cold-start
@@ -1319,6 +1372,16 @@ export async function runDriveEpicCli(argv, overrides = {}) {
1319
1372
  }
1320
1373
  return escalate(deps, epicKey, `conductor readiness could not be read: ${safeDiagnosticMessage(err, "readiness request failed")}`);
1321
1374
  }
1375
+ if (ctx.options.planFile !== undefined) {
1376
+ const prepared = await prepareSetupSelection(ctx);
1377
+ if (!prepared.ok) {
1378
+ return refuse(deps, [
1379
+ `${epicKey}: REFUSED (${prepared.reasonId}) — the selected run or policy could not be verified.`,
1380
+ "Nothing was created or started.",
1381
+ ]);
1382
+ }
1383
+ ctx = { ...ctx, handoff: prepared.handoff };
1384
+ }
1322
1385
  // --- Route. Exactly one path from here on. -------------------------------
1323
1386
  const selection = selectConductor(readiness);
1324
1387
  // BAPI-1022: surface any containment-hazard advisory before dispatch or
@@ -1377,7 +1440,7 @@ export async function runDriveEpicCli(argv, overrides = {}) {
1377
1440
  const classification = classifyV2Readiness(readiness, {
1378
1441
  attended: parsed.options.attended === true,
1379
1442
  intoBase: parsed.options.intoBase === true,
1380
- hasPolicyFile: parsed.options.policyFile !== undefined,
1443
+ effectivePolicy: ctx.handoff?.selection,
1381
1444
  });
1382
1445
  // Green: create the run, then make sure something will claim its jobs
1383
1446
  // (BAPI-1279). Before this, green delegated to setup-epic and nothing
@@ -1594,6 +1657,61 @@ function escalate(deps, epicKey, reason) {
1594
1657
  deps.errorLog(text);
1595
1658
  return 1;
1596
1659
  }
1660
+ /** Resolve policy intent once for the setup delegation, after a fresh run read. */
1661
+ async function prepareSetupSelection(ctx, coldStart = false) {
1662
+ const live = coldStart ? { kind: "none" } : await ctx.deps.readLiveEpicRun(ctx.access, ctx.epicKey);
1663
+ if (live.kind === "conflict")
1664
+ return { ok: false, reasonId: "live-run-ambiguous" };
1665
+ if (live.kind === "unknown")
1666
+ return { ok: false, reasonId: "live-run-unknown" };
1667
+ const explicit = ctx.options.policyFile;
1668
+ const selectedPath = explicit ?? (live.kind === "none" ? ctx.profile?.policyRef?.path : undefined);
1669
+ let pinnedPolicy;
1670
+ let digest = null;
1671
+ if (selectedPath !== undefined) {
1672
+ let raw = "";
1673
+ const read = await readSetupEpicPolicyFile(selectedPath, async (filePath) => {
1674
+ raw = await ctx.deps.readPolicyFile(filePath);
1675
+ return raw;
1676
+ });
1677
+ if (!read.ok)
1678
+ return { ok: false, reasonId: "policy-file-unreadable" };
1679
+ pinnedPolicy = read.policy;
1680
+ digest = epicLaunchPolicyDigest(raw);
1681
+ }
1682
+ const policy = coldStart
1683
+ ? { kind: "deferred_cold_preview", source: "deferred", digest: null }
1684
+ : live.kind === "live"
1685
+ ? explicit && digest
1686
+ ? { kind: "explicit_replacement_request", source: "explicit", digest }
1687
+ : { kind: "approved_stored", source: "stored", digest: null }
1688
+ : { kind: "new_run", source: explicit ? "explicit" : selectedPath ? "profile" : "default", digest };
1689
+ const selection = resolveEpicLaunchSelection({
1690
+ baseUrl: ctx.access.baseUrl,
1691
+ repoName: ctx.access.repoName,
1692
+ targetSource: ctx.targetSource,
1693
+ profile: ctx.profile,
1694
+ topology: planeEndpointForBaseUrl(ctx.deps, ctx.access.baseUrl) ? "local_plane" : "remote",
1695
+ policy,
1696
+ escalationChannel: pinnedPolicy
1697
+ ? policyDeclaresNotifyWebhook(pinnedPolicy) ? "configured webhook" : "account email"
1698
+ : live.kind === "live" ? "unavailable" : "account email",
1699
+ });
1700
+ if (pinnedPolicy)
1701
+ pinEpicLaunchPolicy(selection, pinnedPolicy);
1702
+ return { ok: true, handoff: { selection, access: ctx.access, profile: ctx.profile } };
1703
+ }
1704
+ async function runSelectedSetupEpic(ctx) {
1705
+ const prepared = ctx.handoff
1706
+ ? { ok: true, handoff: ctx.handoff }
1707
+ : await prepareSetupSelection(ctx);
1708
+ if (!prepared.ok) {
1709
+ ctx.deps.errorLog(`${ctx.epicKey}: REFUSED (${prepared.reasonId}) — the selected run or policy could not be verified.`);
1710
+ return { exitCode: 1 };
1711
+ }
1712
+ const result = await ctx.deps.runSetupEpic(buildSetupEpicArgv(ctx.options), prepared.handoff);
1713
+ return result;
1714
+ }
1597
1715
  /** Emit one refusal, guarded so it can never name a second conductor. */
1598
1716
  function refuse(deps, lines) {
1599
1717
  const text = lines.join("\n");
@@ -1601,6 +1719,11 @@ function refuse(deps, lines) {
1601
1719
  deps.errorLog(text);
1602
1720
  return 1;
1603
1721
  }
1722
+ function durableActorAdmissionDenied(readiness) {
1723
+ const evidence = readiness.admission_evidence;
1724
+ return evidence?.schema_version === 1 &&
1725
+ evidence.records.some((record) => record.predicate === "durable_actor" && record.verdict === "denied");
1726
+ }
1604
1727
  /**
1605
1728
  * The plane endpoint, when `baseUrl` names it; otherwise `null`.
1606
1729
  *
@@ -1706,10 +1829,18 @@ async function coldStartPlane(ctx) {
1706
1829
  ...LAUNCHED_PLANE_LEFT_RUNNING,
1707
1830
  ]);
1708
1831
  }
1832
+ const prepared = await prepareSetupSelection(ctx);
1833
+ if (!prepared.ok) {
1834
+ return refuse(deps, [
1835
+ `${epicKey}: REFUSED (${prepared.reasonId}) — the selected run or policy could not be verified.`,
1836
+ ...LAUNCHED_PLANE_LEFT_RUNNING,
1837
+ ]);
1838
+ }
1839
+ ctx = { ...ctx, handoff: prepared.handoff };
1709
1840
  const classification = classifyV2Readiness(readiness, {
1710
1841
  attended: ctx.options.attended === true,
1711
1842
  intoBase: ctx.options.intoBase === true,
1712
- hasPolicyFile: ctx.options.policyFile !== undefined,
1843
+ effectivePolicy: ctx.handoff?.selection,
1713
1844
  });
1714
1845
  if (classification.kind === "operator_blocked") {
1715
1846
  return refuse(deps, [
@@ -1755,6 +1886,12 @@ async function attachToPlane(ctx, manifest, readiness) {
1755
1886
  "Nothing was started. Fix the credential and run drive-epic again.",
1756
1887
  ]);
1757
1888
  }
1889
+ if (rooted.access.repoName !== ctx.access.repoName || rooted.access.baseUrl !== ctx.access.baseUrl) {
1890
+ return refuse(deps, [
1891
+ `${epicKey}: REFUSED (selection-conflict) — manifest-root credentials resolved a different repository or endpoint.`,
1892
+ "Nothing was started or changed.",
1893
+ ]);
1894
+ }
1758
1895
  const attachCtx = { ...ctx, access: rooted.access };
1759
1896
  // R-7, decided BEFORE any create request. It binds the LANES only
1760
1897
  // (BAPI-1279): a plane whose lanes serve another run still has a reconciler
@@ -1816,7 +1953,7 @@ async function createRunAndRequestLanes(ctx, plane) {
1816
1953
  const { deps, epicKey } = ctx;
1817
1954
  const launched = plane.origin === "launched";
1818
1955
  // ---- PHASE TWO, part one: create the run ------------------------------
1819
- const setup = await deps.runSetupEpic(buildSetupEpicArgv(ctx.options));
1956
+ const setup = await runSelectedSetupEpic(ctx);
1820
1957
  if (setup.exitCode !== 0) {
1821
1958
  // The control plane is deliberately LEFT RUNNING, and NO EXECUTOR LANE
1822
1959
  // was spawned — there is nothing to claim a job that may not exist.
@@ -2107,11 +2244,17 @@ function liveRunUnknownRefusal(epicKey, reason) {
2107
2244
  async function createRunWithRunExecutor(ctx, readiness) {
2108
2245
  const { deps, epicKey, access, profile } = ctx;
2109
2246
  if (profile !== null && profile.executionMode === "durable") {
2247
+ if (durableActorAdmissionDenied(readiness)) {
2248
+ return refuse(deps, [
2249
+ `${epicKey}: REFUSED (actor-admission-denied) — durable actor admission was denied at the selected target.`,
2250
+ "No run was created and no local executor was started.",
2251
+ ]);
2252
+ }
2110
2253
  const coverage = readiness.executor.repo_wide_executor_live;
2111
2254
  if (coverage !== true) {
2112
2255
  return refuse(deps, durableCoverageRefusal(epicKey, access, profile, coverage, "No run was created").lines);
2113
2256
  }
2114
- const setup = await deps.runSetupEpic(buildSetupEpicArgv(ctx.options));
2257
+ const setup = await runSelectedSetupEpic(ctx);
2115
2258
  if (setup.exitCode === 0) {
2116
2259
  deps.log([
2117
2260
  "",
@@ -2126,7 +2269,7 @@ async function createRunWithRunExecutor(ctx, readiness) {
2126
2269
  if (!prepared.ok) {
2127
2270
  return refuse(deps, executorPreconditionRefusal(epicKey, prepared.message, "No run was created.").lines);
2128
2271
  }
2129
- const setup = await deps.runSetupEpic(buildSetupEpicArgv(ctx.options));
2272
+ const setup = await runSelectedSetupEpic(ctx);
2130
2273
  if (setup.exitCode !== 0) {
2131
2274
  // setup-epic already said why. Nothing was started for a run that may not exist.
2132
2275
  deps.errorLog(`\n${epicKey}: no executor was started.`);
@@ -2164,6 +2307,11 @@ async function createRunWithRunExecutor(ctx, readiness) {
2164
2307
  function reportExecutorOutcome(ctx, ensured, epicRunId, framing) {
2165
2308
  const { deps, epicKey, access } = ctx;
2166
2309
  if (!ensured.ok) {
2310
+ deps.log(JSON.stringify({
2311
+ outcome: "run_exists_executor_unavailable",
2312
+ epic_key: epicKey,
2313
+ epic_run_id: epicRunId,
2314
+ }));
2167
2315
  return refuse(deps, [
2168
2316
  "",
2169
2317
  framing === "created"
@@ -2213,6 +2361,9 @@ async function planeServesLiveRun(ctx, live) {
2213
2361
  const manifest = await findAttachablePlane(ctx);
2214
2362
  if (manifest === null || manifest.epicRunId !== live.epicRunId)
2215
2363
  return false;
2364
+ const owner = await ctx.deps.readPlaneReconcilerState({ access: ctx.access, manifest });
2365
+ if (owner !== "fresh")
2366
+ return false;
2216
2367
  const bound = await ctx.deps.readRunState(ctx.access, manifest.epicRunId);
2217
2368
  return bound.epicKey === ctx.epicKey && !isTerminalEpicRunStatus(bound.status);
2218
2369
  }
@@ -2236,7 +2387,7 @@ async function attachToLiveRun(ctx, readiness, live) {
2236
2387
  const classification = classifyV2Readiness(readiness, {
2237
2388
  attended: ctx.options.attended === true,
2238
2389
  intoBase: ctx.options.intoBase === true,
2239
- hasPolicyFile: ctx.options.policyFile !== undefined,
2390
+ approvedStoredPolicy: true,
2240
2391
  });
2241
2392
  if (classification.kind === "operator_blocked") {
2242
2393
  return refuse(deps, [
@@ -2252,9 +2403,28 @@ async function attachToLiveRun(ctx, readiness, live) {
2252
2403
  return refuseRemoteRuntime(ctx, readiness);
2253
2404
  return refuse(deps, localReconcilerRefusal(epicKey, readiness).lines);
2254
2405
  }
2406
+ if (planeTarget) {
2407
+ const manifest = await findAttachablePlane(ctx).catch(() => null);
2408
+ if (manifest?.epicRunId === live.epicRunId) {
2409
+ const admission = await checkPlaneAdmission(ctx, readiness, manifest);
2410
+ if (admission !== null)
2411
+ return refuse(deps, admission);
2412
+ }
2413
+ }
2255
2414
  deps.log(`${epicKey}: run ${live.epicRunId} is already live (${live.status}) and no plan was supplied or ` +
2256
2415
  "found, so nothing will be created; making sure it has an executor.");
2416
+ if (live.effectivePolicy) {
2417
+ deps.log(`Effective policy: stored; serial execution; caps ${live.effectivePolicy.caps ? JSON.stringify(live.effectivePolicy.caps) : "unavailable"}; ` +
2418
+ `timeouts ${live.effectivePolicy.job_timeouts ? JSON.stringify(live.effectivePolicy.job_timeouts) : "unavailable"}; ` +
2419
+ `channel ${live.effectivePolicy.escalation_channel}.`);
2420
+ }
2257
2421
  if (profile !== null && profile.executionMode === "durable") {
2422
+ if (durableActorAdmissionDenied(readiness)) {
2423
+ return refuse(deps, [
2424
+ `${epicKey}: REFUSED (actor-admission-denied) — durable actor admission was denied at the selected target.`,
2425
+ "No run was changed and no local executor was started.",
2426
+ ]);
2427
+ }
2258
2428
  const coverage = readiness.executor.repo_wide_executor_live;
2259
2429
  if (coverage !== true) {
2260
2430
  return refuse(deps, durableCoverageRefusal(epicKey, access, profile, coverage, "No run was changed").lines);
@@ -2339,6 +2509,16 @@ function reportControlPlaneLaunchFailure(ctx, controlPlane) {
2339
2509
  "No run was created.",
2340
2510
  ]);
2341
2511
  }
2512
+ export const DRIVE_EPIC_ACCESS_REFUSAL_REASON_IDS = {
2513
+ "profile-unusable": "access-profile-unusable",
2514
+ "target-missing": "access-target-missing",
2515
+ "target-conflict": "access-target-conflict",
2516
+ "target-invalid": "access-target-invalid",
2517
+ "repository-conflict": "access-repository-conflict",
2518
+ "repo-missing": "access-repo-missing",
2519
+ "credentials-unavailable": "access-credentials-unavailable",
2520
+ "credential-unbound": "access-credential-unbound",
2521
+ };
2342
2522
  /** The readiness reason a preview reports for a generic not-ready engine. */
2343
2523
  const ENGINE_NOT_READY_REASON_ID = "engine-not-ready";
2344
2524
  const ENGINE_NOT_READY_REMEDY = "Resolve conductor readiness (`install-bridge conductor`, then `doctor`) and run drive-epic again.";
@@ -2370,12 +2550,48 @@ function readSetupPreviewPayload(stdout) {
2370
2550
  if (existing !== null && existing !== undefined && typeof existing !== "string")
2371
2551
  return null;
2372
2552
  const branch = doc.feature_branch;
2373
- if (branch !== undefined && typeof branch !== "string")
2553
+ if (branch !== undefined && branch !== null && typeof branch !== "string")
2374
2554
  return null;
2555
+ const rawFacts = doc.effective_policy;
2556
+ let effectivePolicy = null;
2557
+ if (rawFacts !== undefined && rawFacts !== null) {
2558
+ if (typeof rawFacts !== "object" || Array.isArray(rawFacts))
2559
+ return null;
2560
+ const facts = rawFacts;
2561
+ if (!["explicit", "profile", "default", "stored"].includes(String(facts.source)) ||
2562
+ facts.serial_execution !== "serial" ||
2563
+ !["attended", "unattended"].includes(String(facts.posture)) ||
2564
+ !["configured webhook", "account email", "unavailable"].includes(String(facts.escalation_channel)))
2565
+ return null;
2566
+ const numericMap = (value) => {
2567
+ if (value === null)
2568
+ return null;
2569
+ if (!value || typeof value !== "object" || Array.isArray(value))
2570
+ return undefined;
2571
+ const entries = Object.entries(value);
2572
+ if (entries.some(([key, number]) => !/^[a-z][a-z0-9_]{0,63}$/.test(key) ||
2573
+ typeof number !== "number" || !Number.isFinite(number)))
2574
+ return undefined;
2575
+ return Object.fromEntries(entries);
2576
+ };
2577
+ const caps = numericMap(facts.caps);
2578
+ const jobTimeouts = numericMap(facts.job_timeouts);
2579
+ if (caps === undefined || jobTimeouts === undefined)
2580
+ return null;
2581
+ effectivePolicy = {
2582
+ source: facts.source,
2583
+ serial_execution: "serial",
2584
+ posture: facts.posture,
2585
+ caps,
2586
+ job_timeouts: jobTimeouts,
2587
+ escalation_channel: facts.escalation_channel,
2588
+ };
2589
+ }
2375
2590
  return {
2376
2591
  existingRunId: typeof existing === "string" && existing !== "" ? existing : null,
2377
2592
  featureBranch: typeof branch === "string" && branch !== "" ? branch : null,
2378
2593
  serverValidated: doc.server_validated,
2594
+ effectivePolicy,
2379
2595
  };
2380
2596
  }
2381
2597
  /** The plan DAG's node count, or `null` unless it parses to `{ nodes: [...] }`. */
@@ -2452,62 +2668,135 @@ async function previewDecision(deps, options, preview, refuseWithReadiness, refu
2452
2668
  });
2453
2669
  // --- Target: resolved, never adopted. -------------------------------------
2454
2670
  const accessResult = await deps.resolveAccess(options.repo, undefined, launchSelectionOf(options));
2455
- if (!accessResult.ok)
2456
- return unknownReadiness();
2671
+ if (!accessResult.ok) {
2672
+ if (!accessResult.kind ||
2673
+ !Object.prototype.hasOwnProperty.call(DRIVE_EPIC_ACCESS_REFUSAL_REASON_IDS, accessResult.kind))
2674
+ return unknownReadiness();
2675
+ unknownReadiness();
2676
+ if (accessResult.baseUrl) {
2677
+ preview.target = {
2678
+ base_url: accessResult.baseUrl,
2679
+ kind: planeEndpointForBaseUrl(deps, accessResult.baseUrl) !== null ? "local_plane" : "remote",
2680
+ };
2681
+ }
2682
+ refuseTopLevel({
2683
+ reasonId: DRIVE_EPIC_ACCESS_REFUSAL_REASON_IDS[accessResult.kind],
2684
+ remedy: accessResult.error,
2685
+ });
2686
+ return;
2687
+ }
2457
2688
  const access = accessResult.access;
2458
2689
  const profile = accessResult.profile ?? null;
2459
- const planeTarget = planeEndpointForBaseUrl(deps, access.baseUrl) !== null;
2460
- preview.target = { base_url: access.baseUrl, kind: planeTarget ? "local_plane" : "remote" };
2461
- const ctx = { deps, epicKey, options, access, profile };
2690
+ const endpoint = planeEndpointForBaseUrl(deps, access.baseUrl);
2691
+ const planeTarget = endpoint !== null;
2692
+ preview.target = {
2693
+ base_url: access.baseUrl,
2694
+ kind: planeTarget ? "local_plane" : "remote",
2695
+ ...(accessResult.targetSource === "profile" || accessResult.targetSource === "profile-env"
2696
+ ? { source: accessResult.targetSource }
2697
+ : {}),
2698
+ };
2699
+ const ctx = { deps, epicKey, options, access, profile, targetSource: accessResult.targetSource ?? "default" };
2700
+ // Discover before readiness so a selected local plane can preview its guarded
2701
+ // cold start. Keep the candidate out of ordinary failure documents.
2702
+ let planPath = options.planFile;
2703
+ let plan = { status: "not_found" };
2704
+ let docsRootUnresolved = false;
2705
+ if (planPath !== undefined) {
2706
+ plan = { status: "supplied", path: planPath };
2707
+ }
2708
+ else {
2709
+ const docs = await deps.resolveDocsDir();
2710
+ if (!docs.ok) {
2711
+ docsRootUnresolved = true;
2712
+ }
2713
+ else {
2714
+ const discovered = await discoverEpicPlan(epicKey, docs.docsDir, deps.discoveryFs);
2715
+ plan = {
2716
+ status: discovered.status === "found" ? "found" : discovered.status === "plan_ambiguous" ? "ambiguous" : "not_found",
2717
+ ...(discovered.status === "found" ? { path: discovered.path } : {}),
2718
+ ...(discovered.status === "plan_ambiguous" ? { candidates: [...discovered.candidates] } : {}),
2719
+ ...(discovered.skipped.length > 0
2720
+ ? { skipped: discovered.skipped.map((entry) => ({ path: entry.path, reason: entry.reason })) }
2721
+ : {}),
2722
+ epic_plans_dir: discovered.epic_plans_dir,
2723
+ };
2724
+ if (discovered.status === "found")
2725
+ planPath = discovered.path;
2726
+ }
2727
+ }
2462
2728
  // --- Readiness. -----------------------------------------------------------
2463
2729
  let readiness;
2464
2730
  try {
2465
2731
  readiness = await deps.readReadiness(access);
2466
2732
  }
2467
- catch {
2733
+ catch (err) {
2734
+ if (planPath !== undefined && endpoint !== null && profile?.executionMode !== "durable" &&
2735
+ err instanceof ConductorBridgeApiError && err.kind === "network") {
2736
+ const probe = await deps.probePlanePort(endpoint.host, endpoint.port).catch(() => ({ kind: "error" }));
2737
+ if (probe.kind === "refused") {
2738
+ const preflight = await deps.runPlanePreflight(deps.cwd());
2739
+ const blocker = firstPreconditionRefusal(preflight) ??
2740
+ preflight.diagnostics.find((diagnostic) => diagnostic.severity === "blocking");
2741
+ preview.plan = plan;
2742
+ if (blocker) {
2743
+ return refuseTopLevel({
2744
+ reasonId: blocker.check,
2745
+ remedy: `The local plane preflight refused ${blocker.check}. Fix that condition and preview again.`,
2746
+ });
2747
+ }
2748
+ if (!preflight.ok) {
2749
+ return refuseTopLevel({
2750
+ reasonId: "plane-preflight-failed",
2751
+ remedy: "The local plane preflight could not admit guarded startup. Fix its reported condition and preview again.",
2752
+ });
2753
+ }
2754
+ preview.readiness = { kind: "cold_start" };
2755
+ await finalizeCreatePreview(ctx, preview, planPath, true, refuseTopLevel);
2756
+ if (preview.decision === "create") {
2757
+ preview.cold_start = { plane_endpoint: access.baseUrl, will_start_plane: true, live_run_checked: false };
2758
+ }
2759
+ return;
2760
+ }
2761
+ }
2468
2762
  return unknownReadiness();
2469
2763
  }
2764
+ preview.plan = plan;
2765
+ if (docsRootUnresolved)
2766
+ return refuseTopLevel(docsRootRefusal(epicKey));
2767
+ const live = await deps.readLiveEpicRun(access, epicKey);
2768
+ if (live.kind === "conflict")
2769
+ return refuseTopLevel(liveRunAmbiguousRefusal(epicKey));
2770
+ if (live.kind === "unknown")
2771
+ return refuseTopLevel(liveRunUnknownRefusal(epicKey, live.reason));
2772
+ const previewCtx = { ...ctx, options: { ...options, ...(planPath ? { planFile: planPath } : {}) } };
2773
+ const prepared = planPath ? await prepareSetupSelection(previewCtx) : null;
2774
+ if (prepared !== null && !prepared.ok) {
2775
+ return refuseTopLevel({
2776
+ reasonId: prepared.reasonId,
2777
+ remedy: "The selected run or policy could not be verified. Nothing was created or started.",
2778
+ });
2779
+ }
2470
2780
  const classification = classifyV2Readiness(readiness, {
2471
2781
  attended: options.attended === true,
2472
2782
  intoBase: options.intoBase === true,
2473
- hasPolicyFile: options.policyFile !== undefined,
2783
+ effectivePolicy: prepared?.ok ? prepared.handoff.selection : undefined,
2784
+ approvedStoredPolicy: live.kind === "live" && !planPath,
2474
2785
  });
2475
2786
  preview.readiness = { kind: classification.kind };
2476
2787
  const selection = selectConductor(readiness);
2477
- // --- The plan: supplied, or discovered. -----------------------------------
2478
- let planPath = options.planFile;
2479
- if (planPath !== undefined) {
2480
- preview.plan = { status: "supplied", path: planPath };
2481
- }
2482
- else {
2483
- const docs = await deps.resolveDocsDir();
2484
- if (!docs.ok)
2485
- return refuseTopLevel(docsRootRefusal(epicKey));
2486
- const discovered = await discoverEpicPlan(epicKey, docs.docsDir, deps.discoveryFs);
2487
- preview.plan = {
2488
- status: discovered.status === "found"
2489
- ? "found"
2490
- : discovered.status === "plan_ambiguous"
2491
- ? "ambiguous"
2492
- : "not_found",
2493
- ...(discovered.status === "found" ? { path: discovered.path } : {}),
2494
- ...(discovered.status === "plan_ambiguous" ? { candidates: [...discovered.candidates] } : {}),
2495
- ...(discovered.skipped.length > 0
2496
- ? { skipped: discovered.skipped.map((entry) => ({ path: entry.path, reason: entry.reason })) }
2497
- : {}),
2498
- epic_plans_dir: discovered.epic_plans_dir,
2499
- };
2500
- if (discovered.status === "found")
2501
- planPath = discovered.path;
2502
- }
2503
2788
  // --- The live run. Conflict and unknown refuse whatever the plan says. ------
2504
- const live = await deps.readLiveEpicRun(access, epicKey);
2505
- if (live.kind === "conflict")
2506
- return refuseTopLevel(liveRunAmbiguousRefusal(epicKey));
2507
- if (live.kind === "unknown")
2508
- return refuseTopLevel(liveRunUnknownRefusal(epicKey, live.reason));
2509
2789
  if (live.kind === "live") {
2510
2790
  preview.live_run = { epic_run_id: live.epicRunId, status: live.status };
2791
+ if (live.effectivePolicy) {
2792
+ preview.summary.policy_source = live.effectivePolicy.source;
2793
+ preview.summary.serial_execution = live.effectivePolicy.serial_execution;
2794
+ preview.summary.caps = live.effectivePolicy.caps;
2795
+ preview.summary.job_timeouts = live.effectivePolicy.job_timeouts;
2796
+ preview.summary.escalation_channel_label = live.effectivePolicy.escalation_channel;
2797
+ preview.summary.posture = live.effectivePolicy.posture;
2798
+ preview.summary.escalation_channel = null;
2799
+ }
2511
2800
  }
2512
2801
  // --- No plan: attach to a live run, or stop. --------------------------------
2513
2802
  if (planPath === undefined) {
@@ -2537,31 +2826,32 @@ async function previewDecision(deps, options, preview, refuseWithReadiness, refu
2537
2826
  const facts = await previewExecutorFacts(ctx, readiness, planeTarget, null, "No run was created");
2538
2827
  if (facts !== null)
2539
2828
  return refuseWithReadiness(classification.kind, facts, true);
2829
+ await finalizeCreatePreview(prepared?.ok ? { ...previewCtx, handoff: prepared.handoff } : previewCtx, preview, planPath, false, refuseTopLevel, live.kind === "live");
2830
+ }
2831
+ async function finalizeCreatePreview(ctx, preview, planPath, coldStart, refuseTopLevel, liveRunKnown = false) {
2832
+ const { deps, options, access } = ctx;
2833
+ const prepared = ctx.handoff
2834
+ ? { ok: true, handoff: ctx.handoff }
2835
+ : await prepareSetupSelection(ctx, coldStart);
2836
+ if (!prepared.ok) {
2837
+ return refuseTopLevel({
2838
+ reasonId: prepared.reasonId,
2839
+ remedy: "The selected run or policy could not be verified. Fix it and preview again.",
2840
+ });
2841
+ }
2540
2842
  // Escalation channel, decided with setup-epic's own exported policy helpers. A
2541
2843
  // policy that cannot be read is one fixed refusal; its path, contents, and any
2542
2844
  // webhook URL never leave this function.
2543
2845
  if (preview.summary.posture === "unattended") {
2544
- if (options.policyFile !== undefined) {
2545
- const policy = await readSetupEpicPolicyFile(options.policyFile, deps.readPolicyFile);
2546
- if (!policy.ok) {
2547
- return refuseTopLevel({
2548
- reasonId: "policy-file-unreadable",
2549
- remedy: "The --policy-file could not be read as a JSON object. Fix it or pass a different " +
2550
- "file, then run the preview again.",
2551
- });
2552
- }
2553
- preview.summary.escalation_channel = policyDeclaresNotifyWebhook(policy.policy)
2554
- ? "webhook"
2555
- : "email";
2556
- }
2557
- else {
2558
- preview.summary.escalation_channel = "email";
2559
- }
2846
+ preview.summary.escalation_channel = prepared.handoff.selection.escalationChannel === "configured webhook"
2847
+ ? "webhook"
2848
+ : prepared.handoff.selection.escalationChannel === "account email" ? "email" : null;
2560
2849
  }
2561
2850
  preview.summary.child_count = await readPlanChildCount(deps, planPath);
2562
2851
  const setup = await deps.previewSetupEpic({
2563
- argv: [...buildSetupEpicArgv({ ...options, planFile: planPath }), "--dry-run", "--json"],
2852
+ argv: [...buildSetupEpicArgv({ ...options, planFile: planPath }), ...(coldStart ? ["--local-only"] : []), "--dry-run", "--json"],
2564
2853
  baseUrl: access.baseUrl,
2854
+ handoff: prepared.handoff,
2565
2855
  });
2566
2856
  if (setup.exitCode !== 0) {
2567
2857
  preview.summary.server_validation = "failed";
@@ -2582,6 +2872,18 @@ async function previewDecision(deps, options, preview, refuseWithReadiness, refu
2582
2872
  }
2583
2873
  preview.summary.server_validation = payload.serverValidated ? "passed" : "partial";
2584
2874
  preview.summary.branch = payload.featureBranch;
2875
+ if (payload.effectivePolicy) {
2876
+ const facts = payload.effectivePolicy;
2877
+ preview.summary.policy_source = facts.source;
2878
+ preview.summary.serial_execution = facts.serial_execution;
2879
+ preview.summary.caps = facts.caps;
2880
+ preview.summary.job_timeouts = facts.job_timeouts;
2881
+ preview.summary.escalation_channel_label = facts.escalation_channel;
2882
+ if (!coldStart)
2883
+ preview.summary.posture = facts.posture;
2884
+ preview.summary.escalation_channel = facts.escalation_channel === "configured webhook"
2885
+ ? "webhook" : facts.escalation_channel === "account email" ? "email" : null;
2886
+ }
2585
2887
  preview.summary.auto_merge_scope =
2586
2888
  preview.summary.posture === "unattended" ? payload.featureBranch : null;
2587
2889
  // The bootstrap reusing a run IS an attach: it reports the run's id.
@@ -2593,7 +2895,7 @@ async function previewDecision(deps, options, preview, refuseWithReadiness, refu
2593
2895
  preview.decision = "attach";
2594
2896
  return;
2595
2897
  }
2596
- preview.decision = live.kind === "live" ? "attach" : "create";
2898
+ preview.decision = coldStart ? "create" : liveRunKnown ? "attach" : "create";
2597
2899
  }
2598
2900
  /**
2599
2901
  * The readiness refusal a real launch would print, as a reason id and remedy, or
@@ -2623,6 +2925,12 @@ function previewReadinessRefusal(ctx, classification, readiness, planeTarget, pa
2623
2925
  async function previewExecutorFacts(ctx, readiness, planeTarget, live, outcome) {
2624
2926
  const { deps, epicKey, access, profile } = ctx;
2625
2927
  if (profile !== null && profile.executionMode === "durable") {
2928
+ if (durableActorAdmissionDenied(readiness)) {
2929
+ return {
2930
+ reasonId: "actor-admission-denied",
2931
+ remedy: "Durable actor admission was denied at the selected target. No run was created and no local executor was started.",
2932
+ };
2933
+ }
2626
2934
  const coverage = readiness.executor.repo_wide_executor_live;
2627
2935
  return coverage === true
2628
2936
  ? null
@@ -2630,6 +2938,13 @@ async function previewExecutorFacts(ctx, readiness, planeTarget, live, outcome)
2630
2938
  }
2631
2939
  if (planeTarget) {
2632
2940
  if (live !== null) {
2941
+ const manifest = await findAttachablePlane(ctx).catch(() => null);
2942
+ if (manifest?.epicRunId === live.epicRunId) {
2943
+ const admission = await checkPlaneAdmission(ctx, readiness, manifest);
2944
+ if (admission !== null) {
2945
+ return { reasonId: "plane-reconciler-unconfirmed", remedy: admission.slice(1).join(" ") };
2946
+ }
2947
+ }
2633
2948
  if (await planeServesLiveRun(ctx, live))
2634
2949
  return null;
2635
2950
  }
@@ -2650,11 +2965,12 @@ function emitPreview(deps, options, preview) {
2650
2965
  }
2651
2966
  const lines = [
2652
2967
  `${preview.epic_key}: launch preview (read-only; nothing was created or started)`,
2968
+ ...(preview.cold_start ? [" Decision: conditional create — drive-epic will attempt guarded startup, recheck the live run, and create only if still appropriate."] : []),
2653
2969
  ` Target: ${preview.target.base_url ?? "unresolved"}${preview.target.kind ? ` (${preview.target.kind})` : ""}`,
2654
2970
  ` Readiness: ${preview.readiness.kind}${preview.readiness.reason_id ? ` (${preview.readiness.reason_id})` : ""}`,
2655
2971
  ` Live run: ${preview.live_run
2656
2972
  ? `${preview.live_run.epic_run_id}${preview.live_run.status ? ` (${preview.live_run.status})` : ""}`
2657
- : "none"}`,
2973
+ : preview.cold_start ? "unchecked (plane not yet running)" : "none"}`,
2658
2974
  ` Plan: ${preview.plan.status}${preview.plan.path ? ` — ${preview.plan.path}` : ""}`,
2659
2975
  ];
2660
2976
  for (const candidate of preview.plan.candidates ?? [])
@@ -2663,9 +2979,10 @@ function emitPreview(deps, options, preview) {
2663
2979
  lines.push(` skipped: ${skipped.path} (${skipped.reason})`);
2664
2980
  }
2665
2981
  const s = preview.summary;
2666
- lines.push(` Posture: ${s.posture ?? "n/a"}`, ` Children: ${s.child_count ?? "n/a"}`, ` Branch: ${s.branch ?? "n/a"}`, ` Auto-merge: ${s.auto_merge_scope ?? "n/a"}`, ` Escalation: ${s.escalation_channel ?? "n/a"}`, ` Validation: ${s.server_validation ?? "n/a"}`, ` Decision: ${preview.decision}`);
2667
- if (preview.readiness.remedy)
2982
+ lines.push(` Posture: ${s.posture ?? "n/a"}`, ` Children: ${s.child_count ?? "n/a"}`, ` Branch: ${s.branch ?? "n/a"}`, ` Auto-merge: ${s.auto_merge_scope ?? "n/a"}`, ` Escalation: ${s.escalation_channel ?? "n/a"}`, ` Validation: ${preview.cold_start ? "Partial — plane is not yet running" : s.server_validation ?? "n/a"}`, ` Decision: ${preview.decision}`);
2983
+ if (preview.readiness.remedy && !preview.refusal?.reason_id.startsWith("access-")) {
2668
2984
  lines.push(` Remedy: ${preview.readiness.remedy}`);
2985
+ }
2669
2986
  if (preview.refusal) {
2670
2987
  lines.push(` Refusal: ${preview.refusal.reason_id} — ${preview.refusal.remedy}`);
2671
2988
  }