@bridge_gpt/mcp-server 0.2.65 → 0.2.66
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 +25 -13
- package/build/agent-launchers/claude-executor-adapter.js +12 -18
- package/build/agent-launchers/executor-adapter.js +27 -0
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +230 -10
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +12 -0
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness.js +23 -1
- package/build/conductor/recovery-cli.js +507 -16
- package/build/conductor/recovery-operations.js +325 -2
- package/build/conductor/repair-contract.js +61 -3
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +70 -10
- package/build/drive-epic.js +395 -78
- package/build/epic-implementer-bin.js +24 -22
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/completion-supervision.js +5 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +6 -0
- package/build/executor/http-client.js +15 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +13 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +260 -33
- package/build/executor/job-types.js +8 -13
- package/build/executor/preflight.js +91 -2
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/runner.js +23 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +80 -15
- package/build/executor/worker-guard-hook.js +46 -10
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/index.js +177 -329
- package/build/init.js +107 -4
- package/build/install-bridge.js +284 -20
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +1 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +4 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +180 -108
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +4 -3
- package/build/worktree-core.js +6 -5
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `conductor` operator recovery and repair verbs —
|
|
2
|
+
* `conductor` operator recovery and repair verbs — thirteen CLI-only commands.
|
|
3
3
|
*
|
|
4
4
|
* The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
|
|
5
5
|
* `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
|
|
6
6
|
* `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
|
|
7
7
|
* `scope-catch-up`, `patch-policy`, `resume-run`. BAPI-1300's parked-child
|
|
8
8
|
* continuation pair: `continue-child` and the read-only `continuation-receipt`.
|
|
9
|
+
* BAPI-1326's allowance-preserving CI resume: `resume-ci`.
|
|
9
10
|
*
|
|
10
11
|
* CLI-only by design — this is an explicit ticket non-goal, not an oversight:
|
|
11
|
-
* none of these
|
|
12
|
+
* none of these thirteen command names is ever registered as an MCP tool, tool
|
|
12
13
|
* description, or generated tool-schema input (`index-static.test.ts` guards
|
|
13
14
|
* this). They exist so an operator (or `plane down`, via
|
|
14
15
|
* `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
import { readFile } from "node:fs/promises";
|
|
26
27
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
27
28
|
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES, RESUME_RUN_UNCONFIRMED_MESSAGE, } from "./repair-contract.js";
|
|
29
|
+
import { decodeGuidedBinding, GuidedBindingError, } from "./guided-repair.js";
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// Flag parsing
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
@@ -378,13 +380,26 @@ export async function runAdoptCurrentHeadAndUnparkCommand(argv) {
|
|
|
378
380
|
// ---------------------------------------------------------------------------
|
|
379
381
|
// BAPI-1154 — repair verbs: scope-reparse, dispatch-remediation, merge-child
|
|
380
382
|
// ---------------------------------------------------------------------------
|
|
381
|
-
|
|
383
|
+
// BAPI-1333 — `--binding` is the ONE guided-repair input: the opaque token
|
|
384
|
+
// ExplainRun rendered, carrying the subject the command is pinned to. It is not
|
|
385
|
+
// a version counter the operator composes, so REJECTED_VERSION_FLAGS is unchanged.
|
|
386
|
+
const SCOPE_REPARSE_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id", "--binding"]);
|
|
382
387
|
const DISPATCH_REMEDIATION_VALUE_FLAGS = new Set([
|
|
383
388
|
"--epic-run-id",
|
|
384
389
|
"--ticket-key",
|
|
385
390
|
"--context-file",
|
|
391
|
+
"--binding",
|
|
386
392
|
]);
|
|
387
393
|
const MERGE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--pr"]);
|
|
394
|
+
/**
|
|
395
|
+
* BAPI-1333 — attach `binding` ONLY when `--binding` was given, so an unguided
|
|
396
|
+
* parse result is exactly what it was before.
|
|
397
|
+
*/
|
|
398
|
+
function withBinding(parsed, values) {
|
|
399
|
+
if (!values.has("--binding"))
|
|
400
|
+
return parsed;
|
|
401
|
+
return { ...parsed, binding: requireFlag(values, "--binding") };
|
|
402
|
+
}
|
|
388
403
|
function requireFlag(values, flag) {
|
|
389
404
|
const value = values.get(flag);
|
|
390
405
|
if (value === undefined || value.trim().length === 0) {
|
|
@@ -397,25 +412,25 @@ export function parseScopeReparseArgs(argv) {
|
|
|
397
412
|
if (bools.has("--help")) {
|
|
398
413
|
return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
|
|
399
414
|
}
|
|
400
|
-
return {
|
|
415
|
+
return withBinding({
|
|
401
416
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
402
417
|
scopeId: requireFlag(values, "--scope-id"),
|
|
403
418
|
json: bools.has("--json"),
|
|
404
419
|
help: false,
|
|
405
|
-
};
|
|
420
|
+
}, values);
|
|
406
421
|
}
|
|
407
422
|
export function parseDispatchRemediationArgs(argv) {
|
|
408
423
|
const { values, bools } = tokenizeRecoveryFlags(argv, DISPATCH_REMEDIATION_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
409
424
|
if (bools.has("--help")) {
|
|
410
425
|
return { epicRunId: "", ticketKey: "", contextFile: "", json: bools.has("--json"), help: true };
|
|
411
426
|
}
|
|
412
|
-
return {
|
|
427
|
+
return withBinding({
|
|
413
428
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
414
429
|
ticketKey: requireFlag(values, "--ticket-key"),
|
|
415
430
|
contextFile: requireFlag(values, "--context-file"),
|
|
416
431
|
json: bools.has("--json"),
|
|
417
432
|
help: false,
|
|
418
|
-
};
|
|
433
|
+
}, values);
|
|
419
434
|
}
|
|
420
435
|
export function parseMergeChildArgs(argv) {
|
|
421
436
|
const { values, bools } = tokenizeRecoveryFlags(argv, MERGE_CHILD_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
@@ -457,6 +472,124 @@ export function decodeDriverContextFile(bytes) {
|
|
|
457
472
|
}
|
|
458
473
|
return text;
|
|
459
474
|
}
|
|
475
|
+
/** One repair result, however it ended, as the renderer needs it. */
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// BAPI-1333 — guided (binding-pinned) repairs
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
/** Exit codes shared with `resume-ci`: 0 applied/no-op, 2 refused/stale, 3 unconfirmed. */
|
|
480
|
+
export const GUIDED_REPAIR_EXIT_OK = 0;
|
|
481
|
+
export const GUIDED_REPAIR_EXIT_FAILED = 1;
|
|
482
|
+
export const GUIDED_REPAIR_EXIT_REFUSED = 2;
|
|
483
|
+
export const GUIDED_REPAIR_EXIT_UNCONFIRMED = 3;
|
|
484
|
+
export const GUIDED_REPAIR_UNCONFIRMED_MESSAGE = "the outcome could not be confirmed: the server may or may not have applied this " +
|
|
485
|
+
"guided repair. Re-run the SAME command to re-send the same pinned request, or " +
|
|
486
|
+
"inspect the run read-only";
|
|
487
|
+
const GUIDED_STALE_GUIDANCE = "Nothing was written. The run changed since this command was rendered (possibly " +
|
|
488
|
+
"by this command's own earlier delivery). Re-read the run explanation before " +
|
|
489
|
+
"choosing another action.";
|
|
490
|
+
/**
|
|
491
|
+
* Decode `--binding` and prove it names THIS command, run, and target. Runs
|
|
492
|
+
* before credentials are resolved, so a mismatched token reaches no one.
|
|
493
|
+
*/
|
|
494
|
+
function checkGuidedBinding(token, verb, expected) {
|
|
495
|
+
let binding;
|
|
496
|
+
try {
|
|
497
|
+
binding = decodeGuidedBinding(token);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
throw new ConductorValidationError(err instanceof GuidedBindingError ? err.message : "--binding could not be decoded; nothing was sent.");
|
|
501
|
+
}
|
|
502
|
+
if (binding.verb !== verb ||
|
|
503
|
+
binding.epicRunId !== expected.epicRunId ||
|
|
504
|
+
(expected.scopeId !== undefined && binding.scopeId !== expected.scopeId) ||
|
|
505
|
+
(expected.ticketKey !== undefined && binding.ticketKey !== expected.ticketKey)) {
|
|
506
|
+
throw new ConductorValidationError("--binding was rendered for a different command, run, or target; nothing was sent.");
|
|
507
|
+
}
|
|
508
|
+
return binding;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Send one guided repair and render its closed outcome. Success goes to stdout;
|
|
512
|
+
* every failure goes to stderr (JSON mode included), with the exit codes above.
|
|
513
|
+
*/
|
|
514
|
+
async function runGuidedRepairCommand(input, json) {
|
|
515
|
+
const { guidedRepairOnce } = await import("./recovery-operations.js");
|
|
516
|
+
const { guidedPayloadDigest } = await import("./guided-repair.js");
|
|
517
|
+
const outcome = await guidedRepairOnce({
|
|
518
|
+
binding: input.binding,
|
|
519
|
+
payloadDigest: guidedPayloadDigest(input.payload),
|
|
520
|
+
send: input.send,
|
|
521
|
+
unconfirmedMessage: GUIDED_REPAIR_UNCONFIRMED_MESSAGE,
|
|
522
|
+
});
|
|
523
|
+
const verb = input.binding.verb;
|
|
524
|
+
const runId = input.binding.epicRunId;
|
|
525
|
+
if (outcome.kind === "committed" || outcome.kind === "no_op") {
|
|
526
|
+
const result = outcome.result;
|
|
527
|
+
if (outcome.kind === "committed" && !result.event_recorded) {
|
|
528
|
+
console.error(`Warning: ${verb} on run ${runId} (${input.label}) COMMITTED, but its ` +
|
|
529
|
+
"operator_verb_applied audit event was not recorded. The action stands.");
|
|
530
|
+
}
|
|
531
|
+
if (json) {
|
|
532
|
+
console.log(JSON.stringify({
|
|
533
|
+
ok: true,
|
|
534
|
+
outcome: outcome.kind,
|
|
535
|
+
request_id: outcome.requestId,
|
|
536
|
+
reference_reused: outcome.referenceReused,
|
|
537
|
+
result,
|
|
538
|
+
}));
|
|
539
|
+
return GUIDED_REPAIR_EXIT_OK;
|
|
540
|
+
}
|
|
541
|
+
console.log([
|
|
542
|
+
`${verb} (guided) — run ${runId}`,
|
|
543
|
+
` target: ${input.label}`,
|
|
544
|
+
` ${input.stateLabel}: ${result.before ?? "none"} → ${result.after ?? "none"}`,
|
|
545
|
+
` outcome: ${outcome.kind === "committed" ? "committed" : "no-op (already in effect; nothing written)"}`,
|
|
546
|
+
` request: ${outcome.requestId}${outcome.referenceReused ? " (re-sent from the local reference)" : ""}`,
|
|
547
|
+
` event recorded: ${result.event_recorded}`,
|
|
548
|
+
].join("\n"));
|
|
549
|
+
return GUIDED_REPAIR_EXIT_OK;
|
|
550
|
+
}
|
|
551
|
+
if (outcome.kind === "stale" || outcome.kind === "refused") {
|
|
552
|
+
const payload = {
|
|
553
|
+
ok: false,
|
|
554
|
+
outcome: outcome.kind,
|
|
555
|
+
code: outcome.code,
|
|
556
|
+
message: outcome.message,
|
|
557
|
+
request_id: outcome.requestId,
|
|
558
|
+
nothing_written: true,
|
|
559
|
+
};
|
|
560
|
+
if (json) {
|
|
561
|
+
console.error(JSON.stringify(payload));
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
console.error(`Error: ${verb} ${outcome.kind === "stale" ? "is stale" : "refused"} on run ${runId} ` +
|
|
565
|
+
`(${input.label}) — ${outcome.code}: ${outcome.message}` +
|
|
566
|
+
(outcome.kind === "stale" ? ` ${GUIDED_STALE_GUIDANCE}` : ""));
|
|
567
|
+
}
|
|
568
|
+
return GUIDED_REPAIR_EXIT_REFUSED;
|
|
569
|
+
}
|
|
570
|
+
if (outcome.kind === "unconfirmed") {
|
|
571
|
+
if (json) {
|
|
572
|
+
console.error(JSON.stringify({
|
|
573
|
+
ok: false,
|
|
574
|
+
outcome: "unconfirmed",
|
|
575
|
+
message: outcome.message,
|
|
576
|
+
request_id: outcome.requestId,
|
|
577
|
+
reference_kept: true,
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
console.error(`Error: ${verb} on run ${runId} (${input.label}) — ${outcome.message}.`);
|
|
582
|
+
}
|
|
583
|
+
return GUIDED_REPAIR_EXIT_UNCONFIRMED;
|
|
584
|
+
}
|
|
585
|
+
if (json) {
|
|
586
|
+
console.error(JSON.stringify({ ok: false, outcome: "unavailable", message: outcome.message }));
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
console.error(`Error: ${verb} on run ${runId} (${input.label}) — ${outcome.message}.`);
|
|
590
|
+
}
|
|
591
|
+
return GUIDED_REPAIR_EXIT_FAILED;
|
|
592
|
+
}
|
|
460
593
|
/**
|
|
461
594
|
* Render a repair outcome. Human mode prints the verb and run first, the target
|
|
462
595
|
* second, ONE labeled `Before → After` pair third, then whether this call
|
|
@@ -521,6 +654,12 @@ const SCOPE_REPARSE_USAGE = [
|
|
|
521
654
|
"schedule its parse. The scope must be the one bound to the named run, or the",
|
|
522
655
|
"server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints the scope",
|
|
523
656
|
"lifecycle before → after; it does not wait for the parse to finish.",
|
|
657
|
+
"",
|
|
658
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
659
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
660
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
661
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
662
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
524
663
|
].join("\n");
|
|
525
664
|
export async function runScopeReparseCommand(argv) {
|
|
526
665
|
try {
|
|
@@ -529,9 +668,25 @@ export async function runScopeReparseCommand(argv) {
|
|
|
529
668
|
console.log(SCOPE_REPARSE_USAGE);
|
|
530
669
|
return 0;
|
|
531
670
|
}
|
|
671
|
+
const binding = parsed.binding === undefined
|
|
672
|
+
? null
|
|
673
|
+
: checkGuidedBinding(parsed.binding, "scope-reparse", {
|
|
674
|
+
epicRunId: parsed.epicRunId,
|
|
675
|
+
scopeId: parsed.scopeId,
|
|
676
|
+
});
|
|
532
677
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
533
678
|
if (!access)
|
|
534
679
|
return 1;
|
|
680
|
+
if (binding !== null) {
|
|
681
|
+
const { scopeReparse } = await import("./bridge-api-client.js");
|
|
682
|
+
return await runGuidedRepairCommand({
|
|
683
|
+
binding,
|
|
684
|
+
payload: { scope_id: parsed.scopeId },
|
|
685
|
+
send: (guard) => scopeReparse(access, { epicRunId: parsed.epicRunId, scopeId: parsed.scopeId, guard }),
|
|
686
|
+
label: `scope ${parsed.scopeId}`,
|
|
687
|
+
stateLabel: "scope lifecycle",
|
|
688
|
+
}, parsed.json);
|
|
689
|
+
}
|
|
535
690
|
const { scopeReparseRepair } = await import("./recovery-operations.js");
|
|
536
691
|
const outcome = await scopeReparseRepair(access, {
|
|
537
692
|
epicRunId: parsed.epicRunId,
|
|
@@ -563,6 +718,12 @@ const DISPATCH_REMEDIATION_USAGE = [
|
|
|
563
718
|
`--context-file must be non-empty UTF-8 text of at most DRIVER_CONTEXT_MAX_BYTES`,
|
|
564
719
|
`(${DRIVER_CONTEXT_MAX_BYTES} bytes). It is rejected, never truncated, and is never`,
|
|
565
720
|
"printed, logged, or echoed — it reaches only the worker's prompt.",
|
|
721
|
+
"",
|
|
722
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
723
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
724
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
725
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
726
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
566
727
|
].join("\n");
|
|
567
728
|
export async function runDispatchRemediationCommand(argv, deps = {}) {
|
|
568
729
|
try {
|
|
@@ -581,9 +742,31 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
|
|
|
581
742
|
throw new ConductorValidationError("--context-file could not be read; nothing was sent.");
|
|
582
743
|
}
|
|
583
744
|
const context = decodeDriverContextFile(bytes);
|
|
745
|
+
const binding = parsed.binding === undefined
|
|
746
|
+
? null
|
|
747
|
+
: checkGuidedBinding(parsed.binding, "dispatch-remediation", {
|
|
748
|
+
epicRunId: parsed.epicRunId,
|
|
749
|
+
ticketKey: parsed.ticketKey,
|
|
750
|
+
});
|
|
584
751
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
585
752
|
if (!access)
|
|
586
753
|
return 1;
|
|
754
|
+
if (binding !== null) {
|
|
755
|
+
const { dispatchRemediation } = await import("./bridge-api-client.js");
|
|
756
|
+
return await runGuidedRepairCommand({
|
|
757
|
+
binding,
|
|
758
|
+
// Digested, never stored: the context stays out of the reference file.
|
|
759
|
+
payload: { ticket_key: parsed.ticketKey, context: context },
|
|
760
|
+
send: (guard) => dispatchRemediation(access, {
|
|
761
|
+
epicRunId: parsed.epicRunId,
|
|
762
|
+
ticketKey: parsed.ticketKey,
|
|
763
|
+
context,
|
|
764
|
+
guard,
|
|
765
|
+
}),
|
|
766
|
+
label: `ticket ${parsed.ticketKey}`,
|
|
767
|
+
stateLabel: "code_review gate",
|
|
768
|
+
}, parsed.json);
|
|
769
|
+
}
|
|
587
770
|
const { dispatchRemediationRepair } = await import("./recovery-operations.js");
|
|
588
771
|
const outcome = await dispatchRemediationRepair(access, {
|
|
589
772
|
epicRunId: parsed.epicRunId,
|
|
@@ -651,9 +834,9 @@ export async function runMergeChildCommand(argv) {
|
|
|
651
834
|
// ---------------------------------------------------------------------------
|
|
652
835
|
// BAPI-1176 — repair verbs: scope-catch-up, patch-policy, resume-run
|
|
653
836
|
// ---------------------------------------------------------------------------
|
|
654
|
-
const SCOPE_CATCH_UP_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id"]);
|
|
655
|
-
const PATCH_POLICY_VALUE_FLAGS = new Set(["--epic-run-id", "--patch-file"]);
|
|
656
|
-
const RESUME_RUN_VALUE_FLAGS = new Set(["--epic-run-id"]);
|
|
837
|
+
const SCOPE_CATCH_UP_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id", "--binding"]);
|
|
838
|
+
const PATCH_POLICY_VALUE_FLAGS = new Set(["--epic-run-id", "--patch-file", "--binding"]);
|
|
839
|
+
const RESUME_RUN_VALUE_FLAGS = new Set(["--epic-run-id", "--binding"]);
|
|
657
840
|
/** `--clear-quarantine` is a `resume-run` switch only; no other verb accepts it. */
|
|
658
841
|
const RESUME_RUN_BOOL_FLAGS = new Set(["--json", "--help", "--clear-quarantine"]);
|
|
659
842
|
export function parseScopeCatchUpArgs(argv) {
|
|
@@ -661,24 +844,24 @@ export function parseScopeCatchUpArgs(argv) {
|
|
|
661
844
|
if (bools.has("--help")) {
|
|
662
845
|
return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
|
|
663
846
|
}
|
|
664
|
-
return {
|
|
847
|
+
return withBinding({
|
|
665
848
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
666
849
|
scopeId: requireFlag(values, "--scope-id"),
|
|
667
850
|
json: bools.has("--json"),
|
|
668
851
|
help: false,
|
|
669
|
-
};
|
|
852
|
+
}, values);
|
|
670
853
|
}
|
|
671
854
|
export function parsePatchPolicyArgs(argv) {
|
|
672
855
|
const { values, bools } = tokenizeRecoveryFlags(argv, PATCH_POLICY_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
673
856
|
if (bools.has("--help")) {
|
|
674
857
|
return { epicRunId: "", patchFile: "", json: bools.has("--json"), help: true };
|
|
675
858
|
}
|
|
676
|
-
return {
|
|
859
|
+
return withBinding({
|
|
677
860
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
678
861
|
patchFile: requireFlag(values, "--patch-file"),
|
|
679
862
|
json: bools.has("--json"),
|
|
680
863
|
help: false,
|
|
681
|
-
};
|
|
864
|
+
}, values);
|
|
682
865
|
}
|
|
683
866
|
export function parseResumeRunArgs(argv) {
|
|
684
867
|
const { values, bools } = tokenizeRecoveryFlags(argv, RESUME_RUN_VALUE_FLAGS, RESUME_RUN_BOOL_FLAGS);
|
|
@@ -690,12 +873,12 @@ export function parseResumeRunArgs(argv) {
|
|
|
690
873
|
help: true,
|
|
691
874
|
};
|
|
692
875
|
}
|
|
693
|
-
return {
|
|
876
|
+
return withBinding({
|
|
694
877
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
695
878
|
clearQuarantine: bools.has("--clear-quarantine"),
|
|
696
879
|
json: bools.has("--json"),
|
|
697
880
|
help: false,
|
|
698
|
-
};
|
|
881
|
+
}, values);
|
|
699
882
|
}
|
|
700
883
|
/**
|
|
701
884
|
* Validate and decode a `patch-policy` file's BYTES into a merge-patch object.
|
|
@@ -749,6 +932,12 @@ const SCOPE_CATCH_UP_USAGE = [
|
|
|
749
932
|
"refused (SCOPE_CATCH_UP_REFUSED). The scope must be the one bound to the named",
|
|
750
933
|
"run, or the server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints",
|
|
751
934
|
"the scope lifecycle before → after; it does not wait for a scheduled parse.",
|
|
935
|
+
"",
|
|
936
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
937
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
938
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
939
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
940
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
752
941
|
].join("\n");
|
|
753
942
|
export async function runScopeCatchUpCommand(argv) {
|
|
754
943
|
try {
|
|
@@ -757,9 +946,25 @@ export async function runScopeCatchUpCommand(argv) {
|
|
|
757
946
|
console.log(SCOPE_CATCH_UP_USAGE);
|
|
758
947
|
return 0;
|
|
759
948
|
}
|
|
949
|
+
const binding = parsed.binding === undefined
|
|
950
|
+
? null
|
|
951
|
+
: checkGuidedBinding(parsed.binding, "scope-catch-up", {
|
|
952
|
+
epicRunId: parsed.epicRunId,
|
|
953
|
+
scopeId: parsed.scopeId,
|
|
954
|
+
});
|
|
760
955
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
761
956
|
if (!access)
|
|
762
957
|
return 1;
|
|
958
|
+
if (binding !== null) {
|
|
959
|
+
const { scopeCatchUp } = await import("./bridge-api-client.js");
|
|
960
|
+
return await runGuidedRepairCommand({
|
|
961
|
+
binding,
|
|
962
|
+
payload: { scope_id: parsed.scopeId },
|
|
963
|
+
send: (guard) => scopeCatchUp(access, { epicRunId: parsed.epicRunId, scopeId: parsed.scopeId, guard }),
|
|
964
|
+
label: `scope ${parsed.scopeId}`,
|
|
965
|
+
stateLabel: "scope lifecycle",
|
|
966
|
+
}, parsed.json);
|
|
967
|
+
}
|
|
763
968
|
const { scopeCatchUpRepair } = await import("./recovery-operations.js");
|
|
764
969
|
const outcome = await scopeCatchUpRepair(access, {
|
|
765
970
|
epicRunId: parsed.epicRunId,
|
|
@@ -792,6 +997,12 @@ const PATCH_POLICY_USAGE = [
|
|
|
792
997
|
"--patch-file must hold a non-empty JSON object of at most POLICY_PATCH_MAX_BYTES",
|
|
793
998
|
`(${POLICY_PATCH_MAX_BYTES} bytes). Its contents are never printed; the result names only`,
|
|
794
999
|
"the changed dotted paths and before/after policy digest tokens.",
|
|
1000
|
+
"",
|
|
1001
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
1002
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
1003
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
1004
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
1005
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
795
1006
|
].join("\n");
|
|
796
1007
|
export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
797
1008
|
try {
|
|
@@ -810,9 +1021,23 @@ export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
|
810
1021
|
throw new ConductorValidationError("--patch-file could not be read; nothing was sent.");
|
|
811
1022
|
}
|
|
812
1023
|
const mergePatch = decodePolicyPatchFile(bytes);
|
|
1024
|
+
const binding = parsed.binding === undefined
|
|
1025
|
+
? null
|
|
1026
|
+
: checkGuidedBinding(parsed.binding, "patch-policy", { epicRunId: parsed.epicRunId });
|
|
813
1027
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
814
1028
|
if (!access)
|
|
815
1029
|
return 1;
|
|
1030
|
+
if (binding !== null) {
|
|
1031
|
+
const { patchPolicy } = await import("./bridge-api-client.js");
|
|
1032
|
+
return await runGuidedRepairCommand({
|
|
1033
|
+
binding,
|
|
1034
|
+
// Digested, never stored: no policy value reaches the reference file.
|
|
1035
|
+
payload: { merge_patch: mergePatch },
|
|
1036
|
+
send: (guard) => patchPolicy(access, { epicRunId: parsed.epicRunId, mergePatch, guard }),
|
|
1037
|
+
label: "run policy",
|
|
1038
|
+
stateLabel: "policy digest",
|
|
1039
|
+
}, parsed.json);
|
|
1040
|
+
}
|
|
816
1041
|
const { patchPolicyRepair } = await import("./recovery-operations.js");
|
|
817
1042
|
const outcome = await patchPolicyRepair(access, {
|
|
818
1043
|
epicRunId: parsed.epicRunId,
|
|
@@ -845,6 +1070,12 @@ const RESUME_RUN_USAGE = [
|
|
|
845
1070
|
"",
|
|
846
1071
|
"If the response is lost or cannot be read, the resume may or may not have",
|
|
847
1072
|
"committed: inspect the run read-only before any separately authorized retry.",
|
|
1073
|
+
"",
|
|
1074
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
1075
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
1076
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
1077
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
1078
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
848
1079
|
].join("\n");
|
|
849
1080
|
export async function runResumeRunCommand(argv) {
|
|
850
1081
|
try {
|
|
@@ -853,9 +1084,29 @@ export async function runResumeRunCommand(argv) {
|
|
|
853
1084
|
console.log(RESUME_RUN_USAGE);
|
|
854
1085
|
return 0;
|
|
855
1086
|
}
|
|
1087
|
+
const binding = parsed.binding === undefined
|
|
1088
|
+
? null
|
|
1089
|
+
: checkGuidedBinding(parsed.binding, "resume-run", { epicRunId: parsed.epicRunId });
|
|
1090
|
+
if (binding !== null && binding.controlState === "quarantined" && !parsed.clearQuarantine) {
|
|
1091
|
+
throw new ConductorValidationError("This binding names a quarantined run; --clear-quarantine is required. Nothing was sent.");
|
|
1092
|
+
}
|
|
856
1093
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
857
1094
|
if (!access)
|
|
858
1095
|
return 1;
|
|
1096
|
+
if (binding !== null) {
|
|
1097
|
+
const { resumeRun } = await import("./bridge-api-client.js");
|
|
1098
|
+
return await runGuidedRepairCommand({
|
|
1099
|
+
binding,
|
|
1100
|
+
payload: { clear_quarantine: parsed.clearQuarantine },
|
|
1101
|
+
send: (guard) => resumeRun(access, {
|
|
1102
|
+
epicRunId: parsed.epicRunId,
|
|
1103
|
+
clearQuarantine: parsed.clearQuarantine,
|
|
1104
|
+
guard,
|
|
1105
|
+
}),
|
|
1106
|
+
label: parsed.clearQuarantine ? "run (clear quarantine)" : "run",
|
|
1107
|
+
stateLabel: "run state",
|
|
1108
|
+
}, parsed.json);
|
|
1109
|
+
}
|
|
859
1110
|
const { resumeRunRepair } = await import("./recovery-operations.js");
|
|
860
1111
|
const outcome = await resumeRunRepair(access, {
|
|
861
1112
|
epicRunId: parsed.epicRunId,
|
|
@@ -1047,6 +1298,11 @@ export function renderContinueChildResult(result, target, json) {
|
|
|
1047
1298
|
}
|
|
1048
1299
|
console.error(`${label}: not continuable right now. Nothing was sent.`);
|
|
1049
1300
|
console.error(currentLine(result.current));
|
|
1301
|
+
// BAPI-1344: the park's own guidance names the exit that remains (for a
|
|
1302
|
+
// first attempt with no recorded retained head, the run operator).
|
|
1303
|
+
if (result.current.parked && result.current.needs_human_reason) {
|
|
1304
|
+
console.error(`Park: ${result.current.needs_human_reason}`);
|
|
1305
|
+
}
|
|
1050
1306
|
return 1;
|
|
1051
1307
|
}
|
|
1052
1308
|
if (result.kind === "unconfirmed") {
|
|
@@ -1162,3 +1418,238 @@ export async function runContinuationReceiptCommand(argv) {
|
|
|
1162
1418
|
return 1;
|
|
1163
1419
|
}
|
|
1164
1420
|
}
|
|
1421
|
+
// ---------------------------------------------------------------------------
|
|
1422
|
+
// BAPI-1326 — resume-ci
|
|
1423
|
+
// ---------------------------------------------------------------------------
|
|
1424
|
+
const RESUME_CI_USAGE = [
|
|
1425
|
+
"Usage: epic-implementer resume-ci --epic-run-id <id> --ticket-key <key> [--json]",
|
|
1426
|
+
"",
|
|
1427
|
+
"Resume a child parked on a qualified CI stop (an expired CI wait, exhausted",
|
|
1428
|
+
"required-check repairs, or an unavailable verdict) once CI has finished green.",
|
|
1429
|
+
"The server re-reads the pull request, the specification, and the required",
|
|
1430
|
+
"checks, then returns CI to evaluation at the same head. It creates no job,",
|
|
1431
|
+
"grants no repair or new wait, keeps the original deadline, and does not mark",
|
|
1432
|
+
"CI passed or authorize review or merge.",
|
|
1433
|
+
"",
|
|
1434
|
+
"The operation is pinned locally before it is sent; re-running after a lost",
|
|
1435
|
+
"response reuses it and reports the original result. Nothing else is tried.",
|
|
1436
|
+
"",
|
|
1437
|
+
"Exit codes: 0 committed or replayed; 2 refused; 3 unconfirmed; 1 other failure.",
|
|
1438
|
+
].join("\n");
|
|
1439
|
+
/** Distinct exit codes, so automation can tell a refusal from an unknown outcome. */
|
|
1440
|
+
export const RESUME_CI_EXIT_OK = 0;
|
|
1441
|
+
export const RESUME_CI_EXIT_FAILED = 1;
|
|
1442
|
+
export const RESUME_CI_EXIT_REFUSED = 2;
|
|
1443
|
+
export const RESUME_CI_EXIT_UNCONFIRMED = 3;
|
|
1444
|
+
/**
|
|
1445
|
+
* Fixed operator sentences, chosen by the server's structured code. No server,
|
|
1446
|
+
* provider, or exception text ever reaches the terminal.
|
|
1447
|
+
*/
|
|
1448
|
+
const RESUME_CI_REFUSAL_SENTENCES = {
|
|
1449
|
+
TICKET_NOT_IN_RUN: "the ticket is not part of this run",
|
|
1450
|
+
CI_STOP_NOT_ELIGIBLE: "this CI park has no complete qualified stop record, so resume-ci does not apply",
|
|
1451
|
+
CI_RESUME_STALE_OPERATION: "the ticket is no longer on the CI stop this operation was made for, and its result is not retained",
|
|
1452
|
+
CI_RESUME_OPERATION_CONFLICT: "this operation already names a different CI recovery",
|
|
1453
|
+
ROW_VERSION_CONFLICT: "the ticket kept changing while the resume was being applied",
|
|
1454
|
+
RUN_NOT_DISPATCHABLE: "the run is paused, stopped, quarantined, or not active",
|
|
1455
|
+
CI_RESUME_BUDGET_EXHAUSTED: "the run's wall-clock or cost budget is spent, or could not be read",
|
|
1456
|
+
CI_RESUME_HELD: "the ticket is held by a completion, permission, declassification, or stale-specification hold",
|
|
1457
|
+
CI_RESUME_LIVE_WORK: "corrective work is, or may be, live for this ticket",
|
|
1458
|
+
CI_RESUME_SPEC_CHANGED: "the ticket's specification changed or could not be verified",
|
|
1459
|
+
CI_RESUME_POLICY_CHANGED: "the run's required-check policy changed, or is unavailable or unconfigured",
|
|
1460
|
+
CI_RESUME_BINDING_CHANGED: "the pull request, its head, or its target branch changed since the stop",
|
|
1461
|
+
CI_RESUME_CHECKS_NOT_GREEN: "the bound head's required checks are not currently green",
|
|
1462
|
+
CI_RESUME_VERDICT_UNAVAILABLE: "the CI provider's verdict could not be read in time",
|
|
1463
|
+
CI_RESUME_BUSY: "the ticket or run stayed busy for the whole bounded wait",
|
|
1464
|
+
};
|
|
1465
|
+
/** The next step for a refusal, where one is useful. */
|
|
1466
|
+
const RESUME_CI_REFUSAL_GUIDANCE = {
|
|
1467
|
+
CI_STOP_NOT_ELIGIBLE: "The existing explicit path is `epic-implementer unpark`, which RESETS the CI gate's " +
|
|
1468
|
+
"clock and repair allowance. If the park's origin is unclear, ask the run operator " +
|
|
1469
|
+
"to assess it first.",
|
|
1470
|
+
CI_RESUME_STALE_OPERATION: "Read `epic-implementer explain-run` for the ticket; start a new resume-ci only if " +
|
|
1471
|
+
"its current stop independently qualifies.",
|
|
1472
|
+
CI_RESUME_CHECKS_NOT_GREEN: "Wait for the required checks to finish green, then retry.",
|
|
1473
|
+
CI_RESUME_VERDICT_UNAVAILABLE: "Retry later. If the provider stays unavailable, escalate to the CI-platform operator.",
|
|
1474
|
+
CI_RESUME_BUSY: "Retry the same command; it reuses the same operation.",
|
|
1475
|
+
ROW_VERSION_CONFLICT: "Retry the same command; it reuses the same operation.",
|
|
1476
|
+
RUN_NOT_DISPATCHABLE: "Resume the run first (`epic-implementer resume-run`), then retry.",
|
|
1477
|
+
CI_RESUME_LIVE_WORK: "Retry when the running work has finished.",
|
|
1478
|
+
};
|
|
1479
|
+
const RESUME_CI_STOP_REASON_TEXT = {
|
|
1480
|
+
wait_expired: "an expired CI wait",
|
|
1481
|
+
repair_exhausted: "exhausted required-check repairs",
|
|
1482
|
+
verdict_unavailable: "an unavailable CI verdict",
|
|
1483
|
+
};
|
|
1484
|
+
const RESUME_CI_NOT_ELIGIBLE_TEXT = {
|
|
1485
|
+
ticket_not_found: "the ticket is not part of this run",
|
|
1486
|
+
not_parked: "the ticket is not parked",
|
|
1487
|
+
not_ci_park: "the ticket is not parked at the CI gate",
|
|
1488
|
+
no_typed_stop: "the CI park has no complete qualified stop record (a legacy or incomplete park)",
|
|
1489
|
+
stop_superseded: "the CI stop belongs to an earlier park episode",
|
|
1490
|
+
};
|
|
1491
|
+
/** The one consequence statement every committed result carries. */
|
|
1492
|
+
const RESUME_CI_CONSEQUENCE = "CI is back in evaluation at the same head; its verdict is pending. This action did NOT " +
|
|
1493
|
+
"mark CI passed, authorize review or merge, create a job, extend the deadline, or refund " +
|
|
1494
|
+
"any repair.";
|
|
1495
|
+
export function parseResumeCiArgs(argv) {
|
|
1496
|
+
return parseTicketRecoveryArgs(argv);
|
|
1497
|
+
}
|
|
1498
|
+
function resumeCiOperationSummary(op) {
|
|
1499
|
+
return {
|
|
1500
|
+
resumed_at: op.resumed_at,
|
|
1501
|
+
stop_reason: op.stop_reason,
|
|
1502
|
+
park_generation: op.park_generation,
|
|
1503
|
+
resumed_gate: op.resumed_gate,
|
|
1504
|
+
ci_gate_status: op.ci_gate_status,
|
|
1505
|
+
allowance_effect: op.allowance_effect,
|
|
1506
|
+
preserved_attempts: op.preserved_attempts,
|
|
1507
|
+
preserved_ci_infra_rerun_attempts_used: op.preserved_ci_infra_rerun_attempts_used,
|
|
1508
|
+
preserved_entry_attempts: op.preserved_entry_attempts,
|
|
1509
|
+
deadline_at: op.deadline_at,
|
|
1510
|
+
deadline_source: op.deadline_source,
|
|
1511
|
+
deadline_extended: op.deadline_extended,
|
|
1512
|
+
jobs_created: op.jobs_created,
|
|
1513
|
+
ci_passed_by_action: op.ci_passed_by_action,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function allowanceLine(op) {
|
|
1517
|
+
const infra = op.preserved_ci_infra_rerun_attempts_used ?? 0;
|
|
1518
|
+
const entry = op.preserved_entry_attempts ?? 0;
|
|
1519
|
+
return (`Preserved allowances: repair attempts spent ${op.preserved_attempts}, ` +
|
|
1520
|
+
`infrastructure retries used ${infra}, entry attempts ${entry}.`);
|
|
1521
|
+
}
|
|
1522
|
+
function deadlineLine(op) {
|
|
1523
|
+
if (op.deadline_at === null)
|
|
1524
|
+
return "Deadline unchanged (not recorded).";
|
|
1525
|
+
const source = op.deadline_source === "post_repair" ? "post-repair window" : "original gate window";
|
|
1526
|
+
return `Deadline unchanged: ${op.deadline_at} (${source}).`;
|
|
1527
|
+
}
|
|
1528
|
+
function currentStateLine(current) {
|
|
1529
|
+
return (`Current state (read now, separate from the history above): ticket ${current.ticket_status ?? "unknown"}, ` +
|
|
1530
|
+
`gate ${current.current_gate ?? "unknown"}, CI gate ${current.ci_gate_status ?? "unknown"}, ` +
|
|
1531
|
+
`held by run control: ${current.held_by_run_control ? "yes" : "no"}.`);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Exported for tests: render a `resume-ci` result.
|
|
1535
|
+
*
|
|
1536
|
+
* The outcome word leads the first line. Committed and replayed results go to
|
|
1537
|
+
* stdout with exit 0; refused, unconfirmed, and failed outcomes go to stderr with
|
|
1538
|
+
* their own non-zero codes. No operation key, stop binding, row version, SHA,
|
|
1539
|
+
* credential, provider error, or URL is ever printed, in either mode.
|
|
1540
|
+
*/
|
|
1541
|
+
export function renderResumeCiResult(result, target, json) {
|
|
1542
|
+
const label = `ticket ${target.ticketKey} (run ${target.epicRunId})`;
|
|
1543
|
+
if (result.kind === "committed" || result.kind === "replayed") {
|
|
1544
|
+
const r = result.result;
|
|
1545
|
+
if (json) {
|
|
1546
|
+
console.log(JSON.stringify({
|
|
1547
|
+
ok: true,
|
|
1548
|
+
outcome: result.kind,
|
|
1549
|
+
historical: result.kind === "replayed",
|
|
1550
|
+
epic_run_id: r.epic_run_id,
|
|
1551
|
+
ticket_key: r.ticket_key,
|
|
1552
|
+
operation: resumeCiOperationSummary(r.operation),
|
|
1553
|
+
current: r.current,
|
|
1554
|
+
audit_recorded: r.audit_recorded,
|
|
1555
|
+
audit_warning: r.audit_warning,
|
|
1556
|
+
reference_reused: result.reusedReference,
|
|
1557
|
+
}));
|
|
1558
|
+
return RESUME_CI_EXIT_OK;
|
|
1559
|
+
}
|
|
1560
|
+
const reason = RESUME_CI_STOP_REASON_TEXT[r.operation.stop_reason] ?? "a CI stop";
|
|
1561
|
+
if (result.kind === "committed") {
|
|
1562
|
+
console.log(`Committed: resume-ci for ${label}, which was parked on ${reason}.`);
|
|
1563
|
+
console.log(RESUME_CI_CONSEQUENCE);
|
|
1564
|
+
console.log(allowanceLine(r.operation));
|
|
1565
|
+
console.log(deadlineLine(r.operation));
|
|
1566
|
+
console.log("Next: the reconciler's next identical green poll confirms CI; review and merge checks still apply.");
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
console.log(`Replayed historical result: resume-ci for ${label} committed earlier, at ` +
|
|
1570
|
+
`${r.operation.resumed_at}. Nothing was changed by this call.`);
|
|
1571
|
+
console.log(`History: the ticket was parked on ${reason} and was returned to CI evaluation.`);
|
|
1572
|
+
console.log(allowanceLine(r.operation));
|
|
1573
|
+
console.log(deadlineLine(r.operation));
|
|
1574
|
+
console.log(currentStateLine(r.current));
|
|
1575
|
+
console.log("This history does not show that CI is green now, or that review or merge may proceed.");
|
|
1576
|
+
}
|
|
1577
|
+
if (!r.audit_recorded && r.audit_warning) {
|
|
1578
|
+
console.log(`Warning: ${r.audit_warning}`);
|
|
1579
|
+
}
|
|
1580
|
+
return RESUME_CI_EXIT_OK;
|
|
1581
|
+
}
|
|
1582
|
+
if (result.kind === "refused" || result.kind === "not_eligible" || result.kind === "stale_reference") {
|
|
1583
|
+
const code = result.kind === "refused"
|
|
1584
|
+
? result.errorCode
|
|
1585
|
+
: result.kind === "stale_reference"
|
|
1586
|
+
? "CI_RESUME_STALE_OPERATION"
|
|
1587
|
+
: "CI_STOP_NOT_ELIGIBLE";
|
|
1588
|
+
const sentence = result.kind === "not_eligible"
|
|
1589
|
+
? RESUME_CI_NOT_ELIGIBLE_TEXT[result.reason] ?? "resume-ci does not apply"
|
|
1590
|
+
: RESUME_CI_REFUSAL_SENTENCES[code] ?? "the server refused the request";
|
|
1591
|
+
const guidance = RESUME_CI_REFUSAL_GUIDANCE[code];
|
|
1592
|
+
if (json) {
|
|
1593
|
+
console.error(JSON.stringify({
|
|
1594
|
+
ok: false,
|
|
1595
|
+
outcome: "refused",
|
|
1596
|
+
refusal_code: code,
|
|
1597
|
+
...(result.kind === "not_eligible" ? { local_reason: result.reason } : {}),
|
|
1598
|
+
message: sentence,
|
|
1599
|
+
...(guidance ? { guidance } : {}),
|
|
1600
|
+
nothing_written: true,
|
|
1601
|
+
}));
|
|
1602
|
+
return RESUME_CI_EXIT_REFUSED;
|
|
1603
|
+
}
|
|
1604
|
+
console.error(`Refused: resume-ci for ${label} — ${sentence}. Nothing was written.`);
|
|
1605
|
+
if (guidance)
|
|
1606
|
+
console.error(guidance);
|
|
1607
|
+
return RESUME_CI_EXIT_REFUSED;
|
|
1608
|
+
}
|
|
1609
|
+
if (result.kind === "unconfirmed") {
|
|
1610
|
+
if (json) {
|
|
1611
|
+
console.error(JSON.stringify({
|
|
1612
|
+
ok: false,
|
|
1613
|
+
outcome: "unconfirmed",
|
|
1614
|
+
transport: result.transport,
|
|
1615
|
+
attempts: result.attempts,
|
|
1616
|
+
}));
|
|
1617
|
+
return RESUME_CI_EXIT_UNCONFIRMED;
|
|
1618
|
+
}
|
|
1619
|
+
console.error(`Unconfirmed: resume-ci for ${label} — no durable answer arrived after ${result.attempts} ` +
|
|
1620
|
+
`attempts (${result.transport}). The resume may or may not have committed.`);
|
|
1621
|
+
console.error("Re-run the same command later: it reuses this same operation and reports its original " +
|
|
1622
|
+
"result. Do not use unpark or another recovery in the meantime.");
|
|
1623
|
+
return RESUME_CI_EXIT_UNCONFIRMED;
|
|
1624
|
+
}
|
|
1625
|
+
const message = result.kind === "unavailable" ? result.message : "the outcome could not be read";
|
|
1626
|
+
if (json) {
|
|
1627
|
+
console.error(JSON.stringify({ ok: false, outcome: "failed", message }));
|
|
1628
|
+
}
|
|
1629
|
+
else {
|
|
1630
|
+
console.error(`Failed: resume-ci for ${label} — ${message}.`);
|
|
1631
|
+
}
|
|
1632
|
+
return RESUME_CI_EXIT_FAILED;
|
|
1633
|
+
}
|
|
1634
|
+
export async function runResumeCiCommand(argv) {
|
|
1635
|
+
try {
|
|
1636
|
+
const parsed = parseResumeCiArgs(argv);
|
|
1637
|
+
if (parsed.help) {
|
|
1638
|
+
console.log(RESUME_CI_USAGE);
|
|
1639
|
+
return 0;
|
|
1640
|
+
}
|
|
1641
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
1642
|
+
if (!access)
|
|
1643
|
+
return RESUME_CI_EXIT_FAILED;
|
|
1644
|
+
const { resumeCiOnce } = await import("./recovery-operations.js");
|
|
1645
|
+
const result = await resumeCiOnce(access, {
|
|
1646
|
+
epicRunId: parsed.epicRunId,
|
|
1647
|
+
ticketKey: parsed.ticketKey,
|
|
1648
|
+
});
|
|
1649
|
+
return renderResumeCiResult(result, parsed, parsed.json);
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
1653
|
+
return RESUME_CI_EXIT_FAILED;
|
|
1654
|
+
}
|
|
1655
|
+
}
|