@bridge_gpt/mcp-server 0.2.68 → 0.2.70

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 (166) hide show
  1. package/README.md +226 -320
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +650 -98
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-review-workflow-drift.js +3 -3
  9. package/build/claude-settings.js +84 -2
  10. package/build/cli-help.js +7 -1
  11. package/build/codex-bridge-profile.js +1 -1
  12. package/build/codex-home.js +9 -1
  13. package/build/codex-launcher-pin.js +598 -0
  14. package/build/commands.generated.js +8 -8
  15. package/build/conductor/bridge-api-client.js +598 -19
  16. package/build/conductor/bring-up-facts.js +9 -9
  17. package/build/conductor/doctor.js +466 -18
  18. package/build/conductor/epic-child-continuation.js +54 -3
  19. package/build/conductor/epic-implementer-cli.js +132 -34
  20. package/build/conductor/epic-runtime.js +2 -2
  21. package/build/conductor/epic-state.js +1 -1
  22. package/build/conductor/hook-runtime-qualification.js +1 -1
  23. package/build/conductor/install-doctor.js +4 -2
  24. package/build/conductor/launch-need-probes.js +337 -0
  25. package/build/conductor/local-merge.js +241 -56
  26. package/build/conductor/merge-credential.js +172 -0
  27. package/build/conductor/merge-execution-eligibility.js +1 -1
  28. package/build/conductor/merged-child-check.js +95 -0
  29. package/build/conductor/plan-trim.js +30 -0
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +11 -6
  32. package/build/conductor/readiness-prerequisites.js +41 -0
  33. package/build/conductor/readiness-sections.js +345 -55
  34. package/build/conductor/readiness.js +99 -87
  35. package/build/conductor/recovery-cli.js +606 -21
  36. package/build/conductor/recovery-operations.js +306 -7
  37. package/build/conductor/repair-contract.js +47 -2
  38. package/build/conductor/repository-workflow-remedy.js +8 -6
  39. package/build/conductor/stored-merge-credential.js +76 -0
  40. package/build/conductor/stored-merge-token-probe.js +28 -0
  41. package/build/conductor/temporary-plan.js +50 -0
  42. package/build/conductor/wait-run-cli.js +759 -0
  43. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  44. package/build/conductor-bin.js +2 -2
  45. package/build/conductor-claude-hook-bin.js +2 -2
  46. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  47. package/build/conductor-removed-stub-bin.js +2 -5
  48. package/build/connect-github-api.js +33 -14
  49. package/build/credential-store.js +356 -13
  50. package/build/docs.generated.js +27 -14
  51. package/build/doctor.js +203 -38
  52. package/build/drive-epic-help.js +212 -0
  53. package/build/drive-epic.js +1840 -373
  54. package/build/epic-ci-workflow-drift.js +77 -0
  55. package/build/epic-ci-workflow-install.js +183 -0
  56. package/build/epic-ci-workflow.js +157 -0
  57. package/build/epic-contract-context.js +326 -0
  58. package/build/epic-implementer-bin.js +131 -120
  59. package/build/epic-implementer-claude-hook-bin.js +2 -2
  60. package/build/escalation-reachability.js +134 -0
  61. package/build/executor/cleanup-report.js +99 -0
  62. package/build/executor/cli.js +12 -1
  63. package/build/executor/completion-contract.js +68 -11
  64. package/build/executor/completion-supervision.js +99 -0
  65. package/build/executor/delivery-outcome.js +179 -0
  66. package/build/executor/deps.js +3 -0
  67. package/build/executor/env.js +71 -0
  68. package/build/executor/failure-reason-codes.js +126 -0
  69. package/build/executor/heartbeat.js +38 -0
  70. package/build/executor/http-client.js +115 -4
  71. package/build/executor/integration-job.js +298 -5
  72. package/build/executor/job-errors.js +36 -6
  73. package/build/executor/job-runner.js +720 -36
  74. package/build/executor/likely-tests.js +36 -2
  75. package/build/executor/merge-job.js +145 -2
  76. package/build/executor/observation.js +73 -4
  77. package/build/executor/permissions.js +193 -97
  78. package/build/executor/preflight.js +14 -3
  79. package/build/executor/process-containment.js +28 -2
  80. package/build/executor/prompt-spec.js +50 -5
  81. package/build/executor/rebase-finalization.js +38 -3
  82. package/build/executor/reconcile-overlap-job.js +35 -7
  83. package/build/executor/required-work-ledger.js +236 -81
  84. package/build/executor/resume-pre-spawn.js +38 -1
  85. package/build/executor/runner.js +91 -3
  86. package/build/executor/test-clock.js +8 -0
  87. package/build/executor/types.js +37 -2
  88. package/build/executor/worker-cli-launcher.js +259 -0
  89. package/build/executor/worker-command.js +41 -4
  90. package/build/executor/worker-database-target.js +91 -0
  91. package/build/executor/worker-database.js +908 -0
  92. package/build/executor/worker-finalization.js +106 -0
  93. package/build/executor/worker-guard-hook.js +203 -0
  94. package/build/executor/worker-resources.js +47 -0
  95. package/build/executor/worker-verification-guidance.js +15 -2
  96. package/build/executor/worktree.js +303 -15
  97. package/build/executor-launch.js +11 -1
  98. package/build/generated-assets-doctor.js +9 -1
  99. package/build/generated-assets-health.js +13 -4
  100. package/build/generated-assets-legacy.js +39 -0
  101. package/build/generated-assets-paths.js +12 -1
  102. package/build/generated-assets-repair.js +4 -1
  103. package/build/generated-assets-test-support.js +12 -0
  104. package/build/github-remote.js +33 -0
  105. package/build/github-sign-in.js +62 -0
  106. package/build/index.js +1214 -465
  107. package/build/init.js +39 -1
  108. package/build/install-bridge-conductor-gate.js +78 -0
  109. package/build/install-bridge-conductor-setup.js +413 -0
  110. package/build/install-bridge-conductor.js +1226 -267
  111. package/build/install-bridge.js +629 -350
  112. package/build/launcher-config-inspection.js +184 -19
  113. package/build/mcp-host-targets.js +4 -0
  114. package/build/mcp-identity.js +7 -0
  115. package/build/mcp-install-state.js +8 -2
  116. package/build/merge-pull-request.js +6 -0
  117. package/build/merge-workflow-install.js +763 -345
  118. package/build/phase-result-artifacts.js +75 -20
  119. package/build/pipeline-orchestrator.js +247 -4
  120. package/build/pipelines.generated.js +35 -35
  121. package/build/plan-discovery.js +15 -2
  122. package/build/plan-execution-ledger.js +306 -17
  123. package/build/plan-phase-routing.js +48 -13
  124. package/build/plane/cli.js +105 -51
  125. package/build/plane/defaults.js +3 -0
  126. package/build/plane/plane-freshness.js +294 -0
  127. package/build/plane/schema-compatibility.js +137 -0
  128. package/build/plane/shutdown-deps.js +38 -0
  129. package/build/plane/shutdown.js +83 -8
  130. package/build/plane/terminal-run-cleanup.js +303 -0
  131. package/build/printed-remedy-commands.js +26 -4
  132. package/build/readiness-check.js +6 -2
  133. package/build/readiness-prerequisites.generated.js +532 -0
  134. package/build/readme.generated.js +1 -1
  135. package/build/session-upgrade-notice.js +86 -0
  136. package/build/setup-epic.js +400 -30
  137. package/build/shared-resources.generated.js +5 -0
  138. package/build/start-tickets.js +43 -15
  139. package/build/test-command-detection.js +247 -0
  140. package/build/test-command-validation.js +73 -0
  141. package/build/testing-config-client.js +150 -0
  142. package/build/ticket-delivery-artifacts.js +258 -0
  143. package/build/ticket-delivery-enrollment.js +306 -0
  144. package/build/ticket-delivery.js +205 -0
  145. package/build/ticket-review-summary.js +14 -0
  146. package/build/tool-call-advisory.js +162 -0
  147. package/build/tool-surface-gating.js +28 -89
  148. package/build/update-check.js +97 -16
  149. package/build/update-status.js +61 -71
  150. package/build/upgrade-advice.js +15 -11
  151. package/build/upgrade-cli.js +360 -20
  152. package/build/v2-readiness-requirements.js +552 -0
  153. package/build/version.generated.js +3 -3
  154. package/build/worker-guard-hook-bin.js +2 -2
  155. package/build/worktree-core.js +22 -7
  156. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  157. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  158. package/docs/EPIC-IMPLEMENTER.md +104 -63
  159. package/docs/epic-implementer/getting-started.md +131 -0
  160. package/docs/epic-implementer/self-hosting.md +133 -0
  161. package/package.json +3 -3
  162. package/pipelines/idea-to-ticket.json +1 -1
  163. package/pipelines/implement-ticket.json +8 -8
  164. package/pipelines/review-ticket.json +2 -2
  165. package/design-assets/favicon/favicon-32x32.png +0 -0
  166. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `conductor` operator recovery and repair verbs — fourteen CLI-only commands.
