@bridge_gpt/mcp-server 0.2.66 → 0.2.68

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 (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -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
@@ -121,7 +122,7 @@ function parseRunRecoveryArgs(argv) {
121
122
  return { epicRunId: epicRunId.trim(), json: bools.has("--json"), help: false };
122
123
  }
123
124
  function parseTicketRecoveryArgs(argv) {
124
- 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);
125
126
  if (bools.has("--help")) {
126
127
  return { epicRunId: "", ticketKey: "", json: bools.has("--json"), help: true };
127
128
  }
@@ -136,6 +137,10 @@ function parseTicketRecoveryArgs(argv) {
136
137
  return {
137
138
  epicRunId: epicRunId.trim(),
138
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
+ } : {}),
139
144
  json: bools.has("--json"),
140
145
  help: false,
141
146
  };
@@ -262,7 +267,7 @@ export async function runAbandonRunCommand(argv) {
262
267
  // unpark / adopt-current-head-and-unpark (shared rendering)
263
268
  // ---------------------------------------------------------------------------
264
269
  const UNPARK_USAGE = [
265
- "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]",
266
271
  "",
267
272
  "Move a parked (needs_human) ticket back into its gate machine, once the",
268
273
  "operator has resolved what parked it. Retries a bounded number of times on",
@@ -336,6 +341,13 @@ export function renderTicketRecoveryResult(result, json) {
336
341
  export async function runUnparkCommand(argv) {
337
342
  try {
338
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
+ }
339
351
  if (parsed.help) {
340
352
  console.log(UNPARK_USAGE);
341
353
  return 0;
@@ -347,6 +359,8 @@ export async function runUnparkCommand(argv) {
347
359
  const result = await unparkEpicTicketWithRetry(access, {
348
360
  epicRunId: parsed.epicRunId,
349
361
  ticketKey: parsed.ticketKey,
362
+ reason: parsed.reason,
363
+ cleanupResolution: parsed.cleanupResolution,
350
364
  });
351
365
  return renderTicketRecoveryResult(result, parsed.json);
352
366
  }
@@ -358,6 +372,9 @@ export async function runUnparkCommand(argv) {
358
372
  export async function runAdoptCurrentHeadAndUnparkCommand(argv) {
359
373
  try {
360
374
  const parsed = parseTicketRecoveryArgs(argv);
375
+ if (parsed.cleanupResolution !== undefined || parsed.reason !== undefined) {
376
+ throw new ConductorValidationError("Cleanup resolution applies only to unpark.");
377
+ }
361
378
  if (parsed.help) {
362
379
  console.log(ADOPT_CURRENT_HEAD_USAGE);
363
380
  return 0;
@@ -709,11 +726,13 @@ export async function runScopeReparseCommand(argv) {
709
726
  const DISPATCH_REMEDIATION_USAGE = [
710
727
  "Usage: epic-implementer dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path> [--json]",
711
728
  "",
712
- "Dispatch a remediation job for a ticket at code_review (or parked there),",
729
+ "Dispatch a remediation job for a ticket at code_review or ci (or parked there),",
713
730
  "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",
731
+ "(remediate, or reconcile_overlap for a conflict/overlap ticket; ci_fix for a CI",
732
+ "failure the server's FRESH CI read routes to investigation) and admits it",
715
733
  "through the reconciler's own strict-serial lane, remediation budget, and",
716
- "dedup key. A parked ticket is resumed in the same step.",
734
+ "dedup key. A parked ticket is resumed in the same step (unguided only). An",
735
+ "infrastructure CI failure is refused by name and points to rerun-ci.",
717
736
  "",
718
737
  `--context-file must be non-empty UTF-8 text of at most DRIVER_CONTEXT_MAX_BYTES`,
719
738
  `(${DRIVER_CONTEXT_MAX_BYTES} bytes). It is rejected, never truncated, and is never`,
@@ -764,7 +783,7 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
764
783
  guard,
765
784
  }),
766
785
  label: `ticket ${parsed.ticketKey}`,
767
- stateLabel: "code_review gate",
786
+ stateLabel: "selected gate",
768
787
  }, parsed.json);
769
788
  }
770
789
  const { dispatchRemediationRepair } = await import("./recovery-operations.js");
@@ -777,7 +796,7 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
777
796
  verb: "dispatch-remediation",
778
797
  epicRunId: parsed.epicRunId,
779
798
  targetLabel: `ticket ${parsed.ticketKey}`,
780
- stateLabel: "code_review gate",
799
+ stateLabel: "selected gate",
781
800
  }, parsed.json, (result) => [
782
801
  ` job: ${result.job_type || "unknown"}${result.job_id !== null ? ` (job ${result.job_id})` : ""}`,
783
802
  ]);
@@ -787,6 +806,195 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
787
806
  return 1;
788
807
  }
789
808
  }
809
+ // ---------------------------------------------------------------------------
810
+ // BAPI-1388 — rerun-ci
811
+ // ---------------------------------------------------------------------------
812
+ const RERUN_CI_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--binding"]);
813
+ /** Exit codes: 0 applied (including an all-not-re-runnable no-op), 2 refused/stale, 3 unconfirmed, 1 other. */
814
+ export const RERUN_CI_EXIT_OK = 0;
815
+ export const RERUN_CI_EXIT_FAILED = 1;
816
+ export const RERUN_CI_EXIT_REFUSED = 2;
817
+ export const RERUN_CI_EXIT_UNCONFIRMED = 3;
818
+ const RERUN_CI_USAGE = [
819
+ "Usage: epic-implementer rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
820
+ "",
821
+ "Ask the CI provider to re-run a ticket's FAILED required checks at its bound head,",
822
+ "for a ticket at ci or parked on it. Copy the command exactly as the run",
823
+ "explanation rendered it: --binding pins the ticket version and head it showed.",
824
+ "The SERVER re-reads fresh CI evidence, re-runs each GitHub Actions workflow run at",
825
+ "most once per head, and reports every failed check as enqueued, deduplicated,",
826
+ "deferred, or not_re_runnable.",
827
+ "",
828
+ "rerun-ci NEVER re-runs the claude-review workflow. It charges no allowance and",
829
+ "changes no park state, and a re-run is not a verdict: when CI is green, use",
830
+ "`epic-implementer resume-ci` (a parked ticket) or let the reconciler observe it.",
831
+ "If the failure is a code defect, use `dispatch-remediation --context-file`.",
832
+ "",
833
+ "The request is stored locally before it is sent, so re-running the same command",
834
+ "after a lost response re-sends the SAME request. A subject that has since changed",
835
+ "is refused as stale with nothing written.",
836
+ "",
837
+ "Exit codes: 0 applied (including a no-op when nothing could be re-run); 2 refused",
838
+ "or stale; 3 unconfirmed; 1 other failure (including a server without this verb).",
839
+ ].join("\n");
840
+ export function parseRerunCiArgs(argv) {
841
+ const { values, bools } = tokenizeRecoveryFlags(argv, RERUN_CI_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
842
+ if (bools.has("--help")) {
843
+ return { epicRunId: "", ticketKey: "", binding: "", json: bools.has("--json"), help: true };
844
+ }
845
+ return {
846
+ epicRunId: requireFlag(values, "--epic-run-id"),
847
+ ticketKey: requireFlag(values, "--ticket-key"),
848
+ binding: requireFlag(values, "--binding"),
849
+ json: bools.has("--json"),
850
+ help: false,
851
+ };
852
+ }
853
+ const RERUN_CI_STATUS_TEXT = {
854
+ enqueued: "re-run requested",
855
+ deduplicated: "already requested",
856
+ deferred: "deferred (not requested; run the verb again after a fresh explain-run)",
857
+ not_re_runnable: "not re-runnable",
858
+ };
859
+ const RERUN_CI_REASON_TEXT = {
860
+ not_github_actions: "not a GitHub Actions check",
861
+ no_workflow_run_id: "no workflow run id",
862
+ claude_review_excluded: "claude-review is never re-run",
863
+ };
864
+ const RERUN_CI_OUTCOME_TEXT = {
865
+ enqueued: "re-runs requested",
866
+ partial: "partially requested (some runs deferred)",
867
+ nothing_to_enqueue: "nothing to enqueue (no failed check could be re-run)",
868
+ };
869
+ const RERUN_CI_NEXT_ACTION = {
870
+ enqueued: "Wait for the re-runs to finish, then read `epic-implementer explain-run`. When CI is " +
871
+ "green use `epic-implementer resume-ci` (a parked ticket); this verb did not mark CI passed.",
872
+ partial: "Some runs were not requested. Read `epic-implementer explain-run` and, if still " +
873
+ "offered, re-run the command it renders with its fresh binding.",
874
+ nothing_to_enqueue: "No re-run was possible. Read `epic-implementer explain-run`; for a code defect use " +
875
+ "`dispatch-remediation --context-file`.",
876
+ };
877
+ function rerunCiCheckLine(check) {
878
+ const identity = [
879
+ check.name ?? "(unnamed check)",
880
+ check.workflow_identity ? `workflow ${check.workflow_identity}` : null,
881
+ check.run_id !== null ? `run ${check.run_id}` : null,
882
+ ]
883
+ .filter((part) => part !== null)
884
+ .join(", ");
885
+ const reason = check.reason ? ` — ${RERUN_CI_REASON_TEXT[check.reason] ?? check.reason}` : "";
886
+ const job = check.job_id !== null ? ` (job ${check.job_id})` : "";
887
+ return ` - ${identity}: ${RERUN_CI_STATUS_TEXT[check.status] ?? check.status}${reason}${job}`;
888
+ }
889
+ /**
890
+ * Exported for tests: render one `rerun-ci` outcome. Success goes to stdout and
891
+ * every failure to stderr (`--json` included). An unsupported server is NEVER a
892
+ * success and says it is "not available on this server".
893
+ */
894
+ export function renderRerunCiOutcome(outcome, parsed, json) {
895
+ const label = `ticket ${parsed.ticketKey} (run ${parsed.epicRunId})`;
896
+ if (outcome.kind === "applied") {
897
+ const result = outcome.result;
898
+ if (result.committed && !result.event_recorded) {
899
+ console.error(`Warning: rerun-ci for ${label} COMMITTED, but its operator_verb_applied audit ` +
900
+ "event was not recorded. The action stands.");
901
+ }
902
+ if (json) {
903
+ console.log(JSON.stringify({
904
+ ok: true,
905
+ outcome: "applied",
906
+ request_id: outcome.requestId,
907
+ reference_reused: outcome.referenceReused,
908
+ result,
909
+ }));
910
+ return RERUN_CI_EXIT_OK;
911
+ }
912
+ const lines = [
913
+ `rerun-ci (guided) — ${label}`,
914
+ ` ticket: ${result.ticket_key}`,
915
+ ` head: ${result.head_sha ? "`" + result.head_sha + "`" : "unknown"}`,
916
+ ` outcome: ${RERUN_CI_OUTCOME_TEXT[result.outcome] ?? result.outcome}`,
917
+ " failed checks:",
918
+ ...result.checks.map(rerunCiCheckLine),
919
+ ` next action: ${RERUN_CI_NEXT_ACTION[result.outcome] ?? "Read `epic-implementer explain-run`."}`,
920
+ ` request: ${outcome.requestId}${outcome.referenceReused ? " (re-sent from the local reference)" : ""}`,
921
+ ` event recorded: ${result.event_recorded}`,
922
+ ];
923
+ console.log(lines.join("\n"));
924
+ return RERUN_CI_EXIT_OK;
925
+ }
926
+ if (outcome.kind === "refused" || outcome.kind === "stale") {
927
+ const payload = {
928
+ ok: false,
929
+ outcome: outcome.kind,
930
+ code: outcome.code,
931
+ message: outcome.message,
932
+ request_id: outcome.requestId,
933
+ nothing_written: true,
934
+ };
935
+ if (json) {
936
+ console.error(JSON.stringify(payload));
937
+ }
938
+ else {
939
+ console.error(`Refused: rerun-ci for ${label} — ${outcome.code}: ${outcome.message}. ` +
940
+ (outcome.kind === "stale"
941
+ ? "Nothing was written. The run changed since this command was rendered; re-read the run explanation."
942
+ : "Nothing was written."));
943
+ }
944
+ return RERUN_CI_EXIT_REFUSED;
945
+ }
946
+ if (outcome.kind === "unconfirmed") {
947
+ if (json) {
948
+ console.error(JSON.stringify({
949
+ ok: false,
950
+ outcome: "unconfirmed",
951
+ reason: outcome.reason,
952
+ message: outcome.message,
953
+ request_id: outcome.requestId,
954
+ }));
955
+ }
956
+ else {
957
+ console.error(`Unconfirmed: rerun-ci for ${label} — ${outcome.message}.`);
958
+ }
959
+ return RERUN_CI_EXIT_UNCONFIRMED;
960
+ }
961
+ if (json) {
962
+ console.error(JSON.stringify({ ok: false, outcome: outcome.kind, message: outcome.message }));
963
+ }
964
+ else {
965
+ console.error(`Failed: rerun-ci for ${label} — ${outcome.message}.`);
966
+ }
967
+ return RERUN_CI_EXIT_FAILED;
968
+ }
969
+ export async function runRerunCiCommand(argv) {
970
+ try {
971
+ const parsed = parseRerunCiArgs(argv);
972
+ if (parsed.help) {
973
+ console.log(RERUN_CI_USAGE);
974
+ return 0;
975
+ }
976
+ // Prove the token names THIS command, run, and ticket before credentials are
977
+ // resolved, so a mismatched binding reaches no one.
978
+ const binding = checkGuidedBinding(parsed.binding, "rerun-ci", {
979
+ epicRunId: parsed.epicRunId,
980
+ ticketKey: parsed.ticketKey,
981
+ });
982
+ const access = await resolveRecoveryAccess(parsed.json);
983
+ if (!access)
984
+ return RERUN_CI_EXIT_FAILED;
985
+ const { rerunCiRepair } = await import("./recovery-operations.js");
986
+ const outcome = await rerunCiRepair(access, {
987
+ epicRunId: parsed.epicRunId,
988
+ ticketKey: parsed.ticketKey,
989
+ binding,
990
+ });
991
+ return renderRerunCiOutcome(outcome, parsed, parsed.json);
992
+ }
993
+ catch (error) {
994
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
995
+ return RERUN_CI_EXIT_FAILED;
996
+ }
997
+ }
790
998
  const MERGE_CHILD_USAGE = [
791
999
  "Usage: epic-implementer merge-child --epic-run-id <id> --pr <number> [--json]",
792
1000
  "",
@@ -1128,11 +1336,11 @@ export async function runResumeRunCommand(argv) {
1128
1336
  // ---------------------------------------------------------------------------
1129
1337
  // continue-child / continuation-receipt (BAPI-1300)
1130
1338
  // ---------------------------------------------------------------------------
1131
- 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"]);
1132
1340
  const CONTINUATION_RECEIPT_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--request-id"]);
1133
1341
  const MAX_REASON_CHARS = 500;
1134
1342
  const CONTINUE_CHILD_USAGE = [
1135
- "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]",
1136
1344
  "",
1137
1345
  "Continue a parked child's CURRENT park episode without resetting it: the same",
1138
1346
  "attempt resumes on its retained work with only the allowance it has left (no",
@@ -1159,6 +1367,13 @@ function parseContinuationArgs(argv, valueFlags) {
1159
1367
  const epicRunId = requireFlag(values, "--epic-run-id");
1160
1368
  const ticketKey = requireFlag(values, "--ticket-key");
1161
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
+ }
1162
1377
  if (reason !== undefined && reason.length > MAX_REASON_CHARS) {
1163
1378
  throw new ConductorValidationError(`Flag "--reason" must be at most ${MAX_REASON_CHARS} characters.`);
1164
1379
  }
@@ -1170,6 +1385,7 @@ function parseContinuationArgs(argv, valueFlags) {
1170
1385
  epicRunId,
1171
1386
  ticketKey,
1172
1387
  ...(reason ? { reason } : {}),
1388
+ ...(workerExitProof ? { workerExitProof: workerExitProof } : {}),
1173
1389
  ...(requestId ? { requestId } : {}),
1174
1390
  json: bools.has("--json"),
1175
1391
  help: false,
@@ -1387,6 +1603,7 @@ export async function runContinueChildCommand(argv) {
1387
1603
  epicRunId: parsed.epicRunId,
1388
1604
  ticketKey: parsed.ticketKey,
1389
1605
  ...(parsed.reason ? { reason: parsed.reason } : {}),
1606
+ ...(parsed.workerExitProof ? { workerExitProof: parsed.workerExitProof } : {}),
1390
1607
  });
1391
1608
  return renderContinueChildResult(result, parsed, parsed.json);
1392
1609
  }
@@ -1395,6 +1612,60 @@ export async function runContinueChildCommand(argv) {
1395
1612
  return 1;
1396
1613
  }
1397
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
+ }
1398
1669
  export async function runContinuationReceiptCommand(argv) {
1399
1670
  try {
1400
1671
  const parsed = parseContinuationReceiptArgs(argv);
@@ -1653,3 +1924,30 @@ export async function runResumeCiCommand(argv) {
1653
1924
  return RESUME_CI_EXIT_FAILED;
1654
1925
  }
1655
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,12 +18,12 @@
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, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, resumeCiTicket, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
22
- import { clearGuidedReference, guardForBinding, GUIDED_PAYLOAD_CONFLICT_MESSAGE, GUIDED_REFERENCE_STORE_FAILED_MESSAGE, prepareGuidedRequest, } from "./guided-repair.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
+ 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";
25
25
  import { resolveLocalMergeCredential, resolveLocalMergeMethod, runApprovedLocalMerge, } from "./local-merge.js";
26
- import { RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
26
+ import { RERUN_CI_UNCONFIRMED_REASON, RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
27
27
  /**
28
28
  * Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
29
29
  * begins with a fresh, authoritative state read — so this bounds full
@@ -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) {
@@ -501,6 +502,72 @@ export async function guidedRepairOnce(options, deps = {}) {
501
502
  ? { kind: "committed", result, requestId, referenceReused }
502
503
  : { kind: "no_op", result, requestId, referenceReused };
503
504
  }
505
+ /** The bounded unconfirmed text for a `rerun-ci` whose release write was lost. */
506
+ export const RERUN_CI_COMPENSATION_UNCONFIRMED_MESSAGE = "the server could not confirm whether the workflow runs it did not enqueue are still " +
507
+ "recorded for this head. Re-read the run explanation before choosing another action";
508
+ /**
509
+ * Send ONE guided `rerun-ci`, pinned to its displayed binding, through the shared
510
+ * guided-repair reference (stored BEFORE the first POST; a lost response re-sends
511
+ * the SAME request). The server decides eligibility and every per-check status;
512
+ * this layer only classifies the answer.
513
+ */
514
+ export async function rerunCiRepair(access, options, deps = {}) {
515
+ const call = deps.rerunCi ?? rerunCi;
516
+ const outcome = await guidedRepairOnce({
517
+ binding: options.binding,
518
+ payloadDigest: guidedPayloadDigest({ ticket_key: options.ticketKey }),
519
+ send: (guard) => call(access, { epicRunId: options.epicRunId, ticketKey: options.ticketKey, guard }),
520
+ unconfirmedMessage: "the outcome could not be confirmed: the server may or may not have requested the " +
521
+ "re-runs. Re-run the SAME command to re-send the same pinned request, or inspect " +
522
+ "the run read-only",
523
+ }, deps);
524
+ switch (outcome.kind) {
525
+ case "committed":
526
+ case "no_op":
527
+ return {
528
+ kind: "applied",
529
+ result: outcome.result,
530
+ requestId: outcome.requestId,
531
+ referenceReused: outcome.referenceReused,
532
+ };
533
+ case "stale":
534
+ return {
535
+ kind: "stale",
536
+ code: outcome.code,
537
+ message: outcome.message,
538
+ requestId: outcome.requestId,
539
+ };
540
+ case "refused":
541
+ if (outcome.code === RERUN_CI_ROUTE_UNAVAILABLE_CODE) {
542
+ return { kind: "unsupported", message: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE };
543
+ }
544
+ if (outcome.code === "ROW_VERSION_CONFLICT" &&
545
+ outcome.message.includes(RERUN_CI_UNCONFIRMED_REASON)) {
546
+ return {
547
+ kind: "unconfirmed",
548
+ reason: "compensation_conflict",
549
+ message: RERUN_CI_COMPENSATION_UNCONFIRMED_MESSAGE,
550
+ requestId: outcome.requestId,
551
+ };
552
+ }
553
+ return {
554
+ kind: "refused",
555
+ code: outcome.code,
556
+ status: outcome.status,
557
+ message: outcome.message,
558
+ requestId: outcome.requestId,
559
+ };
560
+ case "unconfirmed":
561
+ return {
562
+ kind: "unconfirmed",
563
+ reason: "transport",
564
+ message: outcome.message,
565
+ requestId: outcome.requestId,
566
+ };
567
+ default:
568
+ return { kind: "unavailable", message: outcome.message };
569
+ }
570
+ }
504
571
  const AUTHORITATIVE_OUTCOMES = new Set([
505
572
  "committed",
506
573
  "already_continued",
@@ -631,6 +698,7 @@ export async function continueChildOnce(access, options, deps = {}) {
631
698
  requestId: ref.requestId,
632
699
  requestFingerprint: ref.requestFingerprint,
633
700
  reason: options.reason,
701
+ workerExitProof: options.workerExitProof,
634
702
  });
635
703
  if (outcome.kind === "result" && AUTHORITATIVE_OUTCOMES.has(outcome.result.outcome)) {
636
704
  await clear();
@@ -658,6 +726,102 @@ export async function continueChildOnce(access, options, deps = {}) {
658
726
  }
659
727
  return { kind: "unconfirmed", requestId: ref.requestId, reusedReference, transport };
660
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
+ }
661
825
  /**
662
826
  * Read a child's current episode and, when a local recovery reference exists (or
663
827
  * `requestId` is given), that request's receipt. One bounded GET; read-only. A