@bridge_gpt/mcp-server 0.2.66 → 0.2.67

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 (100) hide show
  1. package/README.md +252 -77
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +43 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/commands.generated.js +11 -11
  8. package/build/conductor/bridge-api-client.js +143 -1
  9. package/build/conductor/bring-up-facts.js +1 -1
  10. package/build/conductor/doctor.js +3 -3
  11. package/build/conductor/epic-implementer-cli.js +22 -6
  12. package/build/conductor/errors.js +4 -3
  13. package/build/conductor/guided-repair.js +6 -2
  14. package/build/conductor/hook-runtime-qualification.js +1 -1
  15. package/build/conductor/merge-execution-contract.js +27 -8
  16. package/build/conductor/merge-execution-eligibility.js +3 -2
  17. package/build/conductor/pr-ci-producer.js +1 -1
  18. package/build/conductor/readiness-cli.js +18 -2
  19. package/build/conductor/readiness-sections.js +41 -6
  20. package/build/conductor/readiness.js +181 -29
  21. package/build/conductor/recovery-cli.js +200 -8
  22. package/build/conductor/recovery-operations.js +69 -3
  23. package/build/conductor/repair-contract.js +54 -3
  24. package/build/conductor/repository-workflow-remedy.js +125 -0
  25. package/build/conductor/tools.js +2 -2
  26. package/build/conductor-bundle-artifacts.js +8 -0
  27. package/build/docs.generated.js +8 -3
  28. package/build/drive-epic-key.js +6 -5
  29. package/build/drive-epic.js +85 -54
  30. package/build/epic-coherence-input.js +2 -1
  31. package/build/epic-implementer-bin.js +22 -20
  32. package/build/epic-implementer-bundle-cli.js +2 -2
  33. package/build/executor/cli.js +3 -2
  34. package/build/executor/completion-contract.js +48 -5
  35. package/build/executor/completion-supervision.js +4 -0
  36. package/build/executor/env.js +19 -0
  37. package/build/executor/heartbeat.js +1 -1
  38. package/build/executor/http-client.js +20 -0
  39. package/build/executor/job-errors.js +25 -0
  40. package/build/executor/job-runner.js +225 -41
  41. package/build/executor/observation.js +6 -1
  42. package/build/executor/preflight.js +51 -14
  43. package/build/executor/required-work-ledger.js +6 -1
  44. package/build/executor/resume-pre-spawn.js +2 -0
  45. package/build/executor/runner.js +8 -2
  46. package/build/executor/service-unit.js +7 -1
  47. package/build/executor/worker-finalization.js +82 -29
  48. package/build/executor/worker-guard-hook.js +137 -33
  49. package/build/executor/worktree.js +82 -1
  50. package/build/index.js +403 -429
  51. package/build/install-bridge-conductor.js +8 -6
  52. package/build/install-bridge.js +3 -3
  53. package/build/phase-context-git-probe.js +68 -0
  54. package/build/phase-result-artifacts.js +173 -5
  55. package/build/pipelines.generated.js +18 -24
  56. package/build/plane/alembic-head.js +12 -3
  57. package/build/plane/base-worktree-guard.js +147 -0
  58. package/build/plane/cli.js +217 -17
  59. package/build/plane/code-snapshot.js +468 -0
  60. package/build/plane/defaults.js +48 -3
  61. package/build/plane/endpoint.js +41 -0
  62. package/build/plane/manifest.js +120 -1
  63. package/build/plane/member-roster.js +94 -19
  64. package/build/plane/preflight.js +30 -2
  65. package/build/plane/process-cwd.js +67 -0
  66. package/build/plane/restart.js +542 -0
  67. package/build/plane/shutdown.js +31 -0
  68. package/build/plane/status.js +143 -8
  69. package/build/plane/supervisor.js +586 -179
  70. package/build/plane/types.js +54 -2
  71. package/build/printed-remedy-commands.js +37 -0
  72. package/build/readiness-check.js +1 -1
  73. package/build/readme.generated.js +1 -1
  74. package/build/review-generation.js +22 -0
  75. package/build/setup-epic.js +44 -19
  76. package/build/sfcc/config.js +9 -0
  77. package/build/sfcc/credentials.js +100 -38
  78. package/build/sfcc/hostname.js +67 -0
  79. package/build/sfcc/permissions.js +17 -13
  80. package/build/sfcc/register.js +1 -1
  81. package/build/sfcc/tool-wrapper.js +39 -43
  82. package/build/sfcc/write-grants.js +17 -11
  83. package/build/sfcc/write-result.js +4 -3
  84. package/build/sfcc/writes-site-preference.js +20 -8
  85. package/build/start-tickets.js +179 -31
  86. package/build/tool-error-envelope.js +61 -0
  87. package/build/version.generated.js +3 -3
  88. package/build/visual-diff.js +22 -1
  89. package/build/worker-guard-hook-bin.js +3 -6
  90. package/build/worker-guard-hook-main.js +47 -0
  91. package/build/worktree-core.js +130 -4
  92. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  93. package/docs/EPIC-IMPLEMENTER.md +662 -0
  94. package/docs/install/mcp-tool-integrations.md +24 -15
  95. package/docs/install/sfcc-integration.md +12 -4
  96. package/package.json +1 -1
  97. package/pipelines/plan-epic.json +0 -5
  98. package/pipelines/review-ticket.json +1 -1
  99. package/docs/CONDUCTOR.md +0 -605
  100. package/pipelines/idea-to-pr.json +0 -49
@@ -92,12 +92,14 @@
92
92
  * unrelated additive change look like a breaking one here, or worse, hide a real
93
93
  * one.
