@bridge_gpt/mcp-server 0.2.65 → 0.2.67
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 +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- 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 +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- 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/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `conductor` operator recovery and repair verbs —
|
|
2
|
+
* `conductor` operator recovery and repair verbs — fourteen CLI-only commands.
|
|
3
3
|
*
|
|
4
4
|
* The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
|
|
5
5
|
* `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
|
|
6
6
|
* `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
|
|
7
7
|
* `scope-catch-up`, `patch-policy`, `resume-run`. BAPI-1300's parked-child
|
|
8
8
|
* continuation pair: `continue-child` and the read-only `continuation-receipt`.
|
|
9
|
+
* BAPI-1326's allowance-preserving CI resume: `resume-ci`. BAPI-1388's guided
|
|
10
|
+
* provider re-run of failed CI workflow runs: `rerun-ci`.
|
|
9
11
|
*
|
|
10
12
|
* CLI-only by design — this is an explicit ticket non-goal, not an oversight:
|
|
11
|
-
* none of these
|
|
13
|
+
* none of these fourteen command names is ever registered as an MCP tool, tool
|
|
12
14
|
* description, or generated tool-schema input (`index-static.test.ts` guards
|
|
13
15
|
* this). They exist so an operator (or `plane down`, via
|
|
14
16
|
* `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
import { readFile } from "node:fs/promises";
|
|
26
28
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
27
29
|
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES, RESUME_RUN_UNCONFIRMED_MESSAGE, } from "./repair-contract.js";
|
|
30
|
+
import { decodeGuidedBinding, GuidedBindingError, } from "./guided-repair.js";
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
// Flag parsing
|
|
30
33
|
// ---------------------------------------------------------------------------
|
|
@@ -378,13 +381,26 @@ export async function runAdoptCurrentHeadAndUnparkCommand(argv) {
|
|
|
378
381
|
// ---------------------------------------------------------------------------
|
|
379
382
|
// BAPI-1154 — repair verbs: scope-reparse, dispatch-remediation, merge-child
|
|
380
383
|
// ---------------------------------------------------------------------------
|
|
381
|
-
|
|
384
|
+
// BAPI-1333 — `--binding` is the ONE guided-repair input: the opaque token
|
|
385
|
+
// ExplainRun rendered, carrying the subject the command is pinned to. It is not
|
|
386
|
+
// a version counter the operator composes, so REJECTED_VERSION_FLAGS is unchanged.
|
|
387
|
+
const SCOPE_REPARSE_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id", "--binding"]);
|
|
382
388
|
const DISPATCH_REMEDIATION_VALUE_FLAGS = new Set([
|
|
383
389
|
"--epic-run-id",
|
|
384
390
|
"--ticket-key",
|
|
385
391
|
"--context-file",
|
|
392
|
+
"--binding",
|
|
386
393
|
]);
|
|
387
394
|
const MERGE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--pr"]);
|
|
395
|
+
/**
|
|
396
|
+
* BAPI-1333 — attach `binding` ONLY when `--binding` was given, so an unguided
|
|
397
|
+
* parse result is exactly what it was before.
|
|
398
|
+
*/
|
|
399
|
+
function withBinding(parsed, values) {
|
|
400
|
+
if (!values.has("--binding"))
|
|
401
|
+
return parsed;
|
|
402
|
+
return { ...parsed, binding: requireFlag(values, "--binding") };
|
|
403
|
+
}
|
|
388
404
|
function requireFlag(values, flag) {
|
|
389
405
|
const value = values.get(flag);
|
|
390
406
|
if (value === undefined || value.trim().length === 0) {
|
|
@@ -397,25 +413,25 @@ export function parseScopeReparseArgs(argv) {
|
|
|
397
413
|
if (bools.has("--help")) {
|
|
398
414
|
return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
|
|
399
415
|
}
|
|
400
|
-
return {
|
|
416
|
+
return withBinding({
|
|
401
417
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
402
418
|
scopeId: requireFlag(values, "--scope-id"),
|
|
403
419
|
json: bools.has("--json"),
|
|
404
420
|
help: false,
|
|
405
|
-
};
|
|
421
|
+
}, values);
|
|
406
422
|
}
|
|
407
423
|
export function parseDispatchRemediationArgs(argv) {
|
|
408
424
|
const { values, bools } = tokenizeRecoveryFlags(argv, DISPATCH_REMEDIATION_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
409
425
|
if (bools.has("--help")) {
|
|
410
426
|
return { epicRunId: "", ticketKey: "", contextFile: "", json: bools.has("--json"), help: true };
|
|
411
427
|
}
|
|
412
|
-
return {
|
|
428
|
+
return withBinding({
|
|
413
429
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
414
430
|
ticketKey: requireFlag(values, "--ticket-key"),
|
|
415
431
|
contextFile: requireFlag(values, "--context-file"),
|
|
416
432
|
json: bools.has("--json"),
|
|
417
433
|
help: false,
|
|
418
|
-
};
|
|
434
|
+
}, values);
|
|
419
435
|
}
|
|
420
436
|
export function parseMergeChildArgs(argv) {
|
|
421
437
|
const { values, bools } = tokenizeRecoveryFlags(argv, MERGE_CHILD_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
@@ -457,6 +473,124 @@ export function decodeDriverContextFile(bytes) {
|
|
|
457
473
|
}
|
|
458
474
|
return text;
|
|
459
475
|
}
|
|
476
|
+
/** One repair result, however it ended, as the renderer needs it. */
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// BAPI-1333 — guided (binding-pinned) repairs
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
/** Exit codes shared with `resume-ci`: 0 applied/no-op, 2 refused/stale, 3 unconfirmed. */
|
|
481
|
+
export const GUIDED_REPAIR_EXIT_OK = 0;
|
|
482
|
+
export const GUIDED_REPAIR_EXIT_FAILED = 1;
|
|
483
|
+
export const GUIDED_REPAIR_EXIT_REFUSED = 2;
|
|
484
|
+
export const GUIDED_REPAIR_EXIT_UNCONFIRMED = 3;
|
|
485
|
+
export const GUIDED_REPAIR_UNCONFIRMED_MESSAGE = "the outcome could not be confirmed: the server may or may not have applied this " +
|
|
486
|
+
"guided repair. Re-run the SAME command to re-send the same pinned request, or " +
|
|
487
|
+
"inspect the run read-only";
|
|
488
|
+
const GUIDED_STALE_GUIDANCE = "Nothing was written. The run changed since this command was rendered (possibly " +
|
|
489
|
+
"by this command's own earlier delivery). Re-read the run explanation before " +
|
|
490
|
+
"choosing another action.";
|
|
491
|
+
/**
|
|
492
|
+
* Decode `--binding` and prove it names THIS command, run, and target. Runs
|
|
493
|
+
* before credentials are resolved, so a mismatched token reaches no one.
|
|
494
|
+
*/
|
|
495
|
+
function checkGuidedBinding(token, verb, expected) {
|
|
496
|
+
let binding;
|
|
497
|
+
try {
|
|
498
|
+
binding = decodeGuidedBinding(token);
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
throw new ConductorValidationError(err instanceof GuidedBindingError ? err.message : "--binding could not be decoded; nothing was sent.");
|
|
502
|
+
}
|
|
503
|
+
if (binding.verb !== verb ||
|
|
504
|
+
binding.epicRunId !== expected.epicRunId ||
|
|
505
|
+
(expected.scopeId !== undefined && binding.scopeId !== expected.scopeId) ||
|
|
506
|
+
(expected.ticketKey !== undefined && binding.ticketKey !== expected.ticketKey)) {
|
|
507
|
+
throw new ConductorValidationError("--binding was rendered for a different command, run, or target; nothing was sent.");
|
|
508
|
+
}
|
|
509
|
+
return binding;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Send one guided repair and render its closed outcome. Success goes to stdout;
|
|
513
|
+
* every failure goes to stderr (JSON mode included), with the exit codes above.
|
|
514
|
+
*/
|
|
515
|
+
async function runGuidedRepairCommand(input, json) {
|
|
516
|
+
const { guidedRepairOnce } = await import("./recovery-operations.js");
|
|
517
|
+
const { guidedPayloadDigest } = await import("./guided-repair.js");
|
|
518
|
+
const outcome = await guidedRepairOnce({
|
|
519
|
+
binding: input.binding,
|
|
520
|
+
payloadDigest: guidedPayloadDigest(input.payload),
|
|
521
|
+
send: input.send,
|
|
522
|
+
unconfirmedMessage: GUIDED_REPAIR_UNCONFIRMED_MESSAGE,
|
|
523
|
+
});
|
|
524
|
+
const verb = input.binding.verb;
|
|
525
|
+
const runId = input.binding.epicRunId;
|
|
526
|
+
if (outcome.kind === "committed" || outcome.kind === "no_op") {
|
|
527
|
+
const result = outcome.result;
|
|
528
|
+
if (outcome.kind === "committed" && !result.event_recorded) {
|
|
529
|
+
console.error(`Warning: ${verb} on run ${runId} (${input.label}) COMMITTED, but its ` +
|
|
530
|
+
"operator_verb_applied audit event was not recorded. The action stands.");
|
|
531
|
+
}
|
|
532
|
+
if (json) {
|
|
533
|
+
console.log(JSON.stringify({
|
|
534
|
+
ok: true,
|
|
535
|
+
outcome: outcome.kind,
|
|
536
|
+
request_id: outcome.requestId,
|
|
537
|
+
reference_reused: outcome.referenceReused,
|
|
538
|
+
result,
|
|
539
|
+
}));
|
|
540
|
+
return GUIDED_REPAIR_EXIT_OK;
|
|
541
|
+
}
|
|
542
|
+
console.log([
|
|
543
|
+
`${verb} (guided) — run ${runId}`,
|
|
544
|
+
` target: ${input.label}`,
|
|
545
|
+
` ${input.stateLabel}: ${result.before ?? "none"} → ${result.after ?? "none"}`,
|
|
546
|
+
` outcome: ${outcome.kind === "committed" ? "committed" : "no-op (already in effect; nothing written)"}`,
|
|
547
|
+
` request: ${outcome.requestId}${outcome.referenceReused ? " (re-sent from the local reference)" : ""}`,
|
|
548
|
+
` event recorded: ${result.event_recorded}`,
|
|
549
|
+
].join("\n"));
|
|
550
|
+
return GUIDED_REPAIR_EXIT_OK;
|
|
551
|
+
}
|
|
552
|
+
if (outcome.kind === "stale" || outcome.kind === "refused") {
|
|
553
|
+
const payload = {
|
|
554
|
+
ok: false,
|
|
555
|
+
outcome: outcome.kind,
|
|
556
|
+
code: outcome.code,
|
|
557
|
+
message: outcome.message,
|
|
558
|
+
request_id: outcome.requestId,
|
|
559
|
+
nothing_written: true,
|
|
560
|
+
};
|
|
561
|
+
if (json) {
|
|
562
|
+
console.error(JSON.stringify(payload));
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
console.error(`Error: ${verb} ${outcome.kind === "stale" ? "is stale" : "refused"} on run ${runId} ` +
|
|
566
|
+
`(${input.label}) — ${outcome.code}: ${outcome.message}` +
|
|
567
|
+
(outcome.kind === "stale" ? ` ${GUIDED_STALE_GUIDANCE}` : ""));
|
|
568
|
+
}
|
|
569
|
+
return GUIDED_REPAIR_EXIT_REFUSED;
|
|
570
|
+
}
|
|
571
|
+
if (outcome.kind === "unconfirmed") {
|
|
572
|
+
if (json) {
|
|
573
|
+
console.error(JSON.stringify({
|
|
574
|
+
ok: false,
|
|
575
|
+
outcome: "unconfirmed",
|
|
576
|
+
message: outcome.message,
|
|
577
|
+
request_id: outcome.requestId,
|
|
578
|
+
reference_kept: true,
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
console.error(`Error: ${verb} on run ${runId} (${input.label}) — ${outcome.message}.`);
|
|
583
|
+
}
|
|
584
|
+
return GUIDED_REPAIR_EXIT_UNCONFIRMED;
|
|
585
|
+
}
|
|
586
|
+
if (json) {
|
|
587
|
+
console.error(JSON.stringify({ ok: false, outcome: "unavailable", message: outcome.message }));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
console.error(`Error: ${verb} on run ${runId} (${input.label}) — ${outcome.message}.`);
|
|
591
|
+
}
|
|
592
|
+
return GUIDED_REPAIR_EXIT_FAILED;
|
|
593
|
+
}
|
|
460
594
|
/**
|
|
461
595
|
* Render a repair outcome. Human mode prints the verb and run first, the target
|
|
462
596
|
* second, ONE labeled `Before → After` pair third, then whether this call
|
|
@@ -521,6 +655,12 @@ const SCOPE_REPARSE_USAGE = [
|
|
|
521
655
|
"schedule its parse. The scope must be the one bound to the named run, or the",
|
|
522
656
|
"server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints the scope",
|
|
523
657
|
"lifecycle before → after; it does not wait for the parse to finish.",
|
|
658
|
+
"",
|
|
659
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
660
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
661
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
662
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
663
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
524
664
|
].join("\n");
|
|
525
665
|
export async function runScopeReparseCommand(argv) {
|
|
526
666
|
try {
|
|
@@ -529,9 +669,25 @@ export async function runScopeReparseCommand(argv) {
|
|
|
529
669
|
console.log(SCOPE_REPARSE_USAGE);
|
|
530
670
|
return 0;
|
|
531
671
|
}
|
|
672
|
+
const binding = parsed.binding === undefined
|
|
673
|
+
? null
|
|
674
|
+
: checkGuidedBinding(parsed.binding, "scope-reparse", {
|
|
675
|
+
epicRunId: parsed.epicRunId,
|
|
676
|
+
scopeId: parsed.scopeId,
|
|
677
|
+
});
|
|
532
678
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
533
679
|
if (!access)
|
|
534
680
|
return 1;
|
|
681
|
+
if (binding !== null) {
|
|
682
|
+
const { scopeReparse } = await import("./bridge-api-client.js");
|
|
683
|
+
return await runGuidedRepairCommand({
|
|
684
|
+
binding,
|
|
685
|
+
payload: { scope_id: parsed.scopeId },
|
|
686
|
+
send: (guard) => scopeReparse(access, { epicRunId: parsed.epicRunId, scopeId: parsed.scopeId, guard }),
|
|
687
|
+
label: `scope ${parsed.scopeId}`,
|
|
688
|
+
stateLabel: "scope lifecycle",
|
|
689
|
+
}, parsed.json);
|
|
690
|
+
}
|
|
535
691
|
const { scopeReparseRepair } = await import("./recovery-operations.js");
|
|
536
692
|
const outcome = await scopeReparseRepair(access, {
|
|
537
693
|
epicRunId: parsed.epicRunId,
|
|
@@ -554,15 +710,23 @@ export async function runScopeReparseCommand(argv) {
|
|
|
554
710
|
const DISPATCH_REMEDIATION_USAGE = [
|
|
555
711
|
"Usage: epic-implementer dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path> [--json]",
|
|
556
712
|
"",
|
|
557
|
-
"Dispatch a remediation job for a ticket at code_review (or parked there),",
|
|
713
|
+
"Dispatch a remediation job for a ticket at code_review or ci (or parked there),",
|
|
558
714
|
"carrying your driver context to the worker. The SERVER chooses the job type",
|
|
559
|
-
"(remediate, or reconcile_overlap for a conflict/overlap ticket
|
|
715
|
+
"(remediate, or reconcile_overlap for a conflict/overlap ticket; ci_fix for a CI",
|
|
716
|
+
"failure the server's FRESH CI read routes to investigation) and admits it",
|
|
560
717
|
"through the reconciler's own strict-serial lane, remediation budget, and",
|
|
561
|
-
"dedup key. A parked ticket is resumed in the same step.",
|
|
718
|
+
"dedup key. A parked ticket is resumed in the same step (unguided only). An",
|
|
719
|
+
"infrastructure CI failure is refused by name and points to rerun-ci.",
|
|
562
720
|
"",
|
|
563
721
|
`--context-file must be non-empty UTF-8 text of at most DRIVER_CONTEXT_MAX_BYTES`,
|
|
564
722
|
`(${DRIVER_CONTEXT_MAX_BYTES} bytes). It is rejected, never truncated, and is never`,
|
|
565
723
|
"printed, logged, or echoed — it reaches only the worker's prompt.",
|
|
724
|
+
"",
|
|
725
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
726
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
727
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
728
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
729
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
566
730
|
].join("\n");
|
|
567
731
|
export async function runDispatchRemediationCommand(argv, deps = {}) {
|
|
568
732
|
try {
|
|
@@ -581,9 +745,31 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
|
|
|
581
745
|
throw new ConductorValidationError("--context-file could not be read; nothing was sent.");
|
|
582
746
|
}
|
|
583
747
|
const context = decodeDriverContextFile(bytes);
|
|
748
|
+
const binding = parsed.binding === undefined
|
|
749
|
+
? null
|
|
750
|
+
: checkGuidedBinding(parsed.binding, "dispatch-remediation", {
|
|
751
|
+
epicRunId: parsed.epicRunId,
|
|
752
|
+
ticketKey: parsed.ticketKey,
|
|
753
|
+
});
|
|
584
754
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
585
755
|
if (!access)
|
|
586
756
|
return 1;
|
|
757
|
+
if (binding !== null) {
|
|
758
|
+
const { dispatchRemediation } = await import("./bridge-api-client.js");
|
|
759
|
+
return await runGuidedRepairCommand({
|
|
760
|
+
binding,
|
|
761
|
+
// Digested, never stored: the context stays out of the reference file.
|
|
762
|
+
payload: { ticket_key: parsed.ticketKey, context: context },
|
|
763
|
+
send: (guard) => dispatchRemediation(access, {
|
|
764
|
+
epicRunId: parsed.epicRunId,
|
|
765
|
+
ticketKey: parsed.ticketKey,
|
|
766
|
+
context,
|
|
767
|
+
guard,
|
|
768
|
+
}),
|
|
769
|
+
label: `ticket ${parsed.ticketKey}`,
|
|
770
|
+
stateLabel: "selected gate",
|
|
771
|
+
}, parsed.json);
|
|
772
|
+
}
|
|
587
773
|
const { dispatchRemediationRepair } = await import("./recovery-operations.js");
|
|
588
774
|
const outcome = await dispatchRemediationRepair(access, {
|
|
589
775
|
epicRunId: parsed.epicRunId,
|
|
@@ -594,7 +780,7 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
|
|
|
594
780
|
verb: "dispatch-remediation",
|
|
595
781
|
epicRunId: parsed.epicRunId,
|
|
596
782
|
targetLabel: `ticket ${parsed.ticketKey}`,
|
|
597
|
-
stateLabel: "
|
|
783
|
+
stateLabel: "selected gate",
|
|
598
784
|
}, parsed.json, (result) => [
|
|
599
785
|
` job: ${result.job_type || "unknown"}${result.job_id !== null ? ` (job ${result.job_id})` : ""}`,
|
|
600
786
|
]);
|
|
@@ -604,6 +790,195 @@ export async function runDispatchRemediationCommand(argv, deps = {}) {
|
|
|
604
790
|
return 1;
|
|
605
791
|
}
|
|
606
792
|
}
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
// BAPI-1388 — rerun-ci
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
const RERUN_CI_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--binding"]);
|
|
797
|
+
/** Exit codes: 0 applied (including an all-not-re-runnable no-op), 2 refused/stale, 3 unconfirmed, 1 other. */
|
|
798
|
+
export const RERUN_CI_EXIT_OK = 0;
|
|
799
|
+
export const RERUN_CI_EXIT_FAILED = 1;
|
|
800
|
+
export const RERUN_CI_EXIT_REFUSED = 2;
|
|
801
|
+
export const RERUN_CI_EXIT_UNCONFIRMED = 3;
|
|
802
|
+
const RERUN_CI_USAGE = [
|
|
803
|
+
"Usage: epic-implementer rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
|
|
804
|
+
"",
|
|
805
|
+
"Ask the CI provider to re-run a ticket's FAILED required checks at its bound head,",
|
|
806
|
+
"for a ticket at ci or parked on it. Copy the command exactly as the run",
|
|
807
|
+
"explanation rendered it: --binding pins the ticket version and head it showed.",
|
|
808
|
+
"The SERVER re-reads fresh CI evidence, re-runs each GitHub Actions workflow run at",
|
|
809
|
+
"most once per head, and reports every failed check as enqueued, deduplicated,",
|
|
810
|
+
"deferred, or not_re_runnable.",
|
|
811
|
+
"",
|
|
812
|
+
"rerun-ci NEVER re-runs the claude-review workflow. It charges no allowance and",
|
|
813
|
+
"changes no park state, and a re-run is not a verdict: when CI is green, use",
|
|
814
|
+
"`epic-implementer resume-ci` (a parked ticket) or let the reconciler observe it.",
|
|
815
|
+
"If the failure is a code defect, use `dispatch-remediation --context-file`.",
|
|
816
|
+
"",
|
|
817
|
+
"The request is stored locally before it is sent, so re-running the same command",
|
|
818
|
+
"after a lost response re-sends the SAME request. A subject that has since changed",
|
|
819
|
+
"is refused as stale with nothing written.",
|
|
820
|
+
"",
|
|
821
|
+
"Exit codes: 0 applied (including a no-op when nothing could be re-run); 2 refused",
|
|
822
|
+
"or stale; 3 unconfirmed; 1 other failure (including a server without this verb).",
|
|
823
|
+
].join("\n");
|
|
824
|
+
export function parseRerunCiArgs(argv) {
|
|
825
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, RERUN_CI_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
826
|
+
if (bools.has("--help")) {
|
|
827
|
+
return { epicRunId: "", ticketKey: "", binding: "", json: bools.has("--json"), help: true };
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
831
|
+
ticketKey: requireFlag(values, "--ticket-key"),
|
|
832
|
+
binding: requireFlag(values, "--binding"),
|
|
833
|
+
json: bools.has("--json"),
|
|
834
|
+
help: false,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
const RERUN_CI_STATUS_TEXT = {
|
|
838
|
+
enqueued: "re-run requested",
|
|
839
|
+
deduplicated: "already requested",
|
|
840
|
+
deferred: "deferred (not requested; run the verb again after a fresh explain-run)",
|
|
841
|
+
not_re_runnable: "not re-runnable",
|
|
842
|
+
};
|
|
843
|
+
const RERUN_CI_REASON_TEXT = {
|
|
844
|
+
not_github_actions: "not a GitHub Actions check",
|
|
845
|
+
no_workflow_run_id: "no workflow run id",
|
|
846
|
+
claude_review_excluded: "claude-review is never re-run",
|
|
847
|
+
};
|
|
848
|
+
const RERUN_CI_OUTCOME_TEXT = {
|
|
849
|
+
enqueued: "re-runs requested",
|
|
850
|
+
partial: "partially requested (some runs deferred)",
|
|
851
|
+
nothing_to_enqueue: "nothing to enqueue (no failed check could be re-run)",
|
|
852
|
+
};
|
|
853
|
+
const RERUN_CI_NEXT_ACTION = {
|
|
854
|
+
enqueued: "Wait for the re-runs to finish, then read `epic-implementer explain-run`. When CI is " +
|
|
855
|
+
"green use `epic-implementer resume-ci` (a parked ticket); this verb did not mark CI passed.",
|
|
856
|
+
partial: "Some runs were not requested. Read `epic-implementer explain-run` and, if still " +
|
|
857
|
+
"offered, re-run the command it renders with its fresh binding.",
|
|
858
|
+
nothing_to_enqueue: "No re-run was possible. Read `epic-implementer explain-run`; for a code defect use " +
|
|
859
|
+
"`dispatch-remediation --context-file`.",
|
|
860
|
+
};
|
|
861
|
+
function rerunCiCheckLine(check) {
|
|
862
|
+
const identity = [
|
|
863
|
+
check.name ?? "(unnamed check)",
|
|
864
|
+
check.workflow_identity ? `workflow ${check.workflow_identity}` : null,
|
|
865
|
+
check.run_id !== null ? `run ${check.run_id}` : null,
|
|
866
|
+
]
|
|
867
|
+
.filter((part) => part !== null)
|
|
868
|
+
.join(", ");
|
|
869
|
+
const reason = check.reason ? ` — ${RERUN_CI_REASON_TEXT[check.reason] ?? check.reason}` : "";
|
|
870
|
+
const job = check.job_id !== null ? ` (job ${check.job_id})` : "";
|
|
871
|
+
return ` - ${identity}: ${RERUN_CI_STATUS_TEXT[check.status] ?? check.status}${reason}${job}`;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Exported for tests: render one `rerun-ci` outcome. Success goes to stdout and
|
|
875
|
+
* every failure to stderr (`--json` included). An unsupported server is NEVER a
|
|
876
|
+
* success and says it is "not available on this server".
|
|
877
|
+
*/
|
|
878
|
+
export function renderRerunCiOutcome(outcome, parsed, json) {
|
|
879
|
+
const label = `ticket ${parsed.ticketKey} (run ${parsed.epicRunId})`;
|
|
880
|
+
if (outcome.kind === "applied") {
|
|
881
|
+
const result = outcome.result;
|
|
882
|
+
if (result.committed && !result.event_recorded) {
|
|
883
|
+
console.error(`Warning: rerun-ci for ${label} COMMITTED, but its operator_verb_applied audit ` +
|
|
884
|
+
"event was not recorded. The action stands.");
|
|
885
|
+
}
|
|
886
|
+
if (json) {
|
|
887
|
+
console.log(JSON.stringify({
|
|
888
|
+
ok: true,
|
|
889
|
+
outcome: "applied",
|
|
890
|
+
request_id: outcome.requestId,
|
|
891
|
+
reference_reused: outcome.referenceReused,
|
|
892
|
+
result,
|
|
893
|
+
}));
|
|
894
|
+
return RERUN_CI_EXIT_OK;
|
|
895
|
+
}
|
|
896
|
+
const lines = [
|
|
897
|
+
`rerun-ci (guided) — ${label}`,
|
|
898
|
+
` ticket: ${result.ticket_key}`,
|
|
899
|
+
` head: ${result.head_sha ? "`" + result.head_sha + "`" : "unknown"}`,
|
|
900
|
+
` outcome: ${RERUN_CI_OUTCOME_TEXT[result.outcome] ?? result.outcome}`,
|
|
901
|
+
" failed checks:",
|
|
902
|
+
...result.checks.map(rerunCiCheckLine),
|
|
903
|
+
` next action: ${RERUN_CI_NEXT_ACTION[result.outcome] ?? "Read `epic-implementer explain-run`."}`,
|
|
904
|
+
` request: ${outcome.requestId}${outcome.referenceReused ? " (re-sent from the local reference)" : ""}`,
|
|
905
|
+
` event recorded: ${result.event_recorded}`,
|
|
906
|
+
];
|
|
907
|
+
console.log(lines.join("\n"));
|
|
908
|
+
return RERUN_CI_EXIT_OK;
|
|
909
|
+
}
|
|
910
|
+
if (outcome.kind === "refused" || outcome.kind === "stale") {
|
|
911
|
+
const payload = {
|
|
912
|
+
ok: false,
|
|
913
|
+
outcome: outcome.kind,
|
|
914
|
+
code: outcome.code,
|
|
915
|
+
message: outcome.message,
|
|
916
|
+
request_id: outcome.requestId,
|
|
917
|
+
nothing_written: true,
|
|
918
|
+
};
|
|
919
|
+
if (json) {
|
|
920
|
+
console.error(JSON.stringify(payload));
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
console.error(`Refused: rerun-ci for ${label} — ${outcome.code}: ${outcome.message}. ` +
|
|
924
|
+
(outcome.kind === "stale"
|
|
925
|
+
? "Nothing was written. The run changed since this command was rendered; re-read the run explanation."
|
|
926
|
+
: "Nothing was written."));
|
|
927
|
+
}
|
|
928
|
+
return RERUN_CI_EXIT_REFUSED;
|
|
929
|
+
}
|
|
930
|
+
if (outcome.kind === "unconfirmed") {
|
|
931
|
+
if (json) {
|
|
932
|
+
console.error(JSON.stringify({
|
|
933
|
+
ok: false,
|
|
934
|
+
outcome: "unconfirmed",
|
|
935
|
+
reason: outcome.reason,
|
|
936
|
+
message: outcome.message,
|
|
937
|
+
request_id: outcome.requestId,
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
console.error(`Unconfirmed: rerun-ci for ${label} — ${outcome.message}.`);
|
|
942
|
+
}
|
|
943
|
+
return RERUN_CI_EXIT_UNCONFIRMED;
|
|
944
|
+
}
|
|
945
|
+
if (json) {
|
|
946
|
+
console.error(JSON.stringify({ ok: false, outcome: outcome.kind, message: outcome.message }));
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
console.error(`Failed: rerun-ci for ${label} — ${outcome.message}.`);
|
|
950
|
+
}
|
|
951
|
+
return RERUN_CI_EXIT_FAILED;
|
|
952
|
+
}
|
|
953
|
+
export async function runRerunCiCommand(argv) {
|
|
954
|
+
try {
|
|
955
|
+
const parsed = parseRerunCiArgs(argv);
|
|
956
|
+
if (parsed.help) {
|
|
957
|
+
console.log(RERUN_CI_USAGE);
|
|
958
|
+
return 0;
|
|
959
|
+
}
|
|
960
|
+
// Prove the token names THIS command, run, and ticket before credentials are
|
|
961
|
+
// resolved, so a mismatched binding reaches no one.
|
|
962
|
+
const binding = checkGuidedBinding(parsed.binding, "rerun-ci", {
|
|
963
|
+
epicRunId: parsed.epicRunId,
|
|
964
|
+
ticketKey: parsed.ticketKey,
|
|
965
|
+
});
|
|
966
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
967
|
+
if (!access)
|
|
968
|
+
return RERUN_CI_EXIT_FAILED;
|
|
969
|
+
const { rerunCiRepair } = await import("./recovery-operations.js");
|
|
970
|
+
const outcome = await rerunCiRepair(access, {
|
|
971
|
+
epicRunId: parsed.epicRunId,
|
|
972
|
+
ticketKey: parsed.ticketKey,
|
|
973
|
+
binding,
|
|
974
|
+
});
|
|
975
|
+
return renderRerunCiOutcome(outcome, parsed, parsed.json);
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
979
|
+
return RERUN_CI_EXIT_FAILED;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
607
982
|
const MERGE_CHILD_USAGE = [
|
|
608
983
|
"Usage: epic-implementer merge-child --epic-run-id <id> --pr <number> [--json]",
|
|
609
984
|
"",
|
|
@@ -651,9 +1026,9 @@ export async function runMergeChildCommand(argv) {
|
|
|
651
1026
|
// ---------------------------------------------------------------------------
|
|
652
1027
|
// BAPI-1176 — repair verbs: scope-catch-up, patch-policy, resume-run
|
|
653
1028
|
// ---------------------------------------------------------------------------
|
|
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"]);
|
|
1029
|
+
const SCOPE_CATCH_UP_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id", "--binding"]);
|
|
1030
|
+
const PATCH_POLICY_VALUE_FLAGS = new Set(["--epic-run-id", "--patch-file", "--binding"]);
|
|
1031
|
+
const RESUME_RUN_VALUE_FLAGS = new Set(["--epic-run-id", "--binding"]);
|
|
657
1032
|
/** `--clear-quarantine` is a `resume-run` switch only; no other verb accepts it. */
|
|
658
1033
|
const RESUME_RUN_BOOL_FLAGS = new Set(["--json", "--help", "--clear-quarantine"]);
|
|
659
1034
|
export function parseScopeCatchUpArgs(argv) {
|
|
@@ -661,24 +1036,24 @@ export function parseScopeCatchUpArgs(argv) {
|
|
|
661
1036
|
if (bools.has("--help")) {
|
|
662
1037
|
return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
|
|
663
1038
|
}
|
|
664
|
-
return {
|
|
1039
|
+
return withBinding({
|
|
665
1040
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
666
1041
|
scopeId: requireFlag(values, "--scope-id"),
|
|
667
1042
|
json: bools.has("--json"),
|
|
668
1043
|
help: false,
|
|
669
|
-
};
|
|
1044
|
+
}, values);
|
|
670
1045
|
}
|
|
671
1046
|
export function parsePatchPolicyArgs(argv) {
|
|
672
1047
|
const { values, bools } = tokenizeRecoveryFlags(argv, PATCH_POLICY_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
673
1048
|
if (bools.has("--help")) {
|
|
674
1049
|
return { epicRunId: "", patchFile: "", json: bools.has("--json"), help: true };
|
|
675
1050
|
}
|
|
676
|
-
return {
|
|
1051
|
+
return withBinding({
|
|
677
1052
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
678
1053
|
patchFile: requireFlag(values, "--patch-file"),
|
|
679
1054
|
json: bools.has("--json"),
|
|
680
1055
|
help: false,
|
|
681
|
-
};
|
|
1056
|
+
}, values);
|
|
682
1057
|
}
|
|
683
1058
|
export function parseResumeRunArgs(argv) {
|
|
684
1059
|
const { values, bools } = tokenizeRecoveryFlags(argv, RESUME_RUN_VALUE_FLAGS, RESUME_RUN_BOOL_FLAGS);
|
|
@@ -690,12 +1065,12 @@ export function parseResumeRunArgs(argv) {
|
|
|
690
1065
|
help: true,
|
|
691
1066
|
};
|
|
692
1067
|
}
|
|
693
|
-
return {
|
|
1068
|
+
return withBinding({
|
|
694
1069
|
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
695
1070
|
clearQuarantine: bools.has("--clear-quarantine"),
|
|
696
1071
|
json: bools.has("--json"),
|
|
697
1072
|
help: false,
|
|
698
|
-
};
|
|
1073
|
+
}, values);
|
|
699
1074
|
}
|
|
700
1075
|
/**
|
|
701
1076
|
* Validate and decode a `patch-policy` file's BYTES into a merge-patch object.
|
|
@@ -749,6 +1124,12 @@ const SCOPE_CATCH_UP_USAGE = [
|
|
|
749
1124
|
"refused (SCOPE_CATCH_UP_REFUSED). The scope must be the one bound to the named",
|
|
750
1125
|
"run, or the server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints",
|
|
751
1126
|
"the scope lifecycle before → after; it does not wait for a scheduled parse.",
|
|
1127
|
+
"",
|
|
1128
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
1129
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
1130
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
1131
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
1132
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
752
1133
|
].join("\n");
|
|
753
1134
|
export async function runScopeCatchUpCommand(argv) {
|
|
754
1135
|
try {
|
|
@@ -757,9 +1138,25 @@ export async function runScopeCatchUpCommand(argv) {
|
|
|
757
1138
|
console.log(SCOPE_CATCH_UP_USAGE);
|
|
758
1139
|
return 0;
|
|
759
1140
|
}
|
|
1141
|
+
const binding = parsed.binding === undefined
|
|
1142
|
+
? null
|
|
1143
|
+
: checkGuidedBinding(parsed.binding, "scope-catch-up", {
|
|
1144
|
+
epicRunId: parsed.epicRunId,
|
|
1145
|
+
scopeId: parsed.scopeId,
|
|
1146
|
+
});
|
|
760
1147
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
761
1148
|
if (!access)
|
|
762
1149
|
return 1;
|
|
1150
|
+
if (binding !== null) {
|
|
1151
|
+
const { scopeCatchUp } = await import("./bridge-api-client.js");
|
|
1152
|
+
return await runGuidedRepairCommand({
|
|
1153
|
+
binding,
|
|
1154
|
+
payload: { scope_id: parsed.scopeId },
|
|
1155
|
+
send: (guard) => scopeCatchUp(access, { epicRunId: parsed.epicRunId, scopeId: parsed.scopeId, guard }),
|
|
1156
|
+
label: `scope ${parsed.scopeId}`,
|
|
1157
|
+
stateLabel: "scope lifecycle",
|
|
1158
|
+
}, parsed.json);
|
|
1159
|
+
}
|
|
763
1160
|
const { scopeCatchUpRepair } = await import("./recovery-operations.js");
|
|
764
1161
|
const outcome = await scopeCatchUpRepair(access, {
|
|
765
1162
|
epicRunId: parsed.epicRunId,
|
|
@@ -792,6 +1189,12 @@ const PATCH_POLICY_USAGE = [
|
|
|
792
1189
|
"--patch-file must hold a non-empty JSON object of at most POLICY_PATCH_MAX_BYTES",
|
|
793
1190
|
`(${POLICY_PATCH_MAX_BYTES} bytes). Its contents are never printed; the result names only`,
|
|
794
1191
|
"the changed dotted paths and before/after policy digest tokens.",
|
|
1192
|
+
"",
|
|
1193
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
1194
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
1195
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
1196
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
1197
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
795
1198
|
].join("\n");
|
|
796
1199
|
export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
797
1200
|
try {
|
|
@@ -810,9 +1213,23 @@ export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
|
810
1213
|
throw new ConductorValidationError("--patch-file could not be read; nothing was sent.");
|
|
811
1214
|
}
|
|
812
1215
|
const mergePatch = decodePolicyPatchFile(bytes);
|
|
1216
|
+
const binding = parsed.binding === undefined
|
|
1217
|
+
? null
|
|
1218
|
+
: checkGuidedBinding(parsed.binding, "patch-policy", { epicRunId: parsed.epicRunId });
|
|
813
1219
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
814
1220
|
if (!access)
|
|
815
1221
|
return 1;
|
|
1222
|
+
if (binding !== null) {
|
|
1223
|
+
const { patchPolicy } = await import("./bridge-api-client.js");
|
|
1224
|
+
return await runGuidedRepairCommand({
|
|
1225
|
+
binding,
|
|
1226
|
+
// Digested, never stored: no policy value reaches the reference file.
|
|
1227
|
+
payload: { merge_patch: mergePatch },
|
|
1228
|
+
send: (guard) => patchPolicy(access, { epicRunId: parsed.epicRunId, mergePatch, guard }),
|
|
1229
|
+
label: "run policy",
|
|
1230
|
+
stateLabel: "policy digest",
|
|
1231
|
+
}, parsed.json);
|
|
1232
|
+
}
|
|
816
1233
|
const { patchPolicyRepair } = await import("./recovery-operations.js");
|
|
817
1234
|
const outcome = await patchPolicyRepair(access, {
|
|
818
1235
|
epicRunId: parsed.epicRunId,
|
|
@@ -845,6 +1262,12 @@ const RESUME_RUN_USAGE = [
|
|
|
845
1262
|
"",
|
|
846
1263
|
"If the response is lost or cannot be read, the resume may or may not have",
|
|
847
1264
|
"committed: inspect the run read-only before any separately authorized retry.",
|
|
1265
|
+
"",
|
|
1266
|
+
"Guided mode (BAPI-1333): add --binding <token> exactly as the run explanation",
|
|
1267
|
+
"rendered it. The request is pinned to the subject that explanation showed and",
|
|
1268
|
+
"is stored locally before it is sent, so re-running the same command after a",
|
|
1269
|
+
"lost response re-sends the SAME request. A subject that has since changed is",
|
|
1270
|
+
"refused as stale with nothing written (exit 2); an unconfirmed outcome exits 3.",
|
|
848
1271
|
].join("\n");
|
|
849
1272
|
export async function runResumeRunCommand(argv) {
|
|
850
1273
|
try {
|
|
@@ -853,9 +1276,29 @@ export async function runResumeRunCommand(argv) {
|
|
|
853
1276
|
console.log(RESUME_RUN_USAGE);
|
|
854
1277
|
return 0;
|
|
855
1278
|
}
|
|
1279
|
+
const binding = parsed.binding === undefined
|
|
1280
|
+
? null
|
|
1281
|
+
: checkGuidedBinding(parsed.binding, "resume-run", { epicRunId: parsed.epicRunId });
|
|
1282
|
+
if (binding !== null && binding.controlState === "quarantined" && !parsed.clearQuarantine) {
|
|
1283
|
+
throw new ConductorValidationError("This binding names a quarantined run; --clear-quarantine is required. Nothing was sent.");
|
|
1284
|
+
}
|
|
856
1285
|
const access = await resolveRecoveryAccess(parsed.json);
|
|
857
1286
|
if (!access)
|
|
858
1287
|
return 1;
|
|
1288
|
+
if (binding !== null) {
|
|
1289
|
+
const { resumeRun } = await import("./bridge-api-client.js");
|
|
1290
|
+
return await runGuidedRepairCommand({
|
|
1291
|
+
binding,
|
|
1292
|
+
payload: { clear_quarantine: parsed.clearQuarantine },
|
|
1293
|
+
send: (guard) => resumeRun(access, {
|
|
1294
|
+
epicRunId: parsed.epicRunId,
|
|
1295
|
+
clearQuarantine: parsed.clearQuarantine,
|
|
1296
|
+
guard,
|
|
1297
|
+
}),
|
|
1298
|
+
label: parsed.clearQuarantine ? "run (clear quarantine)" : "run",
|
|
1299
|
+
stateLabel: "run state",
|
|
1300
|
+
}, parsed.json);
|
|
1301
|
+
}
|
|
859
1302
|
const { resumeRunRepair } = await import("./recovery-operations.js");
|
|
860
1303
|
const outcome = await resumeRunRepair(access, {
|
|
861
1304
|
epicRunId: parsed.epicRunId,
|
|
@@ -1047,6 +1490,11 @@ export function renderContinueChildResult(result, target, json) {
|
|
|
1047
1490
|
}
|
|
1048
1491
|
console.error(`${label}: not continuable right now. Nothing was sent.`);
|
|
1049
1492
|
console.error(currentLine(result.current));
|
|
1493
|
+
// BAPI-1344: the park's own guidance names the exit that remains (for a
|
|
1494
|
+
// first attempt with no recorded retained head, the run operator).
|
|
1495
|
+
if (result.current.parked && result.current.needs_human_reason) {
|
|
1496
|
+
console.error(`Park: ${result.current.needs_human_reason}`);
|
|
1497
|
+
}
|
|
1050
1498
|
return 1;
|
|
1051
1499
|
}
|
|
1052
1500
|
if (result.kind === "unconfirmed") {
|
|
@@ -1162,3 +1610,238 @@ export async function runContinuationReceiptCommand(argv) {
|
|
|
1162
1610
|
return 1;
|
|
1163
1611
|
}
|
|
1164
1612
|
}
|
|
1613
|
+
// ---------------------------------------------------------------------------
|
|
1614
|
+
// BAPI-1326 — resume-ci
|
|
1615
|
+
// ---------------------------------------------------------------------------
|
|
1616
|
+
const RESUME_CI_USAGE = [
|
|
1617
|
+
"Usage: epic-implementer resume-ci --epic-run-id <id> --ticket-key <key> [--json]",
|
|
1618
|
+
"",
|
|
1619
|
+
"Resume a child parked on a qualified CI stop (an expired CI wait, exhausted",
|
|
1620
|
+
"required-check repairs, or an unavailable verdict) once CI has finished green.",
|
|
1621
|
+
"The server re-reads the pull request, the specification, and the required",
|
|
1622
|
+
"checks, then returns CI to evaluation at the same head. It creates no job,",
|
|
1623
|
+
"grants no repair or new wait, keeps the original deadline, and does not mark",
|
|
1624
|
+
"CI passed or authorize review or merge.",
|
|
1625
|
+
"",
|
|
1626
|
+
"The operation is pinned locally before it is sent; re-running after a lost",
|
|
1627
|
+
"response reuses it and reports the original result. Nothing else is tried.",
|
|
1628
|
+
"",
|
|
1629
|
+
"Exit codes: 0 committed or replayed; 2 refused; 3 unconfirmed; 1 other failure.",
|
|
1630
|
+
].join("\n");
|
|
1631
|
+
/** Distinct exit codes, so automation can tell a refusal from an unknown outcome. */
|
|
1632
|
+
export const RESUME_CI_EXIT_OK = 0;
|
|
1633
|
+
export const RESUME_CI_EXIT_FAILED = 1;
|
|
1634
|
+
export const RESUME_CI_EXIT_REFUSED = 2;
|
|
1635
|
+
export const RESUME_CI_EXIT_UNCONFIRMED = 3;
|
|
1636
|
+
/**
|
|
1637
|
+
* Fixed operator sentences, chosen by the server's structured code. No server,
|
|
1638
|
+
* provider, or exception text ever reaches the terminal.
|
|
1639
|
+
*/
|
|
1640
|
+
const RESUME_CI_REFUSAL_SENTENCES = {
|
|
1641
|
+
TICKET_NOT_IN_RUN: "the ticket is not part of this run",
|
|
1642
|
+
CI_STOP_NOT_ELIGIBLE: "this CI park has no complete qualified stop record, so resume-ci does not apply",
|
|
1643
|
+
CI_RESUME_STALE_OPERATION: "the ticket is no longer on the CI stop this operation was made for, and its result is not retained",
|
|
1644
|
+
CI_RESUME_OPERATION_CONFLICT: "this operation already names a different CI recovery",
|
|
1645
|
+
ROW_VERSION_CONFLICT: "the ticket kept changing while the resume was being applied",
|
|
1646
|
+
RUN_NOT_DISPATCHABLE: "the run is paused, stopped, quarantined, or not active",
|
|
1647
|
+
CI_RESUME_BUDGET_EXHAUSTED: "the run's wall-clock or cost budget is spent, or could not be read",
|
|
1648
|
+
CI_RESUME_HELD: "the ticket is held by a completion, permission, declassification, or stale-specification hold",
|
|
1649
|
+
CI_RESUME_LIVE_WORK: "corrective work is, or may be, live for this ticket",
|
|
1650
|
+
CI_RESUME_SPEC_CHANGED: "the ticket's specification changed or could not be verified",
|
|
1651
|
+
CI_RESUME_POLICY_CHANGED: "the run's required-check policy changed, or is unavailable or unconfigured",
|
|
1652
|
+
CI_RESUME_BINDING_CHANGED: "the pull request, its head, or its target branch changed since the stop",
|
|
1653
|
+
CI_RESUME_CHECKS_NOT_GREEN: "the bound head's required checks are not currently green",
|
|
1654
|
+
CI_RESUME_VERDICT_UNAVAILABLE: "the CI provider's verdict could not be read in time",
|
|
1655
|
+
CI_RESUME_BUSY: "the ticket or run stayed busy for the whole bounded wait",
|
|
1656
|
+
};
|
|
1657
|
+
/** The next step for a refusal, where one is useful. */
|
|
1658
|
+
const RESUME_CI_REFUSAL_GUIDANCE = {
|
|
1659
|
+
CI_STOP_NOT_ELIGIBLE: "The existing explicit path is `epic-implementer unpark`, which RESETS the CI gate's " +
|
|
1660
|
+
"clock and repair allowance. If the park's origin is unclear, ask the run operator " +
|
|
1661
|
+
"to assess it first.",
|
|
1662
|
+
CI_RESUME_STALE_OPERATION: "Read `epic-implementer explain-run` for the ticket; start a new resume-ci only if " +
|
|
1663
|
+
"its current stop independently qualifies.",
|
|
1664
|
+
CI_RESUME_CHECKS_NOT_GREEN: "Wait for the required checks to finish green, then retry.",
|
|
1665
|
+
CI_RESUME_VERDICT_UNAVAILABLE: "Retry later. If the provider stays unavailable, escalate to the CI-platform operator.",
|
|
1666
|
+
CI_RESUME_BUSY: "Retry the same command; it reuses the same operation.",
|
|
1667
|
+
ROW_VERSION_CONFLICT: "Retry the same command; it reuses the same operation.",
|
|
1668
|
+
RUN_NOT_DISPATCHABLE: "Resume the run first (`epic-implementer resume-run`), then retry.",
|
|
1669
|
+
CI_RESUME_LIVE_WORK: "Retry when the running work has finished.",
|
|
1670
|
+
};
|
|
1671
|
+
const RESUME_CI_STOP_REASON_TEXT = {
|
|
1672
|
+
wait_expired: "an expired CI wait",
|
|
1673
|
+
repair_exhausted: "exhausted required-check repairs",
|
|
1674
|
+
verdict_unavailable: "an unavailable CI verdict",
|
|
1675
|
+
};
|
|
1676
|
+
const RESUME_CI_NOT_ELIGIBLE_TEXT = {
|
|
1677
|
+
ticket_not_found: "the ticket is not part of this run",
|
|
1678
|
+
not_parked: "the ticket is not parked",
|
|
1679
|
+
not_ci_park: "the ticket is not parked at the CI gate",
|
|
1680
|
+
no_typed_stop: "the CI park has no complete qualified stop record (a legacy or incomplete park)",
|
|
1681
|
+
stop_superseded: "the CI stop belongs to an earlier park episode",
|
|
1682
|
+
};
|
|
1683
|
+
/** The one consequence statement every committed result carries. */
|
|
1684
|
+
const RESUME_CI_CONSEQUENCE = "CI is back in evaluation at the same head; its verdict is pending. This action did NOT " +
|
|
1685
|
+
"mark CI passed, authorize review or merge, create a job, extend the deadline, or refund " +
|
|
1686
|
+
"any repair.";
|
|
1687
|
+
export function parseResumeCiArgs(argv) {
|
|
1688
|
+
return parseTicketRecoveryArgs(argv);
|
|
1689
|
+
}
|
|
1690
|
+
function resumeCiOperationSummary(op) {
|
|
1691
|
+
return {
|
|
1692
|
+
resumed_at: op.resumed_at,
|
|
1693
|
+
stop_reason: op.stop_reason,
|
|
1694
|
+
park_generation: op.park_generation,
|
|
1695
|
+
resumed_gate: op.resumed_gate,
|
|
1696
|
+
ci_gate_status: op.ci_gate_status,
|
|
1697
|
+
allowance_effect: op.allowance_effect,
|
|
1698
|
+
preserved_attempts: op.preserved_attempts,
|
|
1699
|
+
preserved_ci_infra_rerun_attempts_used: op.preserved_ci_infra_rerun_attempts_used,
|
|
1700
|
+
preserved_entry_attempts: op.preserved_entry_attempts,
|
|
1701
|
+
deadline_at: op.deadline_at,
|
|
1702
|
+
deadline_source: op.deadline_source,
|
|
1703
|
+
deadline_extended: op.deadline_extended,
|
|
1704
|
+
jobs_created: op.jobs_created,
|
|
1705
|
+
ci_passed_by_action: op.ci_passed_by_action,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function allowanceLine(op) {
|
|
1709
|
+
const infra = op.preserved_ci_infra_rerun_attempts_used ?? 0;
|
|
1710
|
+
const entry = op.preserved_entry_attempts ?? 0;
|
|
1711
|
+
return (`Preserved allowances: repair attempts spent ${op.preserved_attempts}, ` +
|
|
1712
|
+
`infrastructure retries used ${infra}, entry attempts ${entry}.`);
|
|
1713
|
+
}
|
|
1714
|
+
function deadlineLine(op) {
|
|
1715
|
+
if (op.deadline_at === null)
|
|
1716
|
+
return "Deadline unchanged (not recorded).";
|
|
1717
|
+
const source = op.deadline_source === "post_repair" ? "post-repair window" : "original gate window";
|
|
1718
|
+
return `Deadline unchanged: ${op.deadline_at} (${source}).`;
|
|
1719
|
+
}
|
|
1720
|
+
function currentStateLine(current) {
|
|
1721
|
+
return (`Current state (read now, separate from the history above): ticket ${current.ticket_status ?? "unknown"}, ` +
|
|
1722
|
+
`gate ${current.current_gate ?? "unknown"}, CI gate ${current.ci_gate_status ?? "unknown"}, ` +
|
|
1723
|
+
`held by run control: ${current.held_by_run_control ? "yes" : "no"}.`);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Exported for tests: render a `resume-ci` result.
|
|
1727
|
+
*
|
|
1728
|
+
* The outcome word leads the first line. Committed and replayed results go to
|
|
1729
|
+
* stdout with exit 0; refused, unconfirmed, and failed outcomes go to stderr with
|
|
1730
|
+
* their own non-zero codes. No operation key, stop binding, row version, SHA,
|
|
1731
|
+
* credential, provider error, or URL is ever printed, in either mode.
|
|
1732
|
+
*/
|
|
1733
|
+
export function renderResumeCiResult(result, target, json) {
|
|
1734
|
+
const label = `ticket ${target.ticketKey} (run ${target.epicRunId})`;
|
|
1735
|
+
if (result.kind === "committed" || result.kind === "replayed") {
|
|
1736
|
+
const r = result.result;
|
|
1737
|
+
if (json) {
|
|
1738
|
+
console.log(JSON.stringify({
|
|
1739
|
+
ok: true,
|
|
1740
|
+
outcome: result.kind,
|
|
1741
|
+
historical: result.kind === "replayed",
|
|
1742
|
+
epic_run_id: r.epic_run_id,
|
|
1743
|
+
ticket_key: r.ticket_key,
|
|
1744
|
+
operation: resumeCiOperationSummary(r.operation),
|
|
1745
|
+
current: r.current,
|
|
1746
|
+
audit_recorded: r.audit_recorded,
|
|
1747
|
+
audit_warning: r.audit_warning,
|
|
1748
|
+
reference_reused: result.reusedReference,
|
|
1749
|
+
}));
|
|
1750
|
+
return RESUME_CI_EXIT_OK;
|
|
1751
|
+
}
|
|
1752
|
+
const reason = RESUME_CI_STOP_REASON_TEXT[r.operation.stop_reason] ?? "a CI stop";
|
|
1753
|
+
if (result.kind === "committed") {
|
|
1754
|
+
console.log(`Committed: resume-ci for ${label}, which was parked on ${reason}.`);
|
|
1755
|
+
console.log(RESUME_CI_CONSEQUENCE);
|
|
1756
|
+
console.log(allowanceLine(r.operation));
|
|
1757
|
+
console.log(deadlineLine(r.operation));
|
|
1758
|
+
console.log("Next: the reconciler's next identical green poll confirms CI; review and merge checks still apply.");
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
console.log(`Replayed historical result: resume-ci for ${label} committed earlier, at ` +
|
|
1762
|
+
`${r.operation.resumed_at}. Nothing was changed by this call.`);
|
|
1763
|
+
console.log(`History: the ticket was parked on ${reason} and was returned to CI evaluation.`);
|
|
1764
|
+
console.log(allowanceLine(r.operation));
|
|
1765
|
+
console.log(deadlineLine(r.operation));
|
|
1766
|
+
console.log(currentStateLine(r.current));
|
|
1767
|
+
console.log("This history does not show that CI is green now, or that review or merge may proceed.");
|
|
1768
|
+
}
|
|
1769
|
+
if (!r.audit_recorded && r.audit_warning) {
|
|
1770
|
+
console.log(`Warning: ${r.audit_warning}`);
|
|
1771
|
+
}
|
|
1772
|
+
return RESUME_CI_EXIT_OK;
|
|
1773
|
+
}
|
|
1774
|
+
if (result.kind === "refused" || result.kind === "not_eligible" || result.kind === "stale_reference") {
|
|
1775
|
+
const code = result.kind === "refused"
|
|
1776
|
+
? result.errorCode
|
|
1777
|
+
: result.kind === "stale_reference"
|
|
1778
|
+
? "CI_RESUME_STALE_OPERATION"
|
|
1779
|
+
: "CI_STOP_NOT_ELIGIBLE";
|
|
1780
|
+
const sentence = result.kind === "not_eligible"
|
|
1781
|
+
? RESUME_CI_NOT_ELIGIBLE_TEXT[result.reason] ?? "resume-ci does not apply"
|
|
1782
|
+
: RESUME_CI_REFUSAL_SENTENCES[code] ?? "the server refused the request";
|
|
1783
|
+
const guidance = RESUME_CI_REFUSAL_GUIDANCE[code];
|
|
1784
|
+
if (json) {
|
|
1785
|
+
console.error(JSON.stringify({
|
|
1786
|
+
ok: false,
|
|
1787
|
+
outcome: "refused",
|
|
1788
|
+
refusal_code: code,
|
|
1789
|
+
...(result.kind === "not_eligible" ? { local_reason: result.reason } : {}),
|
|
1790
|
+
message: sentence,
|
|
1791
|
+
...(guidance ? { guidance } : {}),
|
|
1792
|
+
nothing_written: true,
|
|
1793
|
+
}));
|
|
1794
|
+
return RESUME_CI_EXIT_REFUSED;
|
|
1795
|
+
}
|
|
1796
|
+
console.error(`Refused: resume-ci for ${label} — ${sentence}. Nothing was written.`);
|
|
1797
|
+
if (guidance)
|
|
1798
|
+
console.error(guidance);
|
|
1799
|
+
return RESUME_CI_EXIT_REFUSED;
|
|
1800
|
+
}
|
|
1801
|
+
if (result.kind === "unconfirmed") {
|
|
1802
|
+
if (json) {
|
|
1803
|
+
console.error(JSON.stringify({
|
|
1804
|
+
ok: false,
|
|
1805
|
+
outcome: "unconfirmed",
|
|
1806
|
+
transport: result.transport,
|
|
1807
|
+
attempts: result.attempts,
|
|
1808
|
+
}));
|
|
1809
|
+
return RESUME_CI_EXIT_UNCONFIRMED;
|
|
1810
|
+
}
|
|
1811
|
+
console.error(`Unconfirmed: resume-ci for ${label} — no durable answer arrived after ${result.attempts} ` +
|
|
1812
|
+
`attempts (${result.transport}). The resume may or may not have committed.`);
|
|
1813
|
+
console.error("Re-run the same command later: it reuses this same operation and reports its original " +
|
|
1814
|
+
"result. Do not use unpark or another recovery in the meantime.");
|
|
1815
|
+
return RESUME_CI_EXIT_UNCONFIRMED;
|
|
1816
|
+
}
|
|
1817
|
+
const message = result.kind === "unavailable" ? result.message : "the outcome could not be read";
|
|
1818
|
+
if (json) {
|
|
1819
|
+
console.error(JSON.stringify({ ok: false, outcome: "failed", message }));
|
|
1820
|
+
}
|
|
1821
|
+
else {
|
|
1822
|
+
console.error(`Failed: resume-ci for ${label} — ${message}.`);
|
|
1823
|
+
}
|
|
1824
|
+
return RESUME_CI_EXIT_FAILED;
|
|
1825
|
+
}
|
|
1826
|
+
export async function runResumeCiCommand(argv) {
|
|
1827
|
+
try {
|
|
1828
|
+
const parsed = parseResumeCiArgs(argv);
|
|
1829
|
+
if (parsed.help) {
|
|
1830
|
+
console.log(RESUME_CI_USAGE);
|
|
1831
|
+
return 0;
|
|
1832
|
+
}
|
|
1833
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
1834
|
+
if (!access)
|
|
1835
|
+
return RESUME_CI_EXIT_FAILED;
|
|
1836
|
+
const { resumeCiOnce } = await import("./recovery-operations.js");
|
|
1837
|
+
const result = await resumeCiOnce(access, {
|
|
1838
|
+
epicRunId: parsed.epicRunId,
|
|
1839
|
+
ticketKey: parsed.ticketKey,
|
|
1840
|
+
});
|
|
1841
|
+
return renderResumeCiResult(result, parsed, parsed.json);
|
|
1842
|
+
}
|
|
1843
|
+
catch (error) {
|
|
1844
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
1845
|
+
return RESUME_CI_EXIT_FAILED;
|
|
1846
|
+
}
|
|
1847
|
+
}
|