@bridge_gpt/mcp-server 0.2.69 → 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.
- package/README.md +140 -292
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +57 -41
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +66 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +72 -0
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1067 -536
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +376 -60
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/upgrade-cli.js +13 -0
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `conductor` operator recovery and repair verbs —
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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([
|
|
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
|
|
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(
|
|
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
|
-
|
|
1940
|
-
|
|
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
|
-
|
|
1951
|
-
return 1;
|
|
2535
|
+
catch (error) {
|
|
2536
|
+
return fail(formatEscalateError(error));
|
|
1952
2537
|
}
|
|
1953
2538
|
}
|