94
94
  */
95
+ import { PRINTED_CHECK_NAME_INSTRUCTION, PRINTED_CONDUCTOR_EXECUTOR_INSTALL_COMMAND, PRINTED_CONDUCTOR_INSTALL_COMMAND, PRINTED_CONNECT_GITHUB_COMMAND, PRINTED_EXECUTOR_ID_INSTRUCTION, } from "../printed-remedy-commands.js";
95
96
  import { mapConductorDoctorReportToReadinessChecks, } from "./doctor.js";
96
97
  import { mapInstallStatusChecksToReadinessChecks, } from "../install-doctor.js";
97
98
  import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
98
99
  import { runConductorInstallDoctor, } from "./install-doctor.js";
99
100
  import { BRING_UP_SECTION_LABELS, presentRequiredChecks, } from "./readiness-sections.js";
100
101
  import { classifyBringUpFacts } from "./bring-up-facts.js";
102
+ import { CI_WORKFLOW_PATH, REVIEW_WORKFLOW_PATH, ciWorkflowRemedy, repositoryWorkflowRemedy, } from "./repository-workflow-remedy.js";
101
103
  import { classifyMergeExecutionEligibility, MERGE_EXECUTION_INELIGIBLE_REMEDIATION, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
102
104
  import { createReadinessCheck, createReadinessCheckSafely, summarizeReadinessChecks, validateReadinessChecks, } from "../readiness-check.js";
103
105
  import { ADMISSION_EVIDENCE_SCHEMA_VERSION, } from "./bridge-api-client.js";
@@ -137,7 +139,10 @@ export const SERVER_READINESS_DESCRIPTORS = [
137
139
  id: "single-transition-authority",
138
140
  label: BRING_UP_SECTION_LABELS["single-transition-authority"],
139
141
  },
140
- { id: "reconciler-liveness", label: "Reconciler tick liveness" },
142
+ // The stable ID stays `reconciler-liveness`; only the label changed (BAPI-1360),
143
+ // because the row reports whether ACTIVE runs are making progress, not whether a
144
+ // reconciler process exists.
145
+ { id: "reconciler-liveness", label: "Active-run progress" },
141
146
  { id: "executor-readiness", label: "Executor server observation" },
142
147
  { id: "review-workflow", label: "Review workflow protocol" },
143
148
  { id: "conductor-ci-workflow", label: "Conductor CI migration guard" },
@@ -145,7 +150,7 @@ export const SERVER_READINESS_DESCRIPTORS = [
145
150
  // third unattended refusal reads. Noun phrases, like every row above: these
146
151
  // render as list items under a source heading, not as sentences.
147
152
  { id: "unattended-consent", label: "Repository unattended consent" },
148
- { id: "unattended-notify-default", label: "Verified notify webhook default" },
153
+ { id: "unattended-notify-default", label: "Escalation channel (webhook or email)" },
149
154
  { id: "repository-workflow-readiness", label: "Repository workflow readiness" },
150
155
  ];
151
156
  /** Fixed, secret-free prose per failure kind. Never a status body or header. */
@@ -169,23 +174,29 @@ const SERVER_FAILURE_COPY = {
169
174
  };
170
175
  /** Remediations for the individual server facts, as fixed prose. */
171
176
  const SERVER_REMEDIATIONS = {
172
- supervisorSetup: "run `install conductor` (or `setup-epic`) for this repository to write the project-default supervisor posture.",
173
- supervisorConfig: "run `install conductor` for this repository to write the project-default supervisor configuration.",
177
+ supervisorSetup: `run \`${PRINTED_CONDUCTOR_INSTALL_COMMAND}\` (or \`setup-epic\`) for this repository to write the project-default supervisor posture. ${PRINTED_CHECK_NAME_INSTRUCTION}`,
178
+ supervisorConfig: `run \`${PRINTED_CONDUCTOR_INSTALL_COMMAND}\` for this repository to write the project-default supervisor configuration. ${PRINTED_CHECK_NAME_INSTRUCTION}`,
174
179
  mergeApproval: "set the merge-approval requirement explicitly in the supervisor configuration rather than leaving it unset.",
175
180
  reviewPolicyMissing: "set the repository review-policy default so the merge path and the code_review gate read the same signal.",
176
181
  reviewPolicyDivergent: "align the repository review-policy default with the done gate's review_state condition, or confirm the difference is intended.",
177
- githubCredentials: "connect GitHub from the Bridge setup UI (`install-bridge connect-github`) so owner, repository id, and installation id all resolve.",
182
+ githubCredentials: `connect GitHub from the Bridge setup UI or run \`${PRINTED_CONNECT_GITHUB_COMMAND}\` so owner, repository id, and installation id all resolve.`,
178
183
  actionsProbeFailed: "re-run once credentials resolve; the permission could not be checked, which is not the same as confirmed-missing.",
179
184
  actionsMissing: "grant the GitHub App `actions: write`; without it the conductor's workflow rerun lane fails open on 403.",
180
185
  reconcilerUnreadable: "retry once the Bridge API is reachable; reconciler liveness is unknown here, not confirmed stale.",
181
- reconcilerStale: "start the reconciler for this repository (the `epic-implementer epic-tick` schedule) so ticks resume.",
186
+ // BAPI-1360: the frozen `epic-tick` command is never named. The reconciler runs
187
+ // inside the worker, so the first check is that the worker is up, then the run's
188
+ // own status.
189
+ reconcilerStale: "check the worker first (`server.worker-present`), then inspect the stalled run with `epic-implementer epic-status`.",
182
190
  executorUnreadable: "retry once the Bridge API is reachable; no executor readiness claim is ever made from local state.",
183
- executorNeverSeen: "provision and start an executor (`install conductor --executor-id <id> --repo-wide` or `executor install-service`); readiness comes only from a live server observation.",
191
+ executorNeverSeen: `provision and start an executor (\`${PRINTED_CONDUCTOR_EXECUTOR_INSTALL_COMMAND}\` or \`executor install-service\`); readiness comes only from a live server observation. ${PRINTED_CHECK_NAME_INSTRUCTION} ${PRINTED_EXECUTOR_ID_INSTRUCTION}`,
192
+ executorNoneObservedNoRuns: `no action is needed to launch: \`drive-epic\` starts a run-scoped executor. To keep an executor running between runs, install one with \`${PRINTED_CONDUCTOR_EXECUTOR_INSTALL_COMMAND}\` or \`executor install-service\`. ${PRINTED_CHECK_NAME_INSTRUCTION} ${PRINTED_EXECUTOR_ID_INSTRUCTION}`,
184
193
  executorStale: "restart the executor service; it was observed once and has gone quiet past the server's staleness threshold.",
185
194
  reviewWorkflowUnknown: "re-run; the default branch's review workflow could not be evaluated, which is not the same as it being absent.",
186
195
  reviewWorkflowIncomplete: "install the claude-review workflow template on the default branch so it emits both the run id and the reviewed SHA the verdict protocol reads.",
187
196
  ciWorkflowUnknown: "re-run; the default branch's conductor CI workflow could not be evaluated, which is not the same as the guard being absent.",
188
- ciWorkflowMissingGuard: "add the migration-guard job to the conductor CI workflow on the default branch so migrations are checked before a merge.",
197
+ // BAPI-1359: the missing-guard remedy is built by `ciWorkflowRemedy` (the shared
198
+ // formatter), because its wording depends on WHY the guard is missing — a
199
+ // confirmed Alembic chain, an older server, or an unreadable answer.
189
200
  olderServer: "upgrade the Bridge API deploy so it reports this fact; its state is unknown here, not healthy.",
190
201
  // BAPI-1102. Each names the ONE action that clears it, and none of them
191
202
  // mentions a webhook URL, a destination, or any configuration value.
@@ -193,7 +204,10 @@ const SERVER_REMEDIATIONS = {
193
204
  // admin panel's Setup page. No MCP or CLI surface ever writes it.
194
205
  unattendedConsent: "turn on 'Allow unattended Epic Implementer runs' on the repository's Setup page in the Bridge admin panel, or run setup with --attended. If it is already on, contact Bridge support.",
195
206
  unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or add an email address to the account so escalation alerts can be emailed, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
196
- repositoryWorkflowReadiness: "run `install-bridge conductor` so the review and conductor-CI workflows are confirmed on the default branch, or run setup with --attended.",
207
+ // BAPI-1359: the `repository-workflow-readiness` remedy is built by
208
+ // `repositoryWorkflowRemedy` (the shared formatter) from the SAME report, so it
209
+ // names the specific missing item. The generic "run `install-bridge
210
+ // conductor`" text was retired: no install step creates a migration-guard job.
197
211
  // BAPI-1135. Both are IMPORTED from the shared classifier rather than written
198
212
  // out here, so the remedy command has exactly one spelling across the refusal,
199
213
  // the readiness row, and the doctor — see `merge-execution-contract.ts`.
@@ -331,9 +345,16 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
331
345
  label,
332
346
  status: outcome.status,
333
347
  detail: outcome.detail,
348
+ // A pass carries no remediation, and a `skip` carries one only when its
349
+ // outcome supplied one: the older-server default would misdescribe a fact
350
+ // that was deliberately not evaluated.
334
351
  ...(outcome.status === "pass"
335
352
  ? {}
336
- : { remediation: outcome.remediation ?? SERVER_REMEDIATIONS.olderServer }),
353
+ : outcome.status === "skip"
354
+ ? outcome.remediation === undefined
355
+ ? {}
356
+ : { remediation: outcome.remediation }
357
+ : { remediation: outcome.remediation ?? SERVER_REMEDIATIONS.olderServer }),
337
358
  });
