@bridge_gpt/mcp-server 0.2.67 → 0.2.69

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 (127) hide show
  1. package/README.md +272 -51
  2. package/build/agent-launchers/claude-executor-adapter.js +27 -0
  3. package/build/agents.generated.js +22 -2
  4. package/build/bitbucket-workspace-remote.js +136 -0
  5. package/build/bootstrap-project-identity.js +24 -0
  6. package/build/bridge-permissions.js +1362 -0
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-settings.js +102 -2
  9. package/build/cli-help.js +27 -0
  10. package/build/codex-home.js +9 -1
  11. package/build/codex-launcher-pin.js +598 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +9 -9
  15. package/build/conductor/bridge-api-client.js +714 -0
  16. package/build/conductor/command-drift-production.js +92 -0
  17. package/build/conductor/command-drift.js +89 -0
  18. package/build/conductor/doctor.js +69 -26
  19. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  20. package/build/conductor/epic-implementer-cli.js +76 -22
  21. package/build/conductor/gate-discovery.js +890 -0
  22. package/build/conductor/install-doctor.js +15 -3
  23. package/build/conductor/json-output.js +63 -0
  24. package/build/conductor/readiness-cli.js +95 -10
  25. package/build/conductor/readiness-sections.js +369 -3
  26. package/build/conductor/readiness.js +211 -8
  27. package/build/conductor/recovery-cli.js +110 -4
  28. package/build/conductor/recovery-operations.js +99 -1
  29. package/build/connect-bitbucket-api.js +41 -6
  30. package/build/connect-bitbucket.js +106 -33
  31. package/build/connect-github-api.js +33 -14
  32. package/build/credential-store.js +207 -47
  33. package/build/docs.generated.js +57 -3
  34. package/build/doctor.js +198 -4
  35. package/build/drive-epic.js +45 -8
  36. package/build/epic-coherence-input.test-fixtures.js +4 -3
  37. package/build/epic-implementer-bin.js +1369 -25
  38. package/build/epic-workarounds-path.js +39 -0
  39. package/build/executor/cli.js +30 -0
  40. package/build/executor/completion-contract.js +20 -5
  41. package/build/executor/deps.js +4 -1
  42. package/build/executor/gate-qualification-job.js +499 -0
  43. package/build/executor/guarded-fast-forward-push.js +129 -0
  44. package/build/executor/http-client.js +43 -4
  45. package/build/executor/integration-job.js +122 -8
  46. package/build/executor/job-errors.js +8 -1
  47. package/build/executor/job-runner.js +589 -47
  48. package/build/executor/likely-tests.js +290 -0
  49. package/build/executor/live-worker-registry.js +1 -1
  50. package/build/executor/observation.js +31 -2
  51. package/build/executor/packaged-recipe.js +106 -0
  52. package/build/executor/permissions.js +115 -28
  53. package/build/executor/platform.js +1 -1
  54. package/build/executor/preflight.js +61 -12
  55. package/build/executor/process-containment.js +36 -12
  56. package/build/executor/process.js +15 -7
  57. package/build/executor/prompt-spec.js +33 -1
  58. package/build/executor/recently-failed-tests-file.js +71 -0
  59. package/build/executor/reconcile-overlap-job.js +41 -23
  60. package/build/executor/required-verification.js +1 -1
  61. package/build/executor/required-work-ledger.js +105 -17
  62. package/build/executor/retirement.js +69 -0
  63. package/build/executor/runner.js +114 -11
  64. package/build/executor/test-clock.js +15 -0
  65. package/build/executor/types.js +32 -0
  66. package/build/executor/viewer-tabs.js +58 -8
  67. package/build/executor/worker-command.js +23 -7
  68. package/build/executor/worker-diagnostics.js +37 -0
  69. package/build/executor/worker-finalization.js +31 -5
  70. package/build/executor/worker-guard-hook.js +179 -25
  71. package/build/executor/worker-log.js +4 -3
  72. package/build/executor/worker-verification-guidance.js +37 -0
  73. package/build/executor/worktree.js +88 -11
  74. package/build/first-build-advisory.js +97 -0
  75. package/build/generated-assets-advisory.js +14 -0
  76. package/build/generated-assets-catalog.js +66 -0
  77. package/build/generated-assets-context.js +46 -0
  78. package/build/generated-assets-dependencies.js +26 -0
  79. package/build/generated-assets-doctor.js +50 -0
  80. package/build/generated-assets-health.js +65 -0
  81. package/build/generated-assets-host.js +17 -0
  82. package/build/generated-assets-inventory.js +17 -0
  83. package/build/generated-assets-paths.js +60 -0
  84. package/build/generated-assets-publisher.js +133 -0
  85. package/build/generated-assets-remedy.js +25 -0
  86. package/build/generated-assets-repair.js +72 -0
  87. package/build/generated-assets-session.js +71 -0
  88. package/build/generated-assets-test-support.js +35 -0
  89. package/build/index.js +1109 -695
  90. package/build/init.js +54 -177
  91. package/build/install-bridge-conductor-gate.js +1372 -0
  92. package/build/install-bridge-conductor.js +289 -36
  93. package/build/install-bridge.js +722 -252
  94. package/build/launcher-config-inspection.js +184 -19
  95. package/build/mcp-host-targets.js +4 -0
  96. package/build/mcp-identity.js +7 -0
  97. package/build/mcp-install-state.js +50 -4
  98. package/build/merge-workflow-install.js +3 -4
  99. package/build/pipeline-utils.js +6 -2
  100. package/build/pipelines.generated.js +22 -142
  101. package/build/plan-execution-ledger.js +2 -32
  102. package/build/plane/manifest.js +16 -1
  103. package/build/plane/preflight.js +54 -1
  104. package/build/printed-remedy-commands.js +16 -1
  105. package/build/readme.generated.js +1 -1
  106. package/build/repair-assets.js +93 -0
  107. package/build/run-unit-tests-launcher.js +130 -49
  108. package/build/secret-redaction.js +33 -0
  109. package/build/session-upgrade-notice.js +86 -0
  110. package/build/setup-epic.js +108 -3
  111. package/build/start-tickets-prereqs.js +70 -16
  112. package/build/start-tickets.js +44 -5
  113. package/build/tool-call-advisory.js +162 -0
  114. package/build/tool-surface-gating.js +32 -57
  115. package/build/update-check.js +97 -16
  116. package/build/update-status.js +61 -71
  117. package/build/upgrade-advice.js +15 -11
  118. package/build/upgrade-cli.js +398 -17
  119. package/build/version.generated.js +3 -3
  120. package/build/worker-guard-hook-bin.js +3 -2
  121. package/build/worker-guard-hook-main.js +1 -1
  122. package/build/worktree-core.js +15 -2
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
  124. package/docs/EPIC-IMPLEMENTER.md +68 -2
  125. package/package.json +3 -3
  126. package/pipelines/greenfield-setup.json +4 -128
  127. package/pipelines/implement-ticket.json +3 -2