2
+ * `conductor` operator recovery and repair verbs — sixteen 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
@@ -7,10 +7,12 @@
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
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
+ * provider re-run of failed CI workflow runs: `rerun-ci`. BAPI-1494's owner
11
+ * decision on verification hand-offs: `resolve-handoff`. BAPI-1532's answer to a
12
+ * named plan failure, one forced regeneration on a chosen route: `regenerate-plan`.
11
13
  *
12
14
  * CLI-only by design — this is an explicit ticket non-goal, not an oversight:
13
- * none of these fourteen command names is ever registered as an MCP tool, tool
15
+ * none of these sixteen command names is ever registered as an MCP tool, tool
14
16
  * description, or generated tool-schema input (`index-static.test.ts` guards
15
17
  * this). They exist so an operator (or `plane down`, via
16
18
  * `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
@@ -26,7 +28,9 @@
26
28
  */
27
29
  import { readFile } from "node:fs/promises";
28
30
  import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
29
- import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES, RESUME_RUN_UNCONFIRMED_MESSAGE, } from "./repair-contract.js";
31
+ import { nodeGithubMergeCredentialReadDeps, readLocalGithubRemote } from "./stored-merge-credential.js";
32
+ import { DRIVER_CONTEXT_MAX_BYTES, HANDOFF_RESOLUTION_EVIDENCE_REF_MAX_CHARS, HANDOFF_RESOLUTION_KIND_LABELS, HANDOFF_RESOLUTION_KINDS, HANDOFF_RESOLUTION_MAX_STEPS, HANDOFF_RESOLUTION_REASON_MAX_CHARS, POLICY_PATCH_MAX_BYTES, RESUME_RUN_UNCONFIRMED_MESSAGE, } from "./repair-contract.js";
33
+ import { CONTINUATION_CI_WAITING_REASONS, continuationDispatchedJob, } from "./epic-child-continuation.js";
30
34
  import { decodeGuidedBinding, GuidedBindingError, } from "./guided-repair.js";
31
35
  // ---------------------------------------------------------------------------
32
36
  // Flag parsing
@@ -995,6 +999,196 @@ export async function runRerunCiCommand(argv) {
995
999
  return RERUN_CI_EXIT_FAILED;
996
1000
  }
997
1001
  }
1002
+ // ---------------------------------------------------------------------------
1003
+ // BAPI-1494 — resolve-handoff
1004
+ // ---------------------------------------------------------------------------
1005
+ const RESOLVE_HANDOFF_VALUE_FLAGS = new Set([
1006
+ "--epic-run-id",
1007
+ "--ticket-key",
1008
+ "--steps",
1009
+ "--kind",
1010
+ "--evidence",
1011
+ "--reason",
1012
+ ]);
1013
+ /** Exit codes: 0 recorded (or an identical replay), 1 refused or failed. */
1014
+ export const RESOLVE_HANDOFF_EXIT_OK = 0;
1015
+ export const RESOLVE_HANDOFF_EXIT_FAILED = 1;
1016
+ /** The paired-record instruction every success ends with. */
1017
+ export const RESOLVE_HANDOFF_PAIRED_RECORD_INSTRUCTION = "Release from draft also needs the paired .bridge/epic-workarounds/<KEY>.md record " +
1018
+ "removed in a new epic-head commit. The integration PR leaves draft only when both " +
1019
+ "holds are clear: the hand-off ledger hold and the workaround-folder hold.";
1020
+ const RESOLVE_HANDOFF_USAGE = [
1021
+ "Usage: epic-implementer resolve-handoff --epic-run-id <id> --ticket-key <key> --steps 27,36",
1022
+ " --kind verified|risk_accepted --evidence <ref> --reason <text> [--json]",
1023
+ "",
1024
+ "Record the run owner's decision on a ticket's verification hand-offs: steps a worker",
1025
+ "could not verify unattended. An unresolved hand-off holds the integration PR in draft.",
1026
+ "",
1027
+ " --kind verified the owner completed the verification of the affected criteria",
1028
+ " --kind risk_accepted verification is still incomplete; the owner accepts the risk",
1029
+ "",
1030
+ "This records a HUMAN decision. Run it only with the owner's named steps, kind, evidence",
1031
+ "and reason; it is never an automatic in-run recovery, and workers cannot run it. The",
1032
+ "server records your verified API-key identity as the actor; you cannot supply one.",
1033
+ "--evidence is stored as a reference (at most 500 characters) and is never fetched.",
1034
+ "--reason is at most 2000 characters.",
1035
+ "",
1036
+ RESOLVE_HANDOFF_PAIRED_RECORD_INSTRUCTION,
1037
+ "",
1038
+ "Exit codes: 0 recorded (or the same decision already recorded); 1 refused or failed.",
1039
+ ].join("\n");
1040
+ /** Character count in code points, the way the server's validation counts. */
1041
+ function codePointLength(value) {
1042
+ return [...value].length;
1043
+ }
1044
+ function parseHandoffSteps(raw) {
1045
+ const parts = raw.split(",");
1046
+ const steps = [];
1047
+ for (const part of parts) {
1048
+ const token = part.trim();
1049
+ if (!/^[1-9][0-9]*$/.test(token)) {
1050
+ throw new ConductorValidationError("--steps must be a comma-separated list of positive integers (for example 27,36).");
1051
+ }
1052
+ const step = Number(token);
1053
+ if (!Number.isSafeInteger(step)) {
1054
+ throw new ConductorValidationError("--steps holds a step id that is too large.");
1055
+ }
1056
+ if (steps.includes(step)) {
1057
+ throw new ConductorValidationError(`--steps names step ${step} more than once.`);
1058
+ }
1059
+ steps.push(step);
1060
+ }
1061
+ if (steps.length > HANDOFF_RESOLUTION_MAX_STEPS) {
1062
+ throw new ConductorValidationError(`--steps may name at most ${HANDOFF_RESOLUTION_MAX_STEPS} steps.`);
1063
+ }
1064
+ return steps;
1065
+ }
1066
+ function boundedText(values, flag, max) {
1067
+ const value = requireFlag(values, flag);
1068
+ if (codePointLength(value) > max) {
1069
+ throw new ConductorValidationError(`Flag "${flag}" must be at most ${max} characters.`);
1070
+ }
1071
+ return value;
1072
+ }
1073
+ export function parseResolveHandoffArgs(argv) {
1074
+ const { values, bools } = tokenizeRecoveryFlags(argv, RESOLVE_HANDOFF_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
1075
+ if (bools.has("--help")) {
1076
+ return {
1077
+ epicRunId: "",
1078
+ ticketKey: "",
1079
+ steps: [],
1080
+ kind: "verified",
1081
+ evidenceRef: "",
1082
+ reason: "",
1083
+ json: bools.has("--json"),
1084
+ help: true,
1085
+ };
1086
+ }
1087
+ const kind = requireFlag(values, "--kind");
1088
+ if (!HANDOFF_RESOLUTION_KINDS.includes(kind)) {
1089
+ throw new ConductorValidationError('--kind must be "verified" or "risk_accepted".');
1090
+ }
1091
+ return {
1092
+ epicRunId: requireFlag(values, "--epic-run-id"),
1093
+ ticketKey: requireFlag(values, "--ticket-key"),
1094
+ steps: parseHandoffSteps(requireFlag(values, "--steps")),
1095
+ kind: kind,
1096
+ evidenceRef: boundedText(values, "--evidence", HANDOFF_RESOLUTION_EVIDENCE_REF_MAX_CHARS),
1097
+ reason: boundedText(values, "--reason", HANDOFF_RESOLUTION_REASON_MAX_CHARS),
1098
+ json: bools.has("--json"),
1099
+ help: false,
1100
+ };
1101
+ }
1102
+ /**
1103
+ * Exported for tests: render one `resolve-handoff` outcome. Success goes to stdout
1104
+ * and every failure to stderr (`--json` included). `risk_accepted` always reads
1105
+ * "risk accepted" — never verified or passed.
1106
+ */
1107
+ export function renderResolveHandoffOutcome(outcome, parsed, json) {
1108
+ const label = `ticket ${parsed.ticketKey} (run ${parsed.epicRunId})`;
1109
+ const pairedRecord = RESOLVE_HANDOFF_PAIRED_RECORD_INSTRUCTION.replace("<KEY>", parsed.ticketKey);
1110
+ if (outcome.ok) {
1111
+ const result = outcome.result;
1112
+ const decision = HANDOFF_RESOLUTION_KIND_LABELS[parsed.kind];
1113
+ if (result.committed && !result.event_recorded) {
1114
+ console.error(`Warning: resolve-handoff for ${label} COMMITTED, but its operator_verb_applied ` +
1115
+ "audit event was not recorded. The decision stands.");
1116
+ }
1117
+ if (json) {
1118
+ console.log(JSON.stringify({
1119
+ ok: true,
1120
+ outcome: result.committed ? "committed" : "replayed",
1121
+ decision,
1122
+ result,
1123
+ next_action: pairedRecord,
1124
+ }));
1125
+ return RESOLVE_HANDOFF_EXIT_OK;
1126
+ }
1127
+ const lines = [
1128
+ result.committed
1129
+ ? `resolve-handoff — ${label}: decision recorded (${decision})`
1130
+ : `resolve-handoff — ${label}: this decision was already recorded; nothing was written`,
1131
+ ...result.resolutions.map((res) => ` steps ${res.steps.join(", ")}: ${HANDOFF_RESOLUTION_KIND_LABELS[res.kind]} by ` +
1132
+ `${res.actor} at epic head \`${res.epic_head_sha}\` (binding ${res.binding_attempt_id}, ` +
1133
+ `evidence revision ${res.evidence_revision}); evidence: ${res.evidence_ref}`),
1134
+ ` unresolved hand-offs left on this ticket: ${result.remaining_unresolved}`,
1135
+ ` event recorded: ${result.committed ? String(result.event_recorded) : "not applicable (replay)"}`,
1136
+ ` next action: ${pairedRecord}`,
1137
+ ];
1138
+ console.log(lines.join("\n"));
1139
+ return RESOLVE_HANDOFF_EXIT_OK;
1140
+ }
1141
+ if (outcome.kind === "refused") {
1142
+ if (json) {
1143
+ console.error(JSON.stringify({
1144
+ ok: false,
1145
+ outcome: "refused",
1146
+ code: outcome.code,
1147
+ message: outcome.message,
1148
+ nothing_written: true,
1149
+ }));
1150
+ }
1151
+ else {
1152
+ console.error(`Refused: resolve-handoff for ${label} — ${outcome.code}: ${outcome.message} ` +
1153
+ "Nothing was written.");
1154
+ }
1155
+ return RESOLVE_HANDOFF_EXIT_FAILED;
1156
+ }
1157
+ if (json) {
1158
+ console.error(JSON.stringify({ ok: false, outcome: "unavailable", message: outcome.message }));
1159
+ }
1160
+ else {
1161
+ console.error(`Failed: resolve-handoff for ${label} — ${outcome.message}. Whether the decision was ` +
1162
+ "recorded is unconfirmed; read `epic-implementer explain-run` before retrying.");
1163
+ }
1164
+ return RESOLVE_HANDOFF_EXIT_FAILED;
1165
+ }
1166
+ export async function runResolveHandoffCommand(argv) {
1167
+ try {
1168
+ const parsed = parseResolveHandoffArgs(argv);
1169
+ if (parsed.help) {
1170
+ console.log(RESOLVE_HANDOFF_USAGE);
1171
+ return 0;
1172
+ }
1173
+ const access = await resolveRecoveryAccess(parsed.json);
1174
+ if (!access)
1175
+ return RESOLVE_HANDOFF_EXIT_FAILED;
1176
+ const { resolveHandoffRepair } = await import("./recovery-operations.js");
1177
+ const outcome = await resolveHandoffRepair(access, {
1178
+ epicRunId: parsed.epicRunId,
1179
+ ticketKey: parsed.ticketKey,
1180
+ steps: parsed.steps,
1181
+ kind: parsed.kind,
1182
+ evidenceRef: parsed.evidenceRef,
1183
+ reason: parsed.reason,
1184
+ });
1185
+ return renderResolveHandoffOutcome(outcome, parsed, parsed.json);
1186
+ }
1187
+ catch (error) {
1188
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
1189
+ return RESOLVE_HANDOFF_EXIT_FAILED;
1190
+ }
1191
+ }
998
1192
  const MERGE_CHILD_USAGE = [
999
1193
  "Usage: epic-implementer merge-child --epic-run-id <id> --pr <number> [--json]",
1000
1194
  "",
@@ -1020,9 +1214,14 @@ export async function runMergeChildCommand(argv) {
1020
1214
  if (!access)
1021
1215
  return 1;
1022
1216
  const { mergeChildRepair } = await import("./recovery-operations.js");
1023
- const outcome = await mergeChildRepair(access, {
1024
- epicRunId: parsed.epicRunId,
1025
- prNumber: parsed.prNumber,
1217
+ // BAPI-1461: this verb runs in the person's own session, so a merge token
1218
+ // stored once for this repository is the last rung after the env keys.
1219
+ const outcome = await mergeChildRepair(access, { epicRunId: parsed.epicRunId, prNumber: parsed.prNumber }, {
1220
+ storedMergeCredential: {
1221
+ allowStoredEntry: true,
1222
+ localRemote: await readLocalGithubRemote(process.cwd()),
1223
+ storeDeps: nodeGithubMergeCredentialReadDeps(process.env),
1224
+ },
1026
1225
  });
1027
1226
  return renderRepairResult(outcome, {
1028
1227
  verb: "merge-child",
@@ -1336,11 +1535,17 @@ export async function runResumeRunCommand(argv) {
1336
1535
  // ---------------------------------------------------------------------------
1337
1536
  // continue-child / continuation-receipt (BAPI-1300)
1338
1537
  // ---------------------------------------------------------------------------
1339
- const CONTINUE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--reason", "--worker-exit-proof"]);
1538
+ const CONTINUE_CHILD_VALUE_FLAGS = new Set([
1539
+ "--epic-run-id",
1540
+ "--ticket-key",
1541
+ "--reason",
1542
+ "--worker-exit-proof",
1543
+ "--context-file",
1544
+ ]);
1340
1545
  const CONTINUATION_RECEIPT_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--request-id"]);
1341
1546
  const MAX_REASON_CHARS = 500;
1342
1547
  const CONTINUE_CHILD_USAGE = [
1343
- "Usage: epic-implementer continue-child --epic-run-id <id> --ticket-key <key> [--reason <text> --worker-exit-proof exited|fenced] [--json]",
1548
+ "Usage: epic-implementer continue-child --epic-run-id <id> --ticket-key <key> [--reason <text> --worker-exit-proof exited|fenced] [--context-file <path>] [--json]",
1344
1549
  "",
1345
1550
  "Continue a parked child's CURRENT park episode without resetting it: the same",
1346
1551
  "attempt resumes on its retained work with only the allowance it has left (no",
@@ -1351,6 +1556,18 @@ const CONTINUE_CHILD_USAGE = [
1351
1556
  "committed. Re-running continue-child reuses the SAME request (it never",
1352
1557
  "approves a new head or episode); continuation-receipt looks the receipt up.",
1353
1558
  "Never accepts a version counter or a park generation.",
1559
+ "",
1560
+ "--context-file (CI-repair continuations only): driver context for the repair",
1561
+ "worker, sent beside --reason (never merged into it). It must be non-empty UTF-8",
1562
+ `text of at most DRIVER_CONTEXT_MAX_BYTES (${DRIVER_CONTEXT_MAX_BYTES} bytes); it is`,
1563
+ "rejected, never truncated, and is never printed, logged, or echoed. A re-run of",
1564
+ "a pending request must pass the same file. A server that does not support it",
1565
+ "rejects the request (exit 1); it is never retried without the context.",
1566
+ "",
1567
+ "When CI is already green at the bound head, the server resumes the child",
1568
+ "without a repair: no job is dispatched and no allowance is charged (exit 0).",
1569
+ "While CI is still running or unreadable, nothing is written; re-run the same",
1570
+ "command later — it re-sends the SAME request.",
1354
1571
  ].join("\n");
1355
1572
  const CONTINUATION_RECEIPT_USAGE = [
1356
1573
  "Usage: epic-implementer continuation-receipt --epic-run-id <id> --ticket-key <key> [--request-id <id>] [--json]",
@@ -1381,12 +1598,14 @@ function parseContinuationArgs(argv, valueFlags) {
1381
1598
  if (requestId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/.test(requestId)) {
1382
1599
  throw new ConductorValidationError('Flag "--request-id" is not a valid request id.');
1383
1600
  }
1601
+ const contextFile = values.has("--context-file") ? requireFlag(values, "--context-file") : undefined;
1384
1602
  return {
1385
1603
  epicRunId,
1386
1604
  ticketKey,
1387
1605
  ...(reason ? { reason } : {}),
1388
1606
  ...(workerExitProof ? { workerExitProof: workerExitProof } : {}),
1389
1607
  ...(requestId ? { requestId } : {}),
1608
+ ...(contextFile !== undefined ? { contextFile } : {}),
1390
1609
  json: bools.has("--json"),
1391
1610
  help: false,
1392
1611
  };
@@ -1441,12 +1660,40 @@ function currentLine(current) {
1441
1660
  return (`Current: parked at gate ${current.parked_gate} (episode ${current.park_generation}${age}); ` +
1442
1661
  `${available}.`);
1443
1662
  }
1663
+ // BAPI-1514 — presentation of the CI-aware continuation refusals (closed codes).
1664
+ const CI_GREEN_ADOPTED_REASON = "ci_green_adopted";
1665
+ const CI_GREEN_ADOPTED_TEXT = "already resolved — resumed without repair; no repair job was dispatched and no " +
1666
+ "continuation allowance was charged";
1667
+ const CI_WAITING_TEXT = {
1668
+ ci_pending_at_bound_head: "CI is still running at the bound head.",
1669
+ ci_provider_read_unavailable: "the CI read failed or timed out.",
1670
+ ci_observation_stale: "the pull-request head moved while CI was being read.",
1671
+ };
1672
+ const CI_WAITING_WHEN = {
1673
+ ci_pending_at_bound_head: " after CI finishes",
1674
+ ci_provider_read_unavailable: " after the CI provider recovers",
1675
+ ci_observation_stale: " now (the server re-reads CI)",
1676
+ };
1677
+ const CI_ROUTE_GUIDANCE = {
1678
+ ci_route_conflict: "The pull request is conflicting, so a CI repair is the wrong exit. Resolve the conflict " +
1679
+ "instead (rebase / overlap reconciliation via dispatch-remediation). No repair was dispatched.",
1680
+ ci_route_rerun: "This is an infrastructure CI failure; the server's infrastructure re-run path owns it " +
1681
+ "(rerun-ci). No repair was dispatched.",
1682
+ ci_route_hold: "CI routing is holding; no widened repair was dispatched. Re-read the run state " +
1683
+ "(explain-run) before choosing another action.",
1684
+ driver_context_not_supported: "--context-file applies only to a CI-repair (ci_fix) continuation. Drop --context-file " +
1685
+ "and re-run with --reason only. Nothing was written.",
1686
+ };
1444
1687
  /** Exported for tests: render a `continue-child` result. Returns the exit code. */
1445
1688
  export function renderContinueChildResult(result, target, json) {
1446
1689
  const label = `Ticket ${target.ticketKey} (run ${target.epicRunId})`;
1447
1690
  if (result.kind === "decided") {
1448
1691
  const r = result.result;
1449
- const ok = r.outcome === "committed" || r.outcome === "already_continued";
1692
+ const greenAdopted = r.outcome === "refused" && r.reason_code === CI_GREEN_ADOPTED_REASON;
1693
+ const ok = r.outcome === "committed" || r.outcome === "already_continued" || greenAdopted;
1694
+ // BAPI-1514 — only a committed receipt for THIS request is a real dispatch,
1695
+ // and only that may count against an intervention budget or allowance.
1696
+ const dispatched = continuationDispatchedJob(r);
1450
1697
  if (json) {
1451
1698
  console.log(JSON.stringify({
1452
1699
  ok,
@@ -1457,6 +1704,9 @@ export function renderContinueChildResult(result, target, json) {
1457
1704
  replayed: r.replayed,
1458
1705
  retryable: r.retryable,
1459
1706
  reference_reused: result.reusedReference,
1707
+ dispatched,
1708
+ allowance_charged: dispatched,
1709
+ ...(greenAdopted ? { resolved_without_repair: true } : {}),
1460
1710
  receipt: r.receipt === null ? null : receiptSummary(r.receipt),
1461
1711
  current: currentSummary(r.current),
1462
1712
  }));
@@ -1486,7 +1736,27 @@ export function renderContinueChildResult(result, target, json) {
1486
1736
  console.error(`${label}: continuation UNSUPPORTED (${r.reason_code}) — ${r.explanation}`);
1487
1737
  return 1;
1488
1738
  default:
1739
+ if (greenAdopted) {
1740
+ console.log(`${label}: ${CI_GREEN_ADOPTED_TEXT}.`);
1741
+ if (r.current)
1742
+ console.log(currentLine(r.current));
1743
+ return 0;
1744
+ }
1745
+ if (r.reason_code !== null && CONTINUATION_CI_WAITING_REASONS.has(r.reason_code)) {
1746
+ console.error(`${label}: WAITING (${r.reason_code}) — ${CI_WAITING_TEXT[r.reason_code] ?? r.explanation}`);
1747
+ console.error(`Nothing was written: no repair job was dispatched and no continuation allowance was charged. ` +
1748
+ `Re-run the same continue-child command${CI_WAITING_WHEN[r.reason_code] ?? ""}; it re-sends ` +
1749
+ `the SAME request (${result.requestId}).`);
1750
+ if (r.current?.expected_head_sha) {
1751
+ console.error(`Bound head: ${r.current.expected_head_sha}.`);
1752
+ }
1753
+ return 1;
1754
+ }
1489
1755
  console.error(`${label}: continuation REFUSED (${r.reason_code}) — ${r.explanation}`);
1756
+ if (r.reason_code !== null && CI_ROUTE_GUIDANCE[r.reason_code] !== undefined) {
1757
+ console.error(CI_ROUTE_GUIDANCE[r.reason_code]);
1758
+ return 1;
1759
+ }
1490
1760
  if (r.retryable)
1491
1761
  console.error("This refusal is retryable; nothing was written.");
1492
1762
  return 1;
@@ -1505,6 +1775,11 @@ export function renderContinueChildResult(result, target, json) {
1505
1775
  }
1506
1776
  console.error(`${label}: request REJECTED before anything was written (status ${result.status}` +
1507
1777
  `${result.errorCode ? `, ${result.errorCode}` : ""}).`);
1778
+ if (result.driverContextSent && result.status === 422) {
1779
+ console.error("The request carried --context-file driver context. This server may not support it yet; " +
1780
+ "the request was NOT retried without it. Upgrade the server, or drop --context-file " +
1781
+ "deliberately and re-run.");
1782
+ }
1508
1783
  return 1;
1509
1784
  }
1510
1785
  if (result.kind === "not_continuable") {
@@ -1588,13 +1863,26 @@ export function renderContinuationReceiptResult(result, target, json) {
1588
1863
  console.log(currentLine(status.current));
1589
1864
  return 0;
1590
1865
  }
1591
- export async function runContinueChildCommand(argv) {
1866
+ export async function runContinueChildCommand(argv, deps = {}) {
1592
1867
  try {
1593
1868
  const parsed = parseContinueChildArgs(argv);
1594
1869
  if (parsed.help) {
1595
1870
  console.log(CONTINUE_CHILD_USAGE);
1596
1871
  return 0;
1597
1872
  }
1873
+ // BAPI-1514 — read and validate the context BEFORE credentials are resolved,
1874
+ // exactly as dispatch-remediation does, so an invalid file reaches no one.
1875
+ let driverContext;
1876
+ if (parsed.contextFile !== undefined) {
1877
+ let bytes;
1878
+ try {
1879
+ bytes = await (deps.readFile ?? ((p) => readFile(p)))(parsed.contextFile);
1880
+ }
1881
+ catch {
1882
+ throw new ConductorValidationError("--context-file could not be read; nothing was sent.");
1883
+ }
1884
+ driverContext = decodeDriverContextFile(bytes);
1885
+ }
1598
1886
  const access = await resolveRecoveryAccess(parsed.json);
1599
1887
  if (!access)
1600
1888
  return 1;
@@ -1604,6 +1892,7 @@ export async function runContinueChildCommand(argv) {
1604
1892
  ticketKey: parsed.ticketKey,
1605
1893
  ...(parsed.reason ? { reason: parsed.reason } : {}),
1606
1894
  ...(parsed.workerExitProof ? { workerExitProof: parsed.workerExitProof } : {}),
1895
+ ...(driverContext !== undefined ? { driverContext } : {}),
1607
1896
  });
1608
1897
  return renderContinueChildResult(result, parsed, parsed.json);
1609
1898
  }
@@ -1651,8 +1940,18 @@ export async function runGrantContinuationCommand(argv) {
1651
1940
  return 1;
1652
1941
  }
1653
1942
  const { requestId, outcome: result } = operation;
1654
- if (json)
1655
- console.log(JSON.stringify({ ...result, request_id: requestId }));
1943
+ if (json) {
1944
+ console.log(JSON.stringify(result.kind === "result"
1945
+ ? {
1946
+ kind: result.kind,
1947
+ committed: result.committed,
1948
+ replayed: result.replayed,
1949
+ request_id: requestId,
1950
+ source_park_generation: result.sourceParkGeneration,
1951
+ gate: result.gate,
1952
+ }
1953
+ : { ...result, request_id: requestId }));
1954
+ }
1656
1955
  else if (result.kind === "result")
1657
1956
  console.log(`Grant ${result.replayed ? "replayed" : "committed"}: ${requestId}. The child remains parked.`);
1658
1957
  else if (result.kind === "unconfirmed")
@@ -1924,8 +2223,280 @@ export async function runResumeCiCommand(argv) {
1924
2223
  return RESUME_CI_EXIT_FAILED;
1925
2224
  }
1926
2225
  }
2226
+ // ---------------------------------------------------------------------------
2227
+ // BAPI-1532 — regenerate-plan
2228
+ // ---------------------------------------------------------------------------
2229
+ const REGENERATE_PLAN_USAGE = [
2230
+ "Usage: epic-implementer regenerate-plan --epic-run-id <id> --ticket-key <key>",
2231
+ " --route <test|implementation> --reason <text> [--json]",
2232
+ "",
2233
+ "Answer a child parked on a NAMED plan failure with ONE forced plan regeneration on",
2234
+ "the route you choose. The server checks the failure reason's finite allowance, then",
2235
+ "commits the route override, the gate reset, and its audit event together.",
2236
+ "",
2237
+ "Success means the override and reset COMMITTED. It does not mean a usable plan",
2238
+ "exists: request the plan and poll get_plan until it shows plan_metadata_status: ok.",
2239
+ "",
2240
+ "The operation is pinned locally before it is sent; re-running after a lost response",
2241
+ "reuses it and reports the original result. Nothing else is tried.",
2242
+ "",
2243
+ "Exit codes: 0 committed or replayed; 2 refused or stale; 3 unconfirmed; 1 other failure.",
2244
+ ].join("\n");
2245
+ export const REGENERATE_PLAN_EXIT_OK = 0;
2246
+ export const REGENERATE_PLAN_EXIT_FAILED = 1;
2247
+ export const REGENERATE_PLAN_EXIT_REFUSED = 2;
2248
+ export const REGENERATE_PLAN_EXIT_UNCONFIRMED = 3;
2249
+ /** The fixed text a server that lacks the route is reported under. */
2250
+ export const REGENERATE_PLAN_SERVER_TOO_OLD = "server too old";
2251
+ export function parseRegeneratePlanArgs(argv) {
2252
+ const { values, bools } = tokenizeRecoveryFlags(argv, new Set([...TICKET_VALUE_FLAGS, "--route", "--reason"]), RECOVERY_BOOL_FLAGS);
2253
+ if (bools.has("--help")) {
2254
+ return { epicRunId: "", ticketKey: "", route: "test", json: bools.has("--json"), help: true };
2255
+ }
2256
+ const epicRunId = values.get("--epic-run-id");
2257
+ if (epicRunId === undefined || epicRunId.trim().length === 0) {
2258
+ throw new ConductorValidationError('Flag "--epic-run-id" is required and must be non-empty.');
2259
+ }
2260
+ const ticketKey = values.get("--ticket-key");
2261
+ if (ticketKey === undefined || ticketKey.trim().length === 0) {
2262
+ throw new ConductorValidationError('Flag "--ticket-key" is required and must be non-empty.');
2263
+ }
2264
+ const route = values.get("--route")?.trim();
2265
+ if (route !== "test" && route !== "implementation") {
2266
+ throw new ConductorValidationError('Flag "--route" is required and must be "test" or "implementation".');
2267
+ }
2268
+ const reason = values.get("--reason")?.trim();
2269
+ if (reason === undefined || reason.length === 0) {
2270
+ throw new ConductorValidationError('Flag "--reason" is required and must be non-empty.');
2271
+ }
2272
+ return {
2273
+ epicRunId: epicRunId.trim(),
2274
+ ticketKey: ticketKey.trim(),
2275
+ route,
2276
+ reason,
2277
+ json: bools.has("--json"),
2278
+ help: false,
2279
+ };
2280
+ }
2281
+ /** Fixed operator sentences, chosen by the server's structured code. */
2282
+ const REGENERATE_PLAN_REFUSAL_SENTENCES = {
2283
+ TICKET_NOT_IN_RUN: "the ticket is not part of this run",
2284
+ NOT_FOUND: "the run or ticket was not found",
2285
+ PLAN_REGEN_NOT_ELIGIBLE: "the ticket is not parked on a named plan failure at the implement gate",
2286
+ PLAN_REGEN_OPERATION_CONFLICT: "this operation already names a different regeneration",
2287
+ PLAN_REGEN_ANSWERED_CONFLICT: "this park was already answered with a different route",
2288
+ PLAN_REGEN_STALE_PARK: "the ticket is on a different park than this request was made for",
2289
+ PLAN_REGEN_ALLOWANCE_SPENT: "every regeneration this failure reason allows has been used",
2290
+ PLAN_REGEN_ROUTE_NOT_ALLOWED: "that route is not the one the next regeneration allows",
2291
+ PLAN_REGEN_RUN_NOT_ACTIVE: "the run is paused, stopped, quarantined, or not active",
2292
+ PLAN_REGEN_BUSY: "the ticket or run stayed busy for the whole bounded wait",
2293
+ PLAN_REGEN_SCHEMA_UNAVAILABLE: "the server database has not been migrated for this action yet",
2294
+ SPEC_SNAPSHOT_UNAVAILABLE: "the ticket's current specification could not be read",
2295
+ CONFLICT: "the ticket kept changing while the request was being applied",
2296
+ };
2297
+ const REGENERATE_PLAN_GUIDANCE = "Read `epic-implementer explain-run` for the ticket's current park and next action.";
2298
+ const REGENERATE_PLAN_NOT_ELIGIBLE_TEXT = {
2299
+ ticket_not_found: "the ticket is not part of this run",
2300
+ not_parked: "the ticket is not parked",
2301
+ not_implement_park: "the ticket is not parked at the implement gate",
2302
+ no_plan_failure: "the park has no named plan failure (a legacy or unrelated park)",
2303
+ park_superseded: "the plan failure was already answered or belongs to an earlier park",
2304
+ };
2305
+ /**
2306
+ * Exported for tests: render a `regenerate-plan` result.
2307
+ *
2308
+ * Committed and replayed results go to stdout with exit 0 (JSON stdout stays
2309
+ * machine-readable; progress goes to stderr). Refused, stale, unconfirmed, and
2310
+ * failed outcomes go to stderr with their own non-zero codes. No operation key,
2311
+ * row version, credential, server exception, or URL is ever printed.
2312
+ */
2313
+ export function renderRegeneratePlanResult(result, target, json) {
2314
+ const label = `ticket ${target.ticketKey} (run ${target.epicRunId})`;
2315
+ if (result.kind === "committed" || result.kind === "replayed" || result.kind === "answered_replay") {
2316
+ const r = result.result;
2317
+ if (json) {
2318
+ console.log(JSON.stringify({
2319
+ ok: true,
2320
+ outcome: result.kind,
2321
+ historical: result.kind !== "committed",
2322
+ epic_run_id: r.epic_run_id,
2323
+ ticket_key: r.ticket_key,
2324
+ accepted_route: r.accepted_route,
2325
+ answered_park_generation: r.answered_park_generation,
2326
+ ticket_status: r.ticket_status,
2327
+ plan_ready: false,
2328
+ reference_reused: result.reusedReference,
2329
+ }));
2330
+ return REGENERATE_PLAN_EXIT_OK;
2331
+ }
2332
+ if (result.kind === "committed") {
2333
+ console.log(`Committed: the ${r.accepted_route} route override and the gate reset for ${label}. ` +
2334
+ "This does NOT mean a usable plan exists.");
2335
+ console.log("Next: request the plan and poll get_plan until it shows plan_metadata_status: ok.");
2336
+ }
2337
+ else {
2338
+ console.log(`Replayed historical result: regenerate-plan for ${label} committed earlier on the ` +
2339
+ `${r.accepted_route} route. Nothing was changed by this call.`);
2340
+ console.log("This does not show that a usable plan exists.");
2341
+ }
2342
+ return REGENERATE_PLAN_EXIT_OK;
2343
+ }
2344
+ if (result.kind === "unsupported_server") {
2345
+ // The server predates the route. Nothing was written and no other mutation
2346
+ // stands in for it.
2347
+ if (json) {
2348
+ console.error(JSON.stringify({
2349
+ ok: false,
2350
+ outcome: "unsupported_server",
2351
+ message: REGENERATE_PLAN_SERVER_TOO_OLD,
2352
+ nothing_written: true,
2353
+ }));
2354
+ }
2355
+ else {
2356
+ console.error(REGENERATE_PLAN_SERVER_TOO_OLD);
2357
+ }
2358
+ return REGENERATE_PLAN_EXIT_FAILED;
2359
+ }
2360
+ if (result.kind === "refused" || result.kind === "not_eligible" ||
2361
+ result.kind === "stale_subject" || result.kind === "pinned_mismatch") {
2362
+ let code;
2363
+ let sentence;
2364
+ if (result.kind === "refused") {
2365
+ code = result.errorCode;
2366
+ sentence = REGENERATE_PLAN_REFUSAL_SENTENCES[code] ?? "the server refused the request";
2367
+ }
2368
+ else if (result.kind === "stale_subject") {
2369
+ code = "REPAIR_SUBJECT_STALE";
2370
+ sentence = "the ticket is no longer on the park this operation was made for";
2371
+ }
2372
+ else if (result.kind === "pinned_mismatch") {
2373
+ code = "PLAN_REGEN_PINNED_ROUTE";
2374
+ sentence =
2375
+ `an earlier unconfirmed regenerate-plan is pinned on the ${result.pinnedRoute} route; ` +
2376
+ "re-run it with that route";
2377
+ }
2378
+ else {
2379
+ code = "PLAN_REGEN_NOT_ELIGIBLE";
2380
+ sentence = REGENERATE_PLAN_NOT_ELIGIBLE_TEXT[result.reason] ?? "regenerate-plan does not apply";
2381
+ }
2382
+ if (json) {
2383
+ console.error(JSON.stringify({
2384
+ ok: false,
2385
+ outcome: "refused",
2386
+ refusal_code: code,
2387
+ ...(result.kind === "not_eligible" ? { local_reason: result.reason } : {}),
2388
+ message: sentence,
2389
+ guidance: REGENERATE_PLAN_GUIDANCE,
2390
+ nothing_written: true,
2391
+ }));
2392
+ return REGENERATE_PLAN_EXIT_REFUSED;
2393
+ }
2394
+ console.error(`Refused: regenerate-plan for ${label} — ${sentence}. Nothing was written.`);
2395
+ console.error(REGENERATE_PLAN_GUIDANCE);
2396
+ return REGENERATE_PLAN_EXIT_REFUSED;
2397
+ }
2398
+ if (result.kind === "unconfirmed") {
2399
+ if (json) {
2400
+ console.error(JSON.stringify({
2401
+ ok: false,
2402
+ outcome: "unconfirmed",
2403
+ transport: result.transport,
2404
+ attempts: result.attempts,
2405
+ }));
2406
+ return REGENERATE_PLAN_EXIT_UNCONFIRMED;
2407
+ }
2408
+ console.error(`Unconfirmed: regenerate-plan for ${label} — no durable answer arrived after ${result.attempts} ` +
2409
+ `attempts (${result.transport}). The regeneration may or may not have committed.`);
2410
+ console.error("Re-run the same command later: it reuses this same operation and reports its original " +
2411
+ "result. Do not use unpark or another recovery in the meantime.");
2412
+ return REGENERATE_PLAN_EXIT_UNCONFIRMED;
2413
+ }
2414
+ const message = result.kind === "unavailable" ? result.message : "the outcome could not be read";
2415
+ if (json) {
2416
+ console.error(JSON.stringify({ ok: false, outcome: "failed", message }));
2417
+ }
2418
+ else {
2419
+ console.error(`Failed: regenerate-plan for ${label} — ${message}.`);
2420
+ }
2421
+ return REGENERATE_PLAN_EXIT_FAILED;
2422
+ }
2423
+ export async function runRegeneratePlanCommand(argv) {
2424
+ try {
2425
+ const parsed = parseRegeneratePlanArgs(argv);
2426
+ if (parsed.help) {
2427
+ console.log(REGENERATE_PLAN_USAGE);
2428
+ return 0;
2429
+ }
2430
+ const access = await resolveRecoveryAccess(parsed.json);
2431
+ if (!access)
2432
+ return REGENERATE_PLAN_EXIT_FAILED;
2433
+ const { regeneratePlanOnce } = await import("./recovery-operations.js");
2434
+ if (!parsed.json) {
2435
+ // Progress goes to stderr so stdout stays the result alone.
2436
+ console.error(`Requesting plan regeneration for ticket ${parsed.ticketKey} on the ${parsed.route} route...`);
2437
+ }
2438
+ const result = await regeneratePlanOnce(access, {
2439
+ epicRunId: parsed.epicRunId,
2440
+ ticketKey: parsed.ticketKey,
2441
+ route: parsed.route,
2442
+ reason: parsed.reason ?? "",
2443
+ });
2444
+ return renderRegeneratePlanResult(result, parsed, parsed.json);
2445
+ }
2446
+ catch (error) {
2447
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
2448
+ return REGENERATE_PLAN_EXIT_FAILED;
2449
+ }
2450
+ }
2451
+ const ESCALATE_FALLBACK_MESSAGE = "Escalation request refused or unavailable.";
2452
+ /**
2453
+ * Build the safe failure document for `escalate`. Reads ONLY a bridge error's
2454
+ * `errorCode`, `status`, and already-redacted `bodyPreview`; every other failure
2455
+ * gets a stable local code and a fixed message. Arbitrary exception text is never
2456
+ * inspected, and no command-specific redaction is added here.
2457
+ */
2458
+ export function formatEscalateError(error) {
2459
+ // Recognized by name rather than `instanceof`: the client module is imported
2460
+ // lazily inside the command, so this formatter does not load it.
2461
+ if (error instanceof Error && error.name === "ConductorBridgeApiError") {
2462
+ const bridge = error;
2463
+ const preview = typeof bridge.bodyPreview === "string" ? bridge.bodyPreview.trim() : "";
2464
+ return {
2465
+ ok: false,
2466
+ error_code: typeof bridge.errorCode === "string" && bridge.errorCode.length > 0
2467
+ ? bridge.errorCode
2468
+ : "ESCALATE_REQUEST_FAILED",
2469
+ status: typeof bridge.status === "number" ? bridge.status : null,
2470
+ message: preview.length > 0 ? preview : ESCALATE_FALLBACK_MESSAGE,
2471
+ };
2472
+ }
2473
+ if (error instanceof ConductorValidationError) {
2474
+ return {
2475
+ ok: false,
2476
+ error_code: "ESCALATE_INVALID_ARGUMENTS",
2477
+ status: null,
2478
+ message: "Invalid escalate arguments; run with --help for usage.",
2479
+ };
2480
+ }
2481
+ return {
2482
+ ok: false,
2483
+ error_code: "ESCALATE_UNEXPECTED_FAILURE",
2484
+ status: null,
2485
+ message: ESCALATE_FALLBACK_MESSAGE,
2486
+ };
2487
+ }
1927
2488
  /** No state mutation: callers halt the affected work before asking a question. */
1928
2489
  export async function runEscalateCommand(argv, deps = {}) {
2490
+ // Captured before parsing can throw, so a malformed command line still answers
2491
+ // in the format the caller asked for.
2492
+ const json = argv.includes("--json");
2493
+ const fail = (document) => {
2494
+ if (json)
2495
+ console.log(JSON.stringify(document));
2496
+ else
2497
+ console.error(`Error: ${document.error_code}: ${document.message}`);
2498
+ return 1;
2499
+ };
1929
2500
  try {
1930
2501
  const { values, bools } = tokenizeRecoveryFlags(argv, new Set(["--epic-run-id", "--ticket-key", "--cause", "--action-needed"]), RECOVERY_BOOL_FLAGS);
1931
2502
  if (bools.has("--help")) {
@@ -1936,18 +2507,32 @@ export async function runEscalateCommand(argv, deps = {}) {
1936
2507
  ticketKey: requireFlag(values, "--ticket-key"), cause: requireFlag(values, "--cause"),
1937
2508
  actionNeeded: requireFlag(values, "--action-needed") };
1938
2509
  if (request.actionNeeded.length > 300) {
1939
- console.error("Error: action-needed exceeds 300 characters");
1940
- return 1;
2510
+ return fail({
2511
+ ok: false,
2512
+ error_code: "ESCALATE_INVALID_ARGUMENTS",
2513
+ status: null,
2514
+ message: "action-needed exceeds 300 characters",
2515
+ });
2516
+ }
2517
+ const resolveAccess = deps.resolveAccess ?? (async () => {
2518
+ const { resolveConductorBridgeApiAccess } = await import("./bridge-api-client.js");
2519
+ const resolved = await resolveConductorBridgeApiAccess();
2520
+ return resolved.ok ? resolved.access : null;
2521
+ });
2522
+ const access = await resolveAccess();
2523
+ if (!access) {
2524
+ return fail({
2525
+ ok: false,
2526
+ error_code: "ESCALATE_CREDENTIALS_UNAVAILABLE",
2527
+ status: null,
2528
+ message: "Bridge API credentials could not be resolved.",
2529
+ });
1941
2530
  }
1942
- const access = await (deps.resolveAccess ?? (() => resolveRecoveryAccess(false)))();
1943
- if (!access)
1944
- return 1;
1945
2531
  const escalate = deps.escalate ?? (await import("./bridge-api-client.js")).escalateTicket;
1946
2532
  console.log(JSON.stringify(await escalate(access, request)));
1947
2533
  return 0;
1948
2534
  }
1949
- catch {
1950
- console.error("Error: escalation request refused or unavailable");
1951
- return 1;
2535
+ catch (error) {
2536
+ return fail(formatEscalateError(error));
1952
2537
  }
1953
2538
  }