338
359
  });
339
360
  // BAPI-1260 — appended ONLY when the server reported the block, so every
@@ -377,6 +398,44 @@ const ADMISSION_OWNER_REMEDIATION = {
377
398
  repository_admin: "a repository administrator resolves it (unattended consent is the 'Allow unattended Epic Implementer runs' toggle on the repository's Setup page in the Bridge admin panel); setup never grants it.",
378
399
  bridge_operations: "escalate to Bridge operations; this is not something an operator command can change.",
379
400
  };
401
+ /** The retired and current Bridge CI gate names, spelled once for the fixed remedies. */
402
+ const RETIRED_GATE_NAME = "conductor-ci / gate";
403
+ const CURRENT_GATE_NAME = "epic-implementer-ci / gate";
404
+ /**
405
+ * Fixed, per-reason remedies (BAPI-1360). Assembled from literals and the two
406
+ * hard-coded workflow paths only: a record's reason picks a template, and nothing
407
+ * from the payload is interpolated. `null` means "no specific remedy — use the
408
+ * owner's generic one".
409
+ */
410
+ function admissionReasonRemedy(record, readiness) {
411
+ const sup = readiness.supervisor;
412
+ const ciAbsent = sup.required_checks_ci_workflow_absent === true ||
413
+ (readiness.conductor_ci_workflow?.probe_succeeded === true &&
414
+ readiness.conductor_ci_workflow?.workflow_present === false);
415
+ const reviewAbsent = sup.required_checks_review_workflow_absent === true ||
416
+ (readiness.review_workflow?.probe_succeeded === true &&
417
+ readiness.review_workflow?.workflow_present === false);
418
+ switch (record.reason) {
419
+ case "review_workflow_unconfirmed":
420
+ return (`a repository administrator adds ${REVIEW_WORKFLOW_PATH} to the default branch, or repairs it so it ` +
421
+ "emits the run-id: and Reviewed-SHA: markers in its verdict comment; setup never grants it.");
422
+ case "conductor_ci_workflow_unconfirmed":
423
+ return (`a repository administrator adds ${CI_WORKFLOW_PATH} to the default branch, or repairs it (a ` +
424
+ "migration-guard job is required when the repository has alembic.ini); setup never grants it.");
425
+ case "required_checks_lane_workflow_absent": {
426
+ const files = [ciAbsent ? CI_WORKFLOW_PATH : null, reviewAbsent ? REVIEW_WORKFLOW_PATH : null].filter((file) => file !== null);
427
+ const named = files.length > 0 ? files.join(" and ") : `${CI_WORKFLOW_PATH} and ${REVIEW_WORKFLOW_PATH}`;
428
+ return (`a repository administrator adds ${named} to the default branch so the configured required ` +
429
+ `checks (such as ${CURRENT_GATE_NAME}) have a producer; setup never grants it.`);
430
+ }
431
+ case "required_check_retired_gate":
432
+ return (`a repository administrator replaces ${RETIRED_GATE_NAME} with ${CURRENT_GATE_NAME} in the plan's ` +
433
+ "required_checks, in the supervisor done-gate checks, and in branch protection." +
434
+ (ciAbsent ? ` Also add ${CI_WORKFLOW_PATH} to the default branch so ${CURRENT_GATE_NAME} has a producer.` : ""));
435
+ default:
436
+ return null;
437
+ }
438
+ }
380
439
  /**
381
440
  * Project the server's typed launch evidence (BAPI-1260) into `server.*`
382
441
  * checks, read-only.
@@ -405,13 +464,30 @@ export function admissionEvidenceChecks(readiness) {
405
464
  }),
406
465
  ];
407
466
  }
408
- return evidence.records.map((record) => admissionEvidenceCheck(record));
467
+ return evidence.records.map((record) => admissionEvidenceCheck(record, readiness));
409
468
  }
410
- function admissionEvidenceCheck(record) {
469
+ function admissionEvidenceCheck(record, readiness) {
411
470
  const slug = record.predicate.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
412
471
  const label = ADMISSION_EVIDENCE_LABELS[record.predicate] ?? "Launch evidence";
413
472
  const reason = record.reason ? ` (${record.reason})` : "";
414
- const remediation = ADMISSION_OWNER_REMEDIATION[record.next_action_owner ?? "operator"] ?? ADMISSION_OWNER_REMEDIATION.operator;
473
+ const remediation = admissionReasonRemedy(record, readiness) ??
474
+ ADMISSION_OWNER_REMEDIATION[record.next_action_owner ?? "operator"] ??
475
+ ADMISSION_OWNER_REMEDIATION.operator;
476
+ // BAPI-1360: the durable-actor handshake being unsupported is a known, current
477
+ // limitation, not a hold — it renders as a warning that says so. Every other
478
+ // predicate, and every other reason for this one, keeps its existing branch.
479
+ if (record.predicate === "durable_actor" && record.reason === "actor_handshake_unsupported") {
480
+ return createReadinessCheckSafely({
481
+ id: `server.admission-${slug}`,
482
+ source: "server",
483
+ label,
484
+ status: "warn",
485
+ detail: `not yet supported by this deploy${reason} — this does not hold a launch`,
486
+ // Fixed, non-escalating: the owner remedy would tell the operator to escalate
487
+ // for a limitation that blocks nothing.
488
+ remediation: "no action is needed to launch; this warning clears once the deploy supports the durable-actor handshake.",
489
+ });
490
+ }
415
491
  switch (record.verdict) {
416
492
  case "confirmed":
417
493
  return createReadinessCheckSafely({
@@ -503,7 +579,7 @@ function unattendedOutcomes(readiness) {
503
579
  ? {
504
580
  id: "unattended-consent",
505
581
  status: "pass",
506
- detail: "this repository has consented to unattended conductor runs",
582
+ detail: "this repository has consented to unattended Epic Implementer runs",
507
583
  }
508
584
  : {
509
585
  id: "unattended-consent",
@@ -540,13 +616,21 @@ function unattendedOutcomes(readiness) {
540
616
  ? {
541
617
  id: "repository-workflow-readiness",
542
618
  status: "pass",
543
- detail: "the review and conductor-CI workflows are both confirmed",
619
+ detail: "the review and epic-implementer-ci workflows are both confirmed",
544
620
  }
545
621
  : {
546
622
  id: "repository-workflow-readiness",
547
623
  status: "warn",
548
- detail: "the review and conductor-CI workflows are not both confirmed — an unattended auto-merging run will be refused",
549
- remediation: SERVER_REMEDIATIONS.repositoryWorkflowReadiness,
624
+ // BAPI-1360: name a confirmed-missing file separately from a file that
625
+ // could not be read; the remedy already differs by owner.
626
+ detail: readiness.review_workflow?.probe_succeeded === true &&
627
+ readiness.review_workflow?.workflow_present === false
628
+ ? `${REVIEW_WORKFLOW_PATH} is missing from the default branch — an unattended auto-merging run will be refused`
629
+ : readiness.conductor_ci_workflow?.probe_succeeded === true &&
630
+ readiness.conductor_ci_workflow?.workflow_present === false
631
+ ? `${CI_WORKFLOW_PATH} is missing from the default branch — an unattended auto-merging run will be refused`
632
+ : "the review and epic-implementer-ci workflows are not both confirmed — an unattended auto-merging run will be refused",
633
+ remediation: repositoryWorkflowRemedy(readiness),
550
634
  },
551
635
  ];
552
636
  }
@@ -677,6 +761,17 @@ function reconcilerOutcome(readiness) {
677
761
  remediation: SERVER_REMEDIATIONS.reconcilerUnreadable,
678
762
  };
679
763
  }
764
+ // BAPI-1360 — with no active run there is no progress to judge: the row is an
765
+ // explicit, remedy-free `skip`, not a stale-tick failure about a reconciler that
766
+ // has nothing to reconcile. ONLY a readable response with a strict zero count
767
+ // qualifies; a missing or malformed count keeps the stale/pass reading below.
768
+ if (r.liveness_readable === true && r.active_run_count === 0) {
769
+ return {
770
+ id: "reconciler-liveness",
771
+ status: "skip",
772
+ detail: "no active runs, so there is no run progress to check",
773
+ };
774
+ }
680
775
  if (!r.stale) {
681
776
  return {
682
777
  id: "reconciler-liveness",
@@ -726,10 +821,21 @@ function executorOutcome(readiness) {
726
821
  remediation: SERVER_REMEDIATIONS.executorStale,
727
822
  };
728
823
  }
824
+ // `ready === null`. BAPI-1360: with readable executor AND reconciler liveness and
825
+ // a strict zero active-run count, no executor being present is expected —
826
+ // `drive-epic` starts a run-scoped one — so it is a warning, not a failure.
827
+ if (readiness.reconciler.liveness_readable === true && readiness.reconciler.active_run_count === 0) {
828
+ return {
829
+ id: "executor-readiness",
830
+ status: "warn",
831
+ detail: "no executor was observed; `drive-epic` starts a run-scoped executor at launch, so a durable service is optional",
832
+ remediation: SERVER_REMEDIATIONS.executorNoneObservedNoRuns,
833
+ };
834
+ }
729
835
  return {
730
836
  id: "executor-readiness",
731
837
  status: "fail",
732
- detail: "never observed by the server",
838
+ detail: "no executor was observed while runs are active or their count could not be read",
733
839
  remediation: SERVER_REMEDIATIONS.executorNeverSeen,
734
840
  };
735
841
  }
@@ -754,6 +860,16 @@ function reviewWorkflowOutcome(readiness) {
754
860
  remediation: SERVER_REMEDIATIONS.reviewWorkflowUnknown,
755
861
  };
756
862
  }
863
+ // BAPI-1360 — a SUCCESSFUL probe that found no file is a confirmed absence, kept
864
+ // apart from an unreadable file (Bridge operations) and from missing markers.
865
+ if (w.workflow_present === false) {
866
+ return {
867
+ id: "review-workflow",
868
+ status: "fail",
869
+ detail: `${REVIEW_WORKFLOW_PATH} is missing from the default branch (confirmed)`,
870
+ remediation: `a repository administrator adds ${REVIEW_WORKFLOW_PATH} to the default branch so it emits both the run id and the reviewed SHA the verdict protocol reads.`,
871
+ };
872
+ }
757
873
  if (w.emits_run_id && w.emits_reviewed_sha) {
758
874
  return {
759
875
  id: "review-workflow",
@@ -780,7 +896,7 @@ function ciWorkflowOutcome(readiness) {
780
896
  return {
781
897
  id: "conductor-ci-workflow",
782
898
  status: "fail",
783
- detail: "this Bridge API deploy did not report the conductor-CI workflow facts",
899
+ detail: "this Bridge API deploy did not report the epic-implementer-ci workflow facts",
784
900
  remediation: SERVER_REMEDIATIONS.olderServer,
785
901
  };
786
902
  }
@@ -794,18 +910,51 @@ function ciWorkflowOutcome(readiness) {
794
910
  remediation: SERVER_REMEDIATIONS.ciWorkflowUnknown,
795
911
  };
796
912
  }
797
- return c.migration_guard_present
798
- ? {
913
+ // BAPI-1360 — confirmed absence of the file, not a missing migration guard.
914
+ if (c.workflow_present === false) {
915
+ return {
916
+ id: "conductor-ci-workflow",
917
+ status: "fail",
918
+ detail: `${CI_WORKFLOW_PATH} is missing from the default branch (confirmed)`,
919
+ remediation: `a repository administrator adds ${CI_WORKFLOW_PATH} to the default branch. ${ciWorkflowRemedy(c)}`,
920
+ };
921
+ }
922
+ if (c.migration_guard_present) {
923
+ return {
799
924
  id: "conductor-ci-workflow",
800
925
  status: "pass",
801
926
  detail: "the default branch's conductor CI workflow runs the migration guard",
802
- }
803
- : {
927
+ };
928
+ }
929
+ // BAPI-1359 — the guard is conditional on an Alembic chain. Three explicit
930
+ // server answers, plus the older-server case where neither field is reported.
931
+ if (c.migration_chain_probe_succeeded === true && c.migration_guard_required === false) {
932
+ return {
804
933
  id: "conductor-ci-workflow",
805
- status: "warn",
806
- detail: "the conductor CI workflow does not run the migration guard",
807
- remediation: SERVER_REMEDIATIONS.ciWorkflowMissingGuard,
934
+ status: "pass",
935
+ // States only what was observed. It does not say migrations are safe, or
936
+ // that the repository has none.
937
+ detail: "migration guard not required (no Alembic chain: no alembic.ini on the default branch)",
938
+ };
939
+ }
940
+ if (c.migration_chain_probe_succeeded === false) {
941
+ return {
942
+ id: "conductor-ci-workflow",
943
+ status: "fail",
944
+ detail: "the Alembic check could not be read, so it is not known whether the conductor CI workflow needs the migration guard",
945
+ remediation: ciWorkflowRemedy(c),
808
946
  };
947
+ }
948
+ return {
949
+ id: "conductor-ci-workflow",
950
+ status: "warn",
951
+ detail: c.migration_guard_required === true && c.migration_chain_probe_succeeded === true
952
+ ? "the conductor CI workflow does not run the migration guard, which this repository requires because it has alembic.ini on its default branch"
953
+ : // An older server (no new fields) or a partial report: keep today's
954
+ // wording and today's behavior, and claim nothing about alembic.ini.
955
+ "the conductor CI workflow does not run the migration guard",
956
+ remediation: ciWorkflowRemedy(c),
957
+ };
809
958
  }
810
959
  // ---------------------------------------------------------------------------
811
960
  // Install-time section adapter
@@ -873,7 +1022,7 @@ export function mapInstallSectionsToReadinessChecks(sections) {
873
1022
  ? {}
874
1023
  : {
875
1024
  remediation: section.remediation ??
876
- "run `install conductor` for this repository to re-provision this capability.",
1025
+ `run \`${PRINTED_CONDUCTOR_INSTALL_COMMAND}\` for this repository to re-provision this capability. ${PRINTED_CHECK_NAME_INSTRUCTION}`,
877
1026
  }),
878
1027
  }));
879
1028
  }
@@ -903,13 +1052,14 @@ export async function collectConductorReadinessGate(deps) {
903
1052
  }
904
1053
  // A plane preflight was requested and could not be run. Not requesting one is
905
1054
  // not an unavailability — its prerequisites are still reported as unknowns.
906
- if (deps.runPlanePreflight && planeResult === null)
1055
+ if (deps.runPlanePreflight && deps.planeNotApplicable === undefined && planeResult === null) {
907
1056
  unavailableSources.push("plane");
1057
+ }
908
1058
  const checks = [
909
1059
  ...mapInstallStatusChecksToReadinessChecks((installReport?.legacyInstallChecks ?? null), installReport === null),
910
1060
  ...mapConductorDoctorReportToReadinessChecks(installReport?.legacyConductor ?? null),
911
1061
  ...mapInstallSectionsToReadinessChecks(installReport?.sections ?? null),
912
- ...mapPlanePreflightToReadinessChecks(planeResult),
1062
+ ...mapPlanePreflightToReadinessChecks(planeResult, deps.planeNotApplicable),
913
1063
  ...mapServerReadinessToChecks(installReport?.readiness ?? null, classifyServerFailure(installReport)),
914
1064
  ];
915
1065
  // Validate the ASSEMBLED report, not just the pieces: an injected collector or
@@ -946,6 +1096,8 @@ async function runInstallDoctorSafely(deps) {
946
1096
  }
947
1097
  /** Run the plane preflight, containing any throw. */