@@ -92,17 +92,17 @@
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
+ import { PRINTED_CHECK_NAME_INSTRUCTION, PRINTED_CONDUCTOR_EXECUTOR_INSTALL_COMMAND, PRINTED_CONDUCTOR_INSTALL_COMMAND, PRINTED_CONNECT_GITHUB_COMMAND, PRINTED_EXECUTOR_ID_INSTRUCTION, PRINTED_QUALIFY_GATE_COMMAND, } from "../printed-remedy-commands.js";
96
96
  import { mapConductorDoctorReportToReadinessChecks, } from "./doctor.js";
97
97
  import { mapInstallStatusChecksToReadinessChecks, } from "../install-doctor.js";
98
98
  import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
99
99
  import { runConductorInstallDoctor, } from "./install-doctor.js";
100
- import { BRING_UP_SECTION_LABELS, presentRequiredChecks, } from "./readiness-sections.js";
100
+ import { BRING_UP_SECTION_LABELS, describeGateProfile, describeGateProfileWarning, effectiveReviewModeIsRepositoryPolicy, EPIC_BRANCH_CI_SECTION_IDS, epicBranchCiSections, presentRequiredChecks, quotedSubject, } from "./readiness-sections.js";
101
101
  import { classifyBringUpFacts } from "./bring-up-facts.js";
102
102
  import { CI_WORKFLOW_PATH, REVIEW_WORKFLOW_PATH, ciWorkflowRemedy, repositoryWorkflowRemedy, } from "./repository-workflow-remedy.js";
103
103
  import { classifyMergeExecutionEligibility, MERGE_EXECUTION_INELIGIBLE_REMEDIATION, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
104
104
  import { createReadinessCheck, createReadinessCheckSafely, summarizeReadinessChecks, validateReadinessChecks, } from "../readiness-check.js";
105
- import { ADMISSION_EVIDENCE_SCHEMA_VERSION, } from "./bridge-api-client.js";
105
+ import { ADMISSION_EVIDENCE_SCHEMA_VERSION, gateProfileWaivesCiWorkflow, } from "./bridge-api-client.js";
106
106
  /** This report's own schema version. See the module docstring. */
107
107
  export const CONDUCTOR_READINESS_GATE_SCHEMA_VERSION = 1;
108
108
  /** Fixed statement carried in every report, so no consumer can mistake it for a gate. */
@@ -359,7 +359,17 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
359
359
  });
360
360
  // BAPI-1260 — appended ONLY when the server reported the block, so every
361
361
  // pre-BAPI-1260 report keeps exactly the descriptor set above.
362
- return [...described, ...admissionEvidenceChecks(readiness), ...genericDiagnosticChecks(readiness)];
362
+ return [
363
+ ...described,
364
+ ...admissionEvidenceChecks(readiness),
365
+ // BAPI-1390 — appended ONLY when the server reported a gate profile.
366
+ ...gateProfileChecks(readiness),
367
+ // BAPI-1405 — always three warn-only epic-branch CI rows. An older server
368
+ // that omits the block renders them as "Unreported — upgrade the server",
369
+ // never as "no warning".
370
+ ...epicBranchCiChecks(readiness),
371
+ ...genericDiagnosticChecks(readiness),
372
+ ];
363
373
  }