948
1098
  async function runPlaneSafely(deps) {
1099
+ if (deps.planeNotApplicable !== undefined)
1100
+ return null;
949
1101
  if (!deps.runPlanePreflight)
950
1102
  return null;
951
1103
  try {
@@ -1,15 +1,16 @@
1
1
  /**
2
- * `conductor` operator recovery and repair verbs — thirteen CLI-only commands.
2
+ * `conductor` operator recovery and repair verbs — fourteen CLI-only commands.
3
3
  *
4
4
  * The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
5
5
  * `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
6
6
  * `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
7
7
  * `scope-catch-up`, `patch-policy`, `resume-run`. BAPI-1300's parked-child
8
8
  * continuation pair: `continue-child` and the read-only `continuation-receipt`.
9
- * BAPI-1326's allowance-preserving CI resume: `resume-ci`.
9
+ * BAPI-1326's allowance-preserving CI resume: `resume-ci`. BAPI-1388's guided
10
+ * provider re-run of failed CI workflow runs: `rerun-ci`.
10
11
  *
11
12
  * CLI-only by design — this is an explicit ticket non-goal, not an oversight:
12
- * none of these thirteen command names is ever registered as an MCP tool, tool
13
+ * none of these fourteen command names is ever registered as an MCP tool, tool
13
14
  * description, or generated tool-schema input (`index-static.test.ts` guards
14
15
  * this). They exist so an operator (or `plane down`, via
15
16
  * `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
@@ -709,11 +710,13 @@ export async function runScopeReparseCommand(argv) {
709
710
  const DISPATCH_REMEDIATION_USAGE = [
710
711
  "Usage: epic-implementer dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path> [--json]",
711
712
  "",
712
- "Dispatch a remediation job for a ticket at code_review (or parked there),",
713
+ "Dispatch a remediation job for a ticket at code_review or ci (or parked there),",
713
714
  "carrying your driver context to the worker. The SERVER chooses the job type",
714
- "(remediate, or reconcile_overlap for a conflict/overlap ticket) and admits it",
715
+ "(remediate, or reconcile_overlap for a conflict/overlap ticket; ci_fix for a CI",
716
+ "failure the server's FRESH CI read routes to investigation) and admits it",
715
717
  "through the reconciler's own strict-serial lane, remediation budget, and",
716
- "dedup key. A parked ticket is resumed in the same step.",
718
+ "dedup key. A parked ticket is resumed in the same step (unguided only). An",
719
+ "infrastructure CI failure is refused by name and points to rerun-ci.",
717
720
  "",
718
721
  `--context-file must be non-empty UTF-8 text of at most DRIVER_CONTEXT_MAX_BYTES`,
719
722
  `(${DRIVER_CONTEXT_MAX_BYTES} bytes). It is rejected, never truncated, and is never`,
@@ -764,7 +767,7 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
764
767
  guard,
765
768
  }),
766
769
  label: `ticket ${parsed.ticketKey}`,
767
- stateLabel: "code_review gate",
770
+ stateLabel: "selected gate",
768
771
  }, parsed.json);
769
772
  }
770
773
  const { dispatchRemediationRepair } = await import("./recovery-operations.js");
@@ -777,7 +780,7 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
777
780
  verb: "dispatch-remediation",
778
781
  epicRunId: parsed.epicRunId,
779
782
  targetLabel: `ticket ${parsed.ticketKey}`,
780
- stateLabel: "code_review gate",
783
+ stateLabel: "selected gate",
781
784
  }, parsed.json, (result) => [
782
785
  ` job: ${result.job_type || "unknown"}${result.job_id !== null ? ` (job ${result.job_id})` : ""}`,
783
786
  ]);
@@ -787,6 +790,195 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
787
790
  return 1;
788
791
  }
789
792
  }
793
+ // ---------------------------------------------------------------------------
794
+ // BAPI-1388 — rerun-ci
795
+ // ---------------------------------------------------------------------------
796
+ const RERUN_CI_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--binding"]);
797
+ /** Exit codes: 0 applied (including an all-not-re-runnable no-op), 2 refused/stale, 3 unconfirmed, 1 other. */
798
+ export const RERUN_CI_EXIT_OK = 0;
799
+ export const RERUN_CI_EXIT_FAILED = 1;
800
+ export const RERUN_CI_EXIT_REFUSED = 2;
801
+ export const RERUN_CI_EXIT_UNCONFIRMED = 3;
802
+ const RERUN_CI_USAGE = [
803
+ "Usage: epic-implementer rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
804
+ "",
805
+ "Ask the CI provider to re-run a ticket's FAILED required checks at its bound head,",
806
+ "for a ticket at ci or parked on it. Copy the command exactly as the run",
807
+ "explanation rendered it: --binding pins the ticket version and head it showed.",
808
+ "The SERVER re-reads fresh CI evidence, re-runs each GitHub Actions workflow run at",
809
+ "most once per head, and reports every failed check as enqueued, deduplicated,",
810
+ "deferred, or not_re_runnable.",
811
+ "",
812
+ "rerun-ci NEVER re-runs the claude-review workflow. It charges no allowance and",
813
+ "changes no park state, and a re-run is not a verdict: when CI is green, use",
814
+ "`epic-implementer resume-ci` (a parked ticket) or let the reconciler observe it.",
815
+ "If the failure is a code defect, use `dispatch-remediation --context-file`.",
816
+ "",
817
+ "The request is stored locally before it is sent, so re-running the same command",
818
+ "after a lost response re-sends the SAME request. A subject that has since changed",
819
+ "is refused as stale with nothing written.",
820
+ "",
821
+ "Exit codes: 0 applied (including a no-op when nothing could be re-run); 2 refused",
822
+ "or stale; 3 unconfirmed; 1 other failure (including a server without this verb).",
823
+ ].join("\n");
824
+ export function parseRerunCiArgs(argv) {
825
+ const { values, bools } = tokenizeRecoveryFlags(argv, RERUN_CI_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
826
+ if (bools.has("--help")) {
827
+ return { epicRunId: "", ticketKey: "", binding: "", json: bools.has("--json"), help: true };
828
+ }
829
+ return {
830
+ epicRunId: requireFlag(values, "--epic-run-id"),
831
+ ticketKey: requireFlag(values, "--ticket-key"),
832
+ binding: requireFlag(values, "--binding"),
833
+ json: bools.has("--json"),
834
+ help: false,
835
+ };
836
+ }
837
+ const RERUN_CI_STATUS_TEXT = {
838
+ enqueued: "re-run requested",
839
+ deduplicated: "already requested",
840
+ deferred: "deferred (not requested; run the verb again after a fresh explain-run)",
841
+ not_re_runnable: "not re-runnable",
842
+ };
843
+ const RERUN_CI_REASON_TEXT = {
844
+ not_github_actions: "not a GitHub Actions check",
845
+ no_workflow_run_id: "no workflow run id",
846
+ claude_review_excluded: "claude-review is never re-run",
847
+ };
848
+ const RERUN_CI_OUTCOME_TEXT = {
849
+ enqueued: "re-runs requested",
850
+ partial: "partially requested (some runs deferred)",
851
+ nothing_to_enqueue: "nothing to enqueue (no failed check could be re-run)",
852
+ };
853
+ const RERUN_CI_NEXT_ACTION = {
854
+ enqueued: "Wait for the re-runs to finish, then read `epic-implementer explain-run`. When CI is " +
855
+ "green use `epic-implementer resume-ci` (a parked ticket); this verb did not mark CI passed.",
856
+ partial: "Some runs were not requested. Read `epic-implementer explain-run` and, if still " +
857
+ "offered, re-run the command it renders with its fresh binding.",
858
+ nothing_to_enqueue: "No re-run was possible. Read `epic-implementer explain-run`; for a code defect use " +
859
+ "`dispatch-remediation --context-file`.",
860
+ };
861
+ function rerunCiCheckLine(check) {
862
+ const identity = [
863
+ check.name ?? "(unnamed check)",
864
+ check.workflow_identity ? `workflow ${check.workflow_identity}` : null,
865
+ check.run_id !== null ? `run ${check.run_id}` : null,
866
+ ]
867
+ .filter((part) => part !== null)
868
+ .join(", ");
869
+ const reason = check.reason ? ` — ${RERUN_CI_REASON_TEXT[check.reason] ?? check.reason}` : "";
870
+ const job = check.job_id !== null ? ` (job ${check.job_id})` : "";
871
+ return ` - ${identity}: ${RERUN_CI_STATUS_TEXT[check.status] ?? check.status}${reason}${job}`;
872
+ }
873
+ /**
874
+ * Exported for tests: render one `rerun-ci` outcome. Success goes to stdout and
875
+ * every failure to stderr (`--json` included). An unsupported server is NEVER a
876
+ * success and says it is "not available on this server".
877
+ */
878
+ export function renderRerunCiOutcome(outcome, parsed, json) {
879
+ const label = `ticket ${parsed.ticketKey} (run ${parsed.epicRunId})`;
880
+ if (outcome.kind === "applied") {
881
+ const result = outcome.result;
882
+ if (result.committed && !result.event_recorded) {
883
+ console.error(`Warning: rerun-ci for ${label} COMMITTED, but its operator_verb_applied audit ` +
884
+ "event was not recorded. The action stands.");
885
+ }
886
+ if (json) {
887
+ console.log(JSON.stringify({
888
+ ok: true,
889
+ outcome: "applied",
890
+ request_id: outcome.requestId,
891
+ reference_reused: outcome.referenceReused,
892
+ result,
893
+ }));
894
+ return RERUN_CI_EXIT_OK;
895
+ }
896
+ const lines = [
897
+ `rerun-ci (guided) — ${label}`,
898
+ ` ticket: ${result.ticket_key}`,
899
+ ` head: ${result.head_sha ? "`" + result.head_sha + "`" : "unknown"}`,
900
+ ` outcome: ${RERUN_CI_OUTCOME_TEXT[result.outcome] ?? result.outcome}`,
901
+ " failed checks:",
902
+ ...result.checks.map(rerunCiCheckLine),
903
+ ` next action: ${RERUN_CI_NEXT_ACTION[result.outcome] ?? "Read `epic-implementer explain-run`."}`,
904
+ ` request: ${outcome.requestId}${outcome.referenceReused ? " (re-sent from the local reference)" : ""}`,
905
+ ` event recorded: ${result.event_recorded}`,
906
+ ];
907
+ console.log(lines.join("\n"));
908
+ return RERUN_CI_EXIT_OK;
909
+ }
910
+ if (outcome.kind === "refused" || outcome.kind === "stale") {
911
+ const payload = {
912
+ ok: false,
913
+ outcome: outcome.kind,
914
+ code: outcome.code,
915
+ message: outcome.message,
916
+ request_id: outcome.requestId,
917
+ nothing_written: true,
918
+ };
919
+ if (json) {
920
+ console.error(JSON.stringify(payload));
921
+ }
922
+ else {
923
+ console.error(`Refused: rerun-ci for ${label} — ${outcome.code}: ${outcome.message}. ` +
924
+ (outcome.kind === "stale"
925
+ ? "Nothing was written. The run changed since this command was rendered; re-read the run explanation."
926
+ : "Nothing was written."));
927
+ }
928
+ return RERUN_CI_EXIT_REFUSED;
929
+ }
930
+ if (outcome.kind === "unconfirmed") {
931
+ if (json) {
932
+ console.error(JSON.stringify({
933
+ ok: false,
934
+ outcome: "unconfirmed",
935
+ reason: outcome.reason,
936
+ message: outcome.message,
937
+ request_id: outcome.requestId,
938
+ }));
939
+ }
940
+ else {
941
+ console.error(`Unconfirmed: rerun-ci for ${label} — ${outcome.message}.`);
942
+ }
943
+ return RERUN_CI_EXIT_UNCONFIRMED;
944
+ }
945
+ if (json) {
946
+ console.error(JSON.stringify({ ok: false, outcome: outcome.kind, message: outcome.message }));
947
+ }
948
+ else {
949
+ console.error(`Failed: rerun-ci for ${label} — ${outcome.message}.`);
950
+ }
951
+ return RERUN_CI_EXIT_FAILED;
952
+ }
953
+ export async function runRerunCiCommand(argv) {
954
+ try {
955
+ const parsed = parseRerunCiArgs(argv);
956
+ if (parsed.help) {
957
+ console.log(RERUN_CI_USAGE);
958
+ return 0;
959
+ }
960
+ // Prove the token names THIS command, run, and ticket before credentials are
961
+ // resolved, so a mismatched binding reaches no one.
962
+ const binding = checkGuidedBinding(parsed.binding, "rerun-ci", {
963
+ epicRunId: parsed.epicRunId,
964
+ ticketKey: parsed.ticketKey,
965
+ });
966
+ const access = await resolveRecoveryAccess(parsed.json);
967
+ if (!access)
968
+ return RERUN_CI_EXIT_FAILED;
969
+ const { rerunCiRepair } = await import("./recovery-operations.js");
970
+ const outcome = await rerunCiRepair(access, {
971
+ epicRunId: parsed.epicRunId,
972
+ ticketKey: parsed.ticketKey,
973
+ binding,
974
+ });
975
+ return renderRerunCiOutcome(outcome, parsed, parsed.json);
976
+ }
977
+ catch (error) {
978
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
979
+ return RERUN_CI_EXIT_FAILED;
980
+ }
981
+ }
790
982
  const MERGE_CHILD_USAGE = [
791
983
  "Usage: epic-implementer merge-child --epic-run-id <id> --pr <number> [--json]",
792
984
  "",