364
374
  function genericDiagnosticChecks(readiness) {
365
375
  return (readiness.diagnostics ?? []).map((diagnostic, index) => {
@@ -432,10 +442,156 @@ function admissionReasonRemedy(record, readiness) {
432
442
  return (`a repository administrator replaces ${RETIRED_GATE_NAME} with ${CURRENT_GATE_NAME} in the plan's ` +
433
443
  "required_checks, in the supervisor done-gate checks, and in branch protection." +
434
444
  (ciAbsent ? ` Also add ${CI_WORKFLOW_PATH} to the default branch so ${CURRENT_GATE_NAME} has a producer.` : ""));
445
+ default:
446
+ return gateProfileReasonRemedy(record.reason, record.verdict, record.subject ?? null);
447
+ }
448
+ }
449
+ /** The provider-evidence window the server searches, spelled once for the remedies. */
450
+ const PROVIDER_EVIDENCE_WINDOW = "the last 12 default-branch commits or 8 merged pull-request heads from the last 90 days";
451
+ /**
452
+ * BAPI-1390 — fixed remedies for the four gate-profile reasons. Everything is a
453
+ * literal except the validated `subject`. `null` for any other reason, so an
454
+ * unknown reason falls back to the owner's generic remedy.
455
+ */
456
+ function gateProfileReasonRemedy(reason, verdict, subject) {
457
+ switch (reason) {
458
+ case "provider_check_unseen":
459
+ return (`a repository administrator confirms that the check ${quotedSubject(subject, "named in the gate profile")} ` +
460
+ `reports on the default branch — it was not seen on ${PROVIDER_EVIDENCE_WINDOW} — or edits the gate ` +
461
+ "profile; setup never grants it.");
462
+ case "provider_evidence_unavailable":
463
+ return (`Bridge could not read the provider's check history for ${quotedSubject(subject, "a gate-profile check")} ` +
464
+ `(${PROVIDER_EVIDENCE_WINDOW}), so it is unknown, not absent. Re-run readiness; if it persists, ` +
465
+ "escalate to Bridge operations.");
466
+ case "local_command_unqualified":
467
+ return verdict === "unable_to_verify"
468
+ ? `the qualification of the local gate command ${quotedSubject(subject, "in the gate profile")} could not ` +
469
+ "be read, so it is unknown, not qualified. Re-run readiness; if it persists, escalate to Bridge operations."
470
+ : `the local gate command ${quotedSubject(subject, "in the gate profile")} is not qualified for the current ` +
471
+ "gate-profile revision (unqualified or stale). Qualify it with " +
472
+ `\`${PRINTED_QUALIFY_GATE_COMMAND}\`, or have a repository administrator ` +
473
+ "edit the gate profile.";
474
+ case "migration_guard_unset":
475
+ return verdict === "unable_to_verify"
476
+ ? "Bridge could not confirm whether this repository has alembic.ini on its default branch, so it could " +
477
+ "not decide whether the gate profile needs a migration guard. Re-run readiness, or have a repository " +
478
+ "administrator set a migration guard in the gate profile."
479
+ : "this repository has alembic.ini on its default branch, so its gate profile must name a migration " +
480
+ "guard (a provider check or a local command). A repository administrator sets it in the gate " +
481
+ "profile; setup never grants it.";
435
482
  default:
436
483
  return null;
437
484
  }
438
485
  }
486
+ const GATE_PROFILE_WARNING_REMEDIATION = "no action is needed to launch; confirm the check reports once an epic branch runs CI.";
487
+ /**
488
+ * BAPI-1390 — the gate-profile row plus one warn row per warning. Appended
489
+ * ONLY when the server reported the block, so an older server's report is
490
+ * unchanged. The row is descriptive: the server's admission evidence is the
491
+ * authority, and the warning rows never hold a launch.
492
+ */
493
+ export function gateProfileChecks(readiness) {
494
+ const profile = readiness.gate_profile;
495
+ if (profile === undefined)
496
+ return [];
497
+ if (profile === null) {
498
+ return [
499
+ createReadinessCheckSafely({
500
+ id: "server.gate-profile",
501
+ source: "server",
502
+ label: "Gate profile",
503
+ status: "fail",
504
+ detail: "the server reported no gate profile, so the CI gate is unverified",
505
+ remediation: SERVER_REMEDIATIONS.olderServer,
506
+ }),
507
+ ];
508
+ }
509
+ const detail = describeGateProfile(profile);
510
+ let row;
511
+ if (profile.read_failed || profile.stored_malformed) {
512
+ row = createReadinessCheckSafely({
513
+ id: "server.gate-profile",
514
+ source: "server",
515
+ label: "Gate profile",
516
+ status: "fail",
517
+ detail: `${profile.read_failed ? "the stored gate profile could not be read" : "the stored gate profile is malformed"} — ${detail}`,
518
+ remediation: profile.read_failed
519
+ ? "re-run readiness; if it persists, escalate to Bridge operations."
520
+ : "a repository administrator re-saves the gate profile; setup never grants it.",
521
+ });
522
+ }
523
+ else if (profile.verdict === "denied" || profile.verdict === "unable_to_verify") {
524
+ row = createReadinessCheckSafely({
525
+ id: "server.gate-profile",
526
+ source: "server",
527
+ label: "Gate profile",
528
+ status: "fail",
529
+ detail: `${profile.verdict === "denied" ? "DENIED" : "could not be verified"}${profile.reason ? ` (${profile.reason})` : ""} — ${detail}`,
530
+ remediation: gateProfileReasonRemedy(profile.reason, profile.verdict, profile.subject) ??
531
+ (profile.verdict === "denied"
532
+ ? ADMISSION_OWNER_REMEDIATION.repository_admin
533
+ : ADMISSION_OWNER_REMEDIATION.bridge_operations),
534
+ });
535
+ }
536
+ else if (profile.verdict === "confirmed" && profile.kind !== "none") {
537
+ row = createReadinessCheckSafely({
538
+ id: "server.gate-profile",
539
+ source: "server",
540
+ label: "Gate profile",
541
+ status: "pass",
542
+ detail,
543
+ });
544
+ }
545
+ else {
546
+ row = createReadinessCheckSafely({
547
+ id: "server.gate-profile",
548
+ source: "server",
549
+ label: "Gate profile",
550
+ status: "warn",
551
+ detail,
552
+ remediation: profile.kind === "none"
553
+ ? "no action is needed to launch; to let child merges proceed without approval, a repository administrator adds checks to the gate profile."
554
+ : "a repository administrator confirms the gate profile; the server's launch evidence decides whether a run is held.",
555
+ });
556
+ }
557
+ const warnings = profile.warnings.map((warning, index) => createReadinessCheckSafely({
558
+ id: `server.gate-profile-warning-${index + 1}`,
559
+ source: "server",
560
+ label: "Gate profile warning",
561
+ status: "warn",
562
+ detail: describeGateProfileWarning(warning.code, warning.name),
563
+ remediation: GATE_PROFILE_WARNING_REMEDIATION,
564
+ }));
565
+ return [row, ...warnings];
566
+ }
567
+ /**
568
+ * BAPI-1405 — the three warn-only epic-branch CI rows as `server.*` checks:
569
+ * `server.effective-review-mode`, `server.epic-branch-ci-decision`, and
570
+ * `server.epic-branch-ci-coverage-warning`.
571
+ *
572
+ * Built from the SAME shared sections the install doctor renders, so the two
573
+ * surfaces cannot word one fact apart. Status is only ever `pass` or `warn`:
574
+ * these rows never fail, never hold a launch, and feed no verdict.
575
+ *
576
+ * Unlike `gateProfileChecks`, an absent or `null` block (older server) still
577
+ * yields the three rows, each `warn` with "Unreported — upgrade the server" and
578
+ * the upgrade remediation. Emitting nothing would read as "no warning", which
579
+ * is the wrong answer: the server simply did not say. Omitted and `null` render
580
+ * identically. The install doctor's own copies of these sections are filtered
581
+ * by `INSTALL_SECTION_IDS_OWNED_ELSEWHERE`, so each row still appears once.
582
+ */
583
+ export function epicBranchCiChecks(readiness) {
584
+ return epicBranchCiSections(readiness.epic_branch_ci).map((section) => createReadinessCheckSafely({
585
+ id: `server.${section.id}`,
586
+ source: "server",
587
+ label: section.label,
588
+ status: section.status === "ok" ? "pass" : "warn",
589
+ detail: section.detail,
590
+ ...(section.status === "ok" || section.remediation === undefined
591
+ ? {}
592
+ : { remediation: section.remediation }),
593
+ }));
594
+ }
439
595
  /**
440
596
  * Project the server's typed launch evidence (BAPI-1260) into `server.*`
441
597
  * checks, read-only.
@@ -626,11 +782,12 @@ function unattendedOutcomes(readiness) {
626
782
  detail: readiness.review_workflow?.probe_succeeded === true &&
627
783
  readiness.review_workflow?.workflow_present === false
628
784
  ? `${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 &&
785
+ : !gateProfileWaivesCiWorkflow(readiness) &&
786
+ readiness.conductor_ci_workflow?.probe_succeeded === true &&
630
787
  readiness.conductor_ci_workflow?.workflow_present === false
631
788
  ? `${CI_WORKFLOW_PATH} is missing from the default branch — an unattended auto-merging run will be refused`
632
789
  : "the review and epic-implementer-ci workflows are not both confirmed — an unattended auto-merging run will be refused",
633
- remediation: repositoryWorkflowRemedy(readiness),
790
+ remediation: gateProfileAwareRepositoryWorkflowRemedy(readiness),
634
791
  },
635
792
  ];
636
793
  }
@@ -639,6 +796,15 @@ function reviewPolicyOutcome(readiness) {
639
796
  const s = readiness.supervisor;
640
797
  const alignment = s.review_policy_alignment;
641
798
  if (alignment !== null) {
799
+ // BAPI-1405 — `repository_policy` requires no review verdict, so a divergent
800
+ // done-gate review signal is not a nag. `invalid` still warns.
801
+ if (alignment.status === "divergent" && effectiveReviewModeIsRepositoryPolicy(readiness)) {
802
+ return {
803
+ id: "review-policy",
804
+ status: "pass",
805
+ detail: "alignment not applicable: the effective review mode is 'repository_policy', which requires no review verdict",
806
+ };
807
+ }
642
808
  if (alignment.status === "divergent" || alignment.status === "invalid") {
643
809
  return {
644
810
  id: "review-policy",
@@ -839,7 +1005,23 @@ function executorOutcome(readiness) {
839
1005
  remediation: SERVER_REMEDIATIONS.executorNeverSeen,
840
1006
  };
841
1007
  }
842
- /** Review-workflow protocol facts. `null` means an older server, not health. */
1008
+ /**
1009
+ * BAPI-1390 — the repository-workflow remedy under the server's gate-profile
1010
+ * rule: with a STORED gate profile the CI lane file is not required, so the
1011
+ * remedy never names it. Without one, this is exactly
1012
+ * {@link repositoryWorkflowRemedy}.
1013
+ */
1014
+ export function gateProfileAwareRepositoryWorkflowRemedy(readiness) {
1015
+ return repositoryWorkflowRemedy(gateProfileWaivesCiWorkflow(readiness) ? { ...readiness, conductor_ci_workflow: null } : readiness);
1016
+ }
1017
+ /**
1018
+ * Review-workflow protocol facts. `null` means an older server, not health.
1019
+ *
1020
+ * BAPI-1390: the server now requires the review file only for a run whose review
1021
+ * policy source is `verdict_protocol`. This repository-scoped report carries no
1022
+ * run policy, so the row keeps its existing behavior; the per-run decision is the
1023
+ * server's (`unattended.repository_readiness_confirmed` and admission evidence).
1024
+ */
843
1025
  function reviewWorkflowOutcome(readiness) {
844
1026
  const w = readiness.review_workflow;
845
1027
  if (w === null) {
@@ -892,6 +1074,16 @@ function reviewWorkflowOutcome(readiness) {
892
1074
  /** Conductor-CI migration-guard facts. `null` means an older server. */
893
1075
  function ciWorkflowOutcome(readiness) {
894
1076
  const c = readiness.conductor_ci_workflow;
1077
+ // BAPI-1390 — a STORED gate profile names its own checks and migration guard,
1078
+ // so the lane file is not required. A healthy file still reports a pass; any
1079
+ // other state is reported as not applicable rather than as a gap.
1080
+ if (gateProfileWaivesCiWorkflow(readiness) && !(c?.probe_succeeded === true && c.migration_guard_present === true)) {
1081
+ return {
1082
+ id: "conductor-ci-workflow",
1083
+ status: "skip",
1084
+ detail: `${CI_WORKFLOW_PATH} is not required: this repository has a stored gate profile that names its own checks`,
1085
+ };
1086
+ }
895
1087
  if (c === null) {
896
1088
  return {
897
1089
  id: "conductor-ci-workflow",
@@ -974,6 +1166,15 @@ const INSTALL_SECTION_IDS_OWNED_ELSEWHERE = new Set([
974
1166
  "supervisor-configuration",
975
1167
  "supervisor-required-checks",
976
1168
  "supervisor-review-alignment",
1169
+ // BAPI-1390 — owned by `gateProfileChecks`.
1170
+ "supervisor-gate-profile",
1171
+ "supervisor-gate-profile-epic-branch-unseen",
1172
+ "supervisor-gate-profile-epic-branch-search",
1173
+ "supervisor-gate-profile-warning",
1174
+ // BAPI-1405 — owned by `epicBranchCiChecks`.
1175
+ EPIC_BRANCH_CI_SECTION_IDS.reviewMode,
1176
+ EPIC_BRANCH_CI_SECTION_IDS.decision,
1177
+ EPIC_BRANCH_CI_SECTION_IDS.coverage,
977
1178
  "github-credentials",
978
1179
  "github-actions-permission",
979
1180
  "reconciler-liveness",
@@ -1059,7 +1260,9 @@ export async function collectConductorReadinessGate(deps) {
1059
1260
  ...mapInstallStatusChecksToReadinessChecks((installReport?.legacyInstallChecks ?? null), installReport === null),
1060
1261
  ...mapConductorDoctorReportToReadinessChecks(installReport?.legacyConductor ?? null),
1061
1262
  ...mapInstallSectionsToReadinessChecks(installReport?.sections ?? null),
1062
- ...mapPlanePreflightToReadinessChecks(planeResult, deps.planeNotApplicable),
1263
+ ...mapPlanePreflightToReadinessChecks(planeResult, deps.planeNotApplicable, {
1264
+ ...(deps.epicRunId !== undefined ? { epicRunId: deps.epicRunId } : {}),
1265
+ }),
1063
1266
  ...mapServerReadinessToChecks(installReport?.readiness ?? null, classifyServerFailure(installReport)),
1064
1267
  ];
1065
1268
  // Validate the ASSEMBLED report, not just the pieces: an injected collector or
@@ -122,7 +122,7 @@ function parseRunRecoveryArgs(argv) {
122
122
  return { epicRunId: epicRunId.trim(), json: bools.has("--json"), help: false };
123
123
  }
124
124
  function parseTicketRecoveryArgs(argv) {
125
- const { values, bools } = tokenizeRecoveryFlags(argv, TICKET_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
125
+ const { values, bools } = tokenizeRecoveryFlags(argv, new Set([...TICKET_VALUE_FLAGS, "--reason", "--cleanup-resolution"]), RECOVERY_BOOL_FLAGS);
126
126
  if (bools.has("--help")) {
127
127
  return { epicRunId: "", ticketKey: "", json: bools.has("--json"), help: true };
128
128
  }
@@ -137,6 +137,10 @@ function parseTicketRecoveryArgs(argv) {
137
137
  return {
138
138
  epicRunId: epicRunId.trim(),
139
139
  ticketKey: ticketKey.trim(),
140
+ ...(values.get("--reason") ? { reason: values.get("--reason").trim() } : {}),
141
+ ...(values.get("--cleanup-resolution") ? {
142
+ cleanupResolution: values.get("--cleanup-resolution"),
143
+ } : {}),
140
144
  json: bools.has("--json"),
141
145
  help: false,
142
146
  };
@@ -263,7 +267,7 @@ export async function runAbandonRunCommand(argv) {
263
267
  // unpark / adopt-current-head-and-unpark (shared rendering)
264
268
  // ---------------------------------------------------------------------------
265
269
  const UNPARK_USAGE = [
266
- "Usage: epic-implementer unpark --epic-run-id <id> --ticket-key <key> [--json]",
270
+ "Usage: epic-implementer unpark --epic-run-id <id> --ticket-key <key> [--cleanup-resolution confirmed|risk_accepted --reason <text>] [--json]",
267
271
  "",
268
272
  "Move a parked (needs_human) ticket back into its gate machine, once the",
269
273
  "operator has resolved what parked it. Retries a bounded number of times on",
@@ -337,6 +341,13 @@ export function renderTicketRecoveryResult(result, json) {
337
341
  export async function runUnparkCommand(argv) {
338
342
  try {
339
343
  const parsed = parseTicketRecoveryArgs(argv);
344
+ if (parsed.cleanupResolution !== undefined &&
345
+ !["confirmed", "risk_accepted"].includes(parsed.cleanupResolution)) {
346
+ throw new ConductorValidationError("--cleanup-resolution must be confirmed or risk_accepted.");
347
+ }
348
+ if (parsed.cleanupResolution !== undefined && !parsed.reason) {
349
+ throw new ConductorValidationError("--cleanup-resolution requires --reason.");
350
+ }
340
351
  if (parsed.help) {
341
352
  console.log(UNPARK_USAGE);
342
353
  return 0;
@@ -348,6 +359,8 @@ export async function runUnparkCommand(argv) {
348
359
  const result = await unparkEpicTicketWithRetry(access, {
349
360
  epicRunId: parsed.epicRunId,
350
361
  ticketKey: parsed.ticketKey,
362
+ reason: parsed.reason,
363
+ cleanupResolution: parsed.cleanupResolution,
351
364
  });
352
365
  return renderTicketRecoveryResult(result, parsed.json);
353
366
  }
@@ -359,6 +372,9 @@ export async function runUnparkCommand(argv) {
359
372
  export async function runAdoptCurrentHeadAndUnparkCommand(argv) {
360
373
  try {
361
374
  const parsed = parseTicketRecoveryArgs(argv);
375
+ if (parsed.cleanupResolution !== undefined || parsed.reason !== undefined) {
376
+ throw new ConductorValidationError("Cleanup resolution applies only to unpark.");
377
+ }
362
378
  if (parsed.help) {
363
379
  console.log(ADOPT_CURRENT_HEAD_USAGE);
364
380
  return 0;
@@ -1320,11 +1336,11 @@ export async function runResumeRunCommand(argv) {
1320
1336
  // ---------------------------------------------------------------------------
1321
1337
  // continue-child / continuation-receipt (BAPI-1300)
1322
1338
  // ---------------------------------------------------------------------------
1323
- const CONTINUE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--reason"]);
1339
+ const CONTINUE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--reason", "--worker-exit-proof"]);
1324
1340
  const CONTINUATION_RECEIPT_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--request-id"]);
1325
1341
  const MAX_REASON_CHARS = 500;
1326
1342
  const CONTINUE_CHILD_USAGE = [
1327
- "Usage: epic-implementer continue-child --epic-run-id <id> --ticket-key <key> [--reason <text>] [--json]",
1343
+ "Usage: epic-implementer continue-child --epic-run-id <id> --ticket-key <key> [--reason <text> --worker-exit-proof exited|fenced] [--json]",
1328
1344
  "",
1329
1345
  "Continue a parked child's CURRENT park episode without resetting it: the same",
1330
1346
  "attempt resumes on its retained work with only the allowance it has left (no",
@@ -1351,6 +1367,13 @@ function parseContinuationArgs(argv, valueFlags) {
1351
1367
  const epicRunId = requireFlag(values, "--epic-run-id");
1352
1368
  const ticketKey = requireFlag(values, "--ticket-key");
1353
1369
  const reason = values.get("--reason")?.trim();
1370
+ const workerExitProof = values.get("--worker-exit-proof")?.trim();
1371
+ if (workerExitProof !== undefined && workerExitProof !== "exited" && workerExitProof !== "fenced") {
1372
+ throw new ConductorValidationError('Flag "--worker-exit-proof" must be exited or fenced.');
1373
+ }
1374
+ if (workerExitProof !== undefined && !reason) {
1375
+ throw new ConductorValidationError('Flag "--worker-exit-proof" requires --reason.');
1376
+ }
1354
1377
  if (reason !== undefined && reason.length > MAX_REASON_CHARS) {
1355
1378
  throw new ConductorValidationError(`Flag "--reason" must be at most ${MAX_REASON_CHARS} characters.`);
1356
1379
  }
@@ -1362,6 +1385,7 @@ function parseContinuationArgs(argv, valueFlags) {
1362
1385
  epicRunId,
1363
1386
  ticketKey,
1364
1387
  ...(reason ? { reason } : {}),
1388
+ ...(workerExitProof ? { workerExitProof: workerExitProof } : {}),
1365
1389
  ...(requestId ? { requestId } : {}),
1366
1390
  json: bools.has("--json"),
1367
1391
  help: false,
@@ -1579,6 +1603,7 @@ export async function runContinueChildCommand(argv) {
1579
1603
  epicRunId: parsed.epicRunId,
1580
1604
  ticketKey: parsed.ticketKey,
1581
1605
  ...(parsed.reason ? { reason: parsed.reason } : {}),
1606
+ ...(parsed.workerExitProof ? { workerExitProof: parsed.workerExitProof } : {}),
1582
1607
  });
1583
1608
  return renderContinueChildResult(result, parsed, parsed.json);
1584
1609
  }
@@ -1587,6 +1612,60 @@ export async function runContinueChildCommand(argv) {
1587
1612
  return 1;
1588
1613
  }
1589
1614
  }
1615
+ /** Explicit, bounded grant for the current parked attempt. */
1616
+ export async function runGrantContinuationCommand(argv) {
1617
+ try {
1618
+ const { values, bools } = tokenizeRecoveryFlags(argv, new Set(["--epic-run-id", "--ticket-key", "--extra-continuations", "--extra-seconds", "--reason", "--request-id"]), RECOVERY_BOOL_FLAGS);
1619
+ if (bools.has("--help")) {
1620
+ console.log("Usage: epic-implementer grant-continuation --epic-run-id <id> --ticket-key <key> --reason <text> [--extra-continuations 1] [--extra-seconds <1..3600>] [--request-id <id>] [--json]");
1621
+ return 0;
1622
+ }
1623
+ const epicRunId = requireFlag(values, "--epic-run-id");
1624
+ const ticketKey = requireFlag(values, "--ticket-key");
1625
+ const reason = requireFlag(values, "--reason");
1626
+ const extraContinuations = Number(values.get("--extra-continuations") ?? "0");
1627
+ const extraSeconds = Number(values.get("--extra-seconds") ?? "0");
1628
+ if (!Number.isInteger(extraContinuations) || extraContinuations < 0 || extraContinuations > 1 ||
1629
+ !Number.isInteger(extraSeconds) || extraSeconds < 0 || extraSeconds > 3600 ||
1630
+ (extraContinuations === 0 && extraSeconds === 0) || reason.length > MAX_REASON_CHARS) {
1631
+ throw new ConductorValidationError("Grant amounts or reason are outside the allowed bounds.");
1632
+ }
1633
+ const explicitRequestId = values.get("--request-id");
1634
+ if (explicitRequestId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/.test(explicitRequestId)) {
1635
+ throw new ConductorValidationError("--request-id is malformed.");
1636
+ }
1637
+ const json = bools.has("--json");
1638
+ const access = await resolveRecoveryAccess(json);
1639
+ if (!access)
1640
+ return 1;
1641
+ const { grantContinuationOnce } = await import("./recovery-operations.js");
1642
+ const operation = await grantContinuationOnce(access, {
1643
+ epicRunId, ticketKey, extraContinuations, extraSeconds, reason,
1644
+ ...(explicitRequestId !== undefined ? { requestId: explicitRequestId } : {}),
1645
+ });
1646
+ if (operation.kind === "not_parked") {
1647
+ throw new ConductorValidationError("The child is not parked at a continuable gate.");
1648
+ }
1649
+ if (operation.kind === "unavailable") {
1650
+ console.error(`Error: ${operation.message}`);
1651
+ return 1;
1652
+ }
1653
+ const { requestId, outcome: result } = operation;
1654
+ if (json)
1655
+ console.log(JSON.stringify({ ...result, request_id: requestId }));
1656
+ else if (result.kind === "result")
1657
+ console.log(`Grant ${result.replayed ? "replayed" : "committed"}: ${requestId}. The child remains parked.`);
1658
+ else if (result.kind === "unconfirmed")
1659
+ console.error(`Grant outcome unconfirmed. Rerun the same command; it reuses request ${requestId}.`);
1660
+ else
1661
+ console.error(`Grant refused (${result.status}${result.errorCode ? `, ${result.errorCode}` : ""}).`);
1662
+ return result.kind === "result" ? 0 : 1;
1663
+ }
1664
+ catch (error) {
1665
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
1666
+ return 1;
1667
+ }
1668
+ }
1590
1669
  export async function runContinuationReceiptCommand(argv) {
1591
1670
  try {
1592
1671
  const parsed = parseContinuationReceiptArgs(argv);
@@ -1845,3 +1924,30 @@ export async function runResumeCiCommand(argv) {
1845
1924
  return RESUME_CI_EXIT_FAILED;
1846
1925
  }
1847
1926
  }
1927
+ /** No state mutation: callers halt the affected work before asking a question. */
1928
+ export async function runEscalateCommand(argv, deps = {}) {
1929
+ try {
1930
+ const { values, bools } = tokenizeRecoveryFlags(argv, new Set(["--epic-run-id", "--ticket-key", "--cause", "--action-needed"]), RECOVERY_BOOL_FLAGS);
1931
+ if (bools.has("--help")) {
1932
+ console.log("Usage: epic-implementer escalate --epic-run-id <id> --ticket-key <key> --cause <cause> --action-needed <text> [--json]");
1933
+ return 0;
1934
+ }
1935
+ const request = { epicRunId: requireFlag(values, "--epic-run-id"),
1936
+ ticketKey: requireFlag(values, "--ticket-key"), cause: requireFlag(values, "--cause"),
1937
+ actionNeeded: requireFlag(values, "--action-needed") };
1938
+ if (request.actionNeeded.length > 300) {
1939
+ console.error("Error: action-needed exceeds 300 characters");
1940
+ return 1;
1941
+ }
1942
+ const access = await (deps.resolveAccess ?? (() => resolveRecoveryAccess(false)))();
1943
+ if (!access)
1944
+ return 1;
1945
+ const escalate = deps.escalate ?? (await import("./bridge-api-client.js")).escalateTicket;
1946
+ console.log(JSON.stringify(await escalate(access, request)));
1947
+ return 0;
1948
+ }
1949
+ catch {
1950
+ console.error("Error: escalation request refused or unavailable");
1951
+ return 1;
1952
+ }
1953
+ }
@@ -18,7 +18,7 @@
18
18
  * callers: `recovery-cli.ts` and `plane/shutdown.ts`.
19
19
  */
20
20
  import { randomUUID } from "crypto";
21
- import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, RERUN_CI_ROUTE_UNAVAILABLE_CODE, RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE, rerunCi, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, resumeCiTicket, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
21
+ import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, RERUN_CI_ROUTE_UNAVAILABLE_CODE, RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE, rerunCi, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, postGrantContinuation, resumeCiTicket, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
22
22
  import { clearGuidedReference, guardForBinding, GUIDED_PAYLOAD_CONFLICT_MESSAGE, GUIDED_REFERENCE_STORE_FAILED_MESSAGE, guidedPayloadDigest, prepareGuidedRequest, } from "./guided-repair.js";
23
23
  import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
24
24
  import { canonicalSha256, clearContinuationReference, clearRecoveryReferenceFile, computeContinuationFingerprint, readRecoveryReferenceFile, writeRecoveryReferenceFile, isFullSha, newContinuationRequestId, readContinuationReference, writeContinuationReference, } from "./epic-child-continuation.js";
@@ -179,6 +179,7 @@ async function ticketRecoveryWithRetry(access, options, mutate) {
179
179
  expectedRowVersion: ticket.row_version,
180
180
  idempotencyKey,
181
181
  reason: options.reason,
182
+ cleanupResolution: options.cleanupResolution,
182
183
  });
183
184
  }
184
185
  catch (err) {
@@ -697,6 +698,7 @@ export async function continueChildOnce(access, options, deps = {}) {
697
698
  requestId: ref.requestId,
698
699
  requestFingerprint: ref.requestFingerprint,
699
700
  reason: options.reason,
701
+ workerExitProof: options.workerExitProof,
700
702
  });
701
703
  if (outcome.kind === "result" && AUTHORITATIVE_OUTCOMES.has(outcome.result.outcome)) {
702
704
  await clear();
@@ -724,6 +726,102 @@ export async function continueChildOnce(access, options, deps = {}) {
724
726
  }
725
727
  return { kind: "unconfirmed", requestId: ref.requestId, reusedReference, transport };
726
728
  }
729
+ /** The namespace of pending `grant-continuation` requests (BAPI-1409). */
730
+ export const CONTINUATION_GRANT_REFERENCE_NAMESPACE = "epic-continuation-grants";
731
+ const GRANT_REQUEST_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/;
732
+ function pendingGrantMatches(raw, wanted) {
733
+ if (raw === null || typeof raw !== "object")
734
+ return null;
735
+ const ref = raw;
736
+ if (ref.version !== 1 ||
737
+ ref.repoName !== wanted.repoName ||
738
+ ref.parkedGate !== wanted.parkedGate ||
739
+ ref.sourceParkGeneration !== wanted.sourceParkGeneration ||
740
+ ref.extraContinuations !== wanted.extraContinuations ||
741
+ ref.extraSeconds !== wanted.extraSeconds ||
742
+ ref.reason !== wanted.reason ||
743
+ typeof ref.requestId !== "string" ||
744
+ !GRANT_REQUEST_ID_RE.test(ref.requestId)) {
745
+ return null;
746
+ }
747
+ return ref.requestId;
748
+ }
749
+ /**
750
+ * Grant bounded continuation capacity, at most one POST per invocation.
751
+ *
752
+ * The request id is pinned locally BEFORE the POST, keyed by run and ticket, and
753
+ * reused while the park episode, gate, amounts, and reason are unchanged. A rerun
754
+ * after a lost response therefore replays the same grant (the server answers it
755
+ * from the stored grant) instead of spending a second one. The reference is
756
+ * cleared once the outcome is authoritative and kept when it is unconfirmed.
757
+ */
758
+ export async function grantContinuationOnce(access, options, deps = {}) {
759
+ const fetchStatus = deps.fetchStatus ?? fetchChildContinuationStatus;
760
+ const post = deps.post ?? postGrantContinuation;
761
+ const store = deps.store ?? {};
762
+ const key = { epicRunId: options.epicRunId, ticketKey: options.ticketKey };
763
+ let status;
764
+ try {
765
+ status = await fetchStatus(access, key);
766
+ }
767
+ catch (err) {
768
+ return { kind: "unavailable", message: safeDiagnosticMessage(err, "could not read the child") };
769
+ }
770
+ const current = status.current;
771
+ if (!current.parked || current.parked_gate === null || current.park_generation === null) {
772
+ return { kind: "not_parked" };
773
+ }
774
+ const wanted = {
775
+ version: 1,
776
+ repoName: access.repoName,
777
+ parkedGate: current.parked_gate,
778
+ sourceParkGeneration: current.park_generation,
779
+ extraContinuations: options.extraContinuations,
780
+ extraSeconds: options.extraSeconds,
781
+ reason: options.reason,
782
+ };
783
+ let requestId = options.requestId ?? null;
784
+ let reusedReference = false;
785
+ const pinned = requestId === null;
786
+ if (pinned) {
787
+ const stored = pendingGrantMatches(await readRecoveryReferenceFile(CONTINUATION_GRANT_REFERENCE_NAMESPACE, key, store), wanted);
788
+ if (stored !== null) {
789
+ requestId = stored;
790
+ reusedReference = true;
791
+ }
792
+ else {
793
+ requestId = `grant_${(deps.newRequestId ?? randomUUID)()}`;
794
+ const reference = {
795
+ ...wanted, requestId, createdAt: new Date().toISOString(),
796
+ };
797
+ try {
798
+ await writeRecoveryReferenceFile(CONTINUATION_GRANT_REFERENCE_NAMESPACE, key, reference, store);
799
+ }
800
+ catch {
801
+ return {
802
+ kind: "unavailable",
803
+ message: "could not store the local grant reference, so the request was not sent",
804
+ };
805
+ }
806
+ }
807
+ }
808
+ const id = requestId;
809
+ const outcome = await post(access, {
810
+ epicRunId: options.epicRunId,
811
+ ticketKey: options.ticketKey,
812
+ requestId: id,
813
+ parkedGate: current.parked_gate,
814
+ expectedRowVersion: current.row_version,
815
+ sourceParkGeneration: current.park_generation,
816
+ extraContinuations: options.extraContinuations,
817
+ extraSeconds: options.extraSeconds,
818
+ reason: options.reason,
819
+ });
820
+ if (pinned && outcome.kind !== "unconfirmed") {
821
+ await clearRecoveryReferenceFile(CONTINUATION_GRANT_REFERENCE_NAMESPACE, key, store);
822
+ }
823
+ return { kind: "posted", requestId: id, reusedReference, outcome };
824
+ }
727
825
  /**
728
826
  * Read a child's current episode and, when a local recovery reference exists (or
729
827
  * `requestId` is given), that request's receipt. One bounded GET; read-only. A