@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
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
* callers: `recovery-cli.ts` and `plane/shutdown.ts`.
|
|
19
19
|
*/
|
|
20
20
|
import { randomUUID } from "crypto";
|
|
21
|
-
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
|
|
21
|
+
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, RERUN_CI_ROUTE_UNAVAILABLE_CODE, RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE, rerunCi, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, resumeCiTicket, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
|
|
22
|
+
import { clearGuidedReference, guardForBinding, GUIDED_PAYLOAD_CONFLICT_MESSAGE, GUIDED_REFERENCE_STORE_FAILED_MESSAGE, guidedPayloadDigest, prepareGuidedRequest, } from "./guided-repair.js";
|
|
22
23
|
import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
|
|
23
|
-
import { clearContinuationReference, computeContinuationFingerprint, isFullSha, newContinuationRequestId, readContinuationReference, writeContinuationReference, } from "./epic-child-continuation.js";
|
|
24
|
+
import { canonicalSha256, clearContinuationReference, clearRecoveryReferenceFile, computeContinuationFingerprint, readRecoveryReferenceFile, writeRecoveryReferenceFile, isFullSha, newContinuationRequestId, readContinuationReference, writeContinuationReference, } from "./epic-child-continuation.js";
|
|
24
25
|
import { resolveLocalMergeCredential, resolveLocalMergeMethod, runApprovedLocalMerge, } from "./local-merge.js";
|
|
25
|
-
import { RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
|
|
26
|
+
import { RERUN_CI_UNCONFIRMED_REASON, RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
|
|
26
27
|
/**
|
|
27
28
|
* Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
|
|
28
29
|
* begins with a fresh, authoritative state read — so this bounds full
|
|
@@ -453,6 +454,119 @@ export async function resumeRunRepair(access, options, deps = {}) {
|
|
|
453
454
|
return { ok: false, kind: "unavailable", message: RESUME_RUN_UNCONFIRMED_MESSAGE };
|
|
454
455
|
}
|
|
455
456
|
}
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// BAPI-1333 — guided (binding-pinned) repairs
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
/** The server's refusal code for a guided repair whose pinned subject moved. */
|
|
461
|
+
export const REPAIR_SUBJECT_STALE_CODE = "REPAIR_SUBJECT_STALE";
|
|
462
|
+
/**
|
|
463
|
+
* Send ONE guided repair request, pinned to its displayed binding.
|
|
464
|
+
*
|
|
465
|
+
* The reference (request id, binding digest, payload digest) is durably stored
|
|
466
|
+
* BEFORE the first POST; a store failure sends nothing and never falls back to
|
|
467
|
+
* an unguarded or resetting verb. Only a structured 4xx is a definitive answer;
|
|
468
|
+
* a 5xx, timeout, dropped connection, or unparseable 2xx is `unconfirmed`, and
|
|
469
|
+
* the reference survives it so a new CLI process re-sends the same request.
|
|
470
|
+
*/
|
|
471
|
+
export async function guidedRepairOnce(options, deps = {}) {
|
|
472
|
+
const prepared = await prepareGuidedRequest(options.binding, options.payloadDigest, deps);
|
|
473
|
+
if (prepared.kind === "store_failed") {
|
|
474
|
+
return { kind: "unavailable", message: GUIDED_REFERENCE_STORE_FAILED_MESSAGE };
|
|
475
|
+
}
|
|
476
|
+
if (prepared.kind === "payload_conflict") {
|
|
477
|
+
return { kind: "unavailable", message: GUIDED_PAYLOAD_CONFLICT_MESSAGE };
|
|
478
|
+
}
|
|
479
|
+
const { requestId, referenceReused } = prepared;
|
|
480
|
+
let result;
|
|
481
|
+
try {
|
|
482
|
+
result = await options.send(guardForBinding(options.binding, requestId));
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
if (err instanceof ConductorBridgeApiError &&
|
|
486
|
+
typeof err.status === "number" &&
|
|
487
|
+
err.status >= 400 &&
|
|
488
|
+
err.status < 500 &&
|
|
489
|
+
err.errorCode) {
|
|
490
|
+
await clearGuidedReference(options.binding, deps);
|
|
491
|
+
const message = err.bodyPreview ?? err.errorCode;
|
|
492
|
+
if (err.errorCode === REPAIR_SUBJECT_STALE_CODE) {
|
|
493
|
+
return { kind: "stale", code: err.errorCode, message, requestId, referenceReused };
|
|
494
|
+
}
|
|
495
|
+
return { kind: "refused", code: err.errorCode, status: err.status, message, requestId };
|
|
496
|
+
}
|
|
497
|
+
return { kind: "unconfirmed", message: options.unconfirmedMessage, requestId, referenceReused };
|
|
498
|
+
}
|
|
499
|
+
await clearGuidedReference(options.binding, deps);
|
|
500
|
+
return result.committed
|
|
501
|
+
? { kind: "committed", result, requestId, referenceReused }
|
|
502
|
+
: { kind: "no_op", result, requestId, referenceReused };
|
|
503
|
+
}
|
|
504
|
+
/** The bounded unconfirmed text for a `rerun-ci` whose release write was lost. */
|
|
505
|
+
export const RERUN_CI_COMPENSATION_UNCONFIRMED_MESSAGE = "the server could not confirm whether the workflow runs it did not enqueue are still " +
|
|
506
|
+
"recorded for this head. Re-read the run explanation before choosing another action";
|
|
507
|
+
/**
|
|
508
|
+
* Send ONE guided `rerun-ci`, pinned to its displayed binding, through the shared
|
|
509
|
+
* guided-repair reference (stored BEFORE the first POST; a lost response re-sends
|
|
510
|
+
* the SAME request). The server decides eligibility and every per-check status;
|
|
511
|
+
* this layer only classifies the answer.
|
|
512
|
+
*/
|
|
513
|
+
export async function rerunCiRepair(access, options, deps = {}) {
|
|
514
|
+
const call = deps.rerunCi ?? rerunCi;
|
|
515
|
+
const outcome = await guidedRepairOnce({
|
|
516
|
+
binding: options.binding,
|
|
517
|
+
payloadDigest: guidedPayloadDigest({ ticket_key: options.ticketKey }),
|
|
518
|
+
send: (guard) => call(access, { epicRunId: options.epicRunId, ticketKey: options.ticketKey, guard }),
|
|
519
|
+
unconfirmedMessage: "the outcome could not be confirmed: the server may or may not have requested the " +
|
|
520
|
+
"re-runs. Re-run the SAME command to re-send the same pinned request, or inspect " +
|
|
521
|
+
"the run read-only",
|
|
522
|
+
}, deps);
|
|
523
|
+
switch (outcome.kind) {
|
|
524
|
+
case "committed":
|
|
525
|
+
case "no_op":
|
|
526
|
+
return {
|
|
527
|
+
kind: "applied",
|
|
528
|
+
result: outcome.result,
|
|
529
|
+
requestId: outcome.requestId,
|
|
530
|
+
referenceReused: outcome.referenceReused,
|
|
531
|
+
};
|
|
532
|
+
case "stale":
|
|
533
|
+
return {
|
|
534
|
+
kind: "stale",
|
|
535
|
+
code: outcome.code,
|
|
536
|
+
message: outcome.message,
|
|
537
|
+
requestId: outcome.requestId,
|
|
538
|
+
};
|
|
539
|
+
case "refused":
|
|
540
|
+
if (outcome.code === RERUN_CI_ROUTE_UNAVAILABLE_CODE) {
|
|
541
|
+
return { kind: "unsupported", message: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE };
|
|
542
|
+
}
|
|
543
|
+
if (outcome.code === "ROW_VERSION_CONFLICT" &&
|
|
544
|
+
outcome.message.includes(RERUN_CI_UNCONFIRMED_REASON)) {
|
|
545
|
+
return {
|
|
546
|
+
kind: "unconfirmed",
|
|
547
|
+
reason: "compensation_conflict",
|
|
548
|
+
message: RERUN_CI_COMPENSATION_UNCONFIRMED_MESSAGE,
|
|
549
|
+
requestId: outcome.requestId,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
kind: "refused",
|
|
554
|
+
code: outcome.code,
|
|
555
|
+
status: outcome.status,
|
|
556
|
+
message: outcome.message,
|
|
557
|
+
requestId: outcome.requestId,
|
|
558
|
+
};
|
|
559
|
+
case "unconfirmed":
|
|
560
|
+
return {
|
|
561
|
+
kind: "unconfirmed",
|
|
562
|
+
reason: "transport",
|
|
563
|
+
message: outcome.message,
|
|
564
|
+
requestId: outcome.requestId,
|
|
565
|
+
};
|
|
566
|
+
default:
|
|
567
|
+
return { kind: "unavailable", message: outcome.message };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
456
570
|
const AUTHORITATIVE_OUTCOMES = new Set([
|
|
457
571
|
"committed",
|
|
458
572
|
"already_continued",
|
|
@@ -644,3 +758,278 @@ export async function lookupContinuationReceipt(access, options, deps = {}) {
|
|
|
644
758
|
}
|
|
645
759
|
return { kind: "not_found", requestId, status };
|
|
646
760
|
}
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
// BAPI-1326 — `resume-ci`: a pinned, bounded, allowance-preserving CI resume
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
/** The recovery-reference namespace of pinned `resume-ci` operations. */
|
|
765
|
+
export const RESUME_CI_REFERENCE_NAMESPACE = "epic-ci-resumes";
|
|
766
|
+
/**
|
|
767
|
+
* Bounded POSTs per invocation. Every POST of one invocation carries the SAME
|
|
768
|
+
* operation key and stop binding; only the row version may be refreshed, and
|
|
769
|
+
* only while every original binding still matches.
|
|
770
|
+
*/
|
|
771
|
+
export const RESUME_CI_POST_LIMIT = 3;
|
|
772
|
+
/** The pause before a retry. Injectable so tests never sleep. */
|
|
773
|
+
const RESUME_CI_RETRY_DELAY_MS = 250;
|
|
774
|
+
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
775
|
+
const FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
|
|
776
|
+
const CI_STOP_REASON_VALUES = new Set(["wait_expired", "verdict_unavailable", "repair_exhausted"]);
|
|
777
|
+
function isRecordValue(value) {
|
|
778
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
779
|
+
}
|
|
780
|
+
function isNonNegativeInteger(value) {
|
|
781
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
782
|
+
}
|
|
783
|
+
/** Strictly validate a stop binding; `null` for anything short of the contract. */
|
|
784
|
+
export function parseCiStopBinding(value) {
|
|
785
|
+
if (!isRecordValue(value))
|
|
786
|
+
return null;
|
|
787
|
+
const checks = value.required_checks;
|
|
788
|
+
if (!isNonNegativeInteger(value.park_generation) ||
|
|
789
|
+
!CI_STOP_REASON_VALUES.has(value.reason) ||
|
|
790
|
+
typeof value.accepted_head_sha !== "string" || !FULL_SHA_PATTERN.test(value.accepted_head_sha) ||
|
|
791
|
+
!isNonNegativeInteger(value.pr_number) || value.pr_number < 1 ||
|
|
792
|
+
typeof value.base_ref !== "string" || !value.base_ref || value.base_ref.length > 200 ||
|
|
793
|
+
!Array.isArray(checks) || checks.length > 64 ||
|
|
794
|
+
!checks.every((c) => typeof c === "string" && c.length > 0 && c.length <= 200) ||
|
|
795
|
+
(value.required_checks_source !== "run" && value.required_checks_source !== "plan") ||
|
|
796
|
+
typeof value.required_checks_fingerprint !== "string" ||
|
|
797
|
+
!FINGERPRINT_PATTERN.test(value.required_checks_fingerprint) ||
|
|
798
|
+
!(value.spec_hash === null || value.spec_hash === undefined ||
|
|
799
|
+
(typeof value.spec_hash === "string" && value.spec_hash.length > 0 && value.spec_hash.length <= 128))) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
park_generation: value.park_generation,
|
|
804
|
+
reason: value.reason,
|
|
805
|
+
accepted_head_sha: value.accepted_head_sha,
|
|
806
|
+
pr_number: value.pr_number,
|
|
807
|
+
base_ref: value.base_ref,
|
|
808
|
+
required_checks: [...checks],
|
|
809
|
+
required_checks_source: value.required_checks_source,
|
|
810
|
+
required_checks_fingerprint: value.required_checks_fingerprint,
|
|
811
|
+
spec_hash: (value.spec_hash ?? null),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Capture the ACTIVE typed CI stop's binding from authenticated ticket state,
|
|
816
|
+
* mirroring the server's selection: the ticket is `needs_human`, its single
|
|
817
|
+
* parked gate is `ci`, and `gates.ci.ci_stop` is a complete typed record for the
|
|
818
|
+
* gate's current park generation. Never derived from the park's reason text.
|
|
819
|
+
*/
|
|
820
|
+
export function captureCiStopBinding(ticket) {
|
|
821
|
+
// The client union predates the v2 gate statuses; the server sends them anyway.
|
|
822
|
+
if (ticket.status !== "needs_human")
|
|
823
|
+
return { ok: false, reason: "not_parked" };
|
|
824
|
+
const gates = isRecordValue(ticket.gates) ? ticket.gates : {};
|
|
825
|
+
const parked = Object.entries(gates).filter(([, state]) => isRecordValue(state) && state.status === "needs_human");
|
|
826
|
+
if (parked.length !== 1 || parked[0][0] !== "ci")
|
|
827
|
+
return { ok: false, reason: "not_ci_park" };
|
|
828
|
+
const ci = parked[0][1];
|
|
829
|
+
const stop = ci.ci_stop;
|
|
830
|
+
if (!isRecordValue(stop) || !isRecordValue(stop.evidence))
|
|
831
|
+
return { ok: false, reason: "no_typed_stop" };
|
|
832
|
+
const generation = isNonNegativeInteger(ci.park_generation) ? ci.park_generation : 0;
|
|
833
|
+
if (stop.park_generation !== generation)
|
|
834
|
+
return { ok: false, reason: "stop_superseded" };
|
|
835
|
+
const evidence = stop.evidence;
|
|
836
|
+
const binding = parseCiStopBinding({
|
|
837
|
+
park_generation: stop.park_generation,
|
|
838
|
+
reason: stop.reason,
|
|
839
|
+
accepted_head_sha: stop.accepted_head_sha,
|
|
840
|
+
pr_number: evidence.pr_number,
|
|
841
|
+
base_ref: evidence.base_ref,
|
|
842
|
+
required_checks: evidence.required_checks,
|
|
843
|
+
required_checks_source: evidence.required_checks_source,
|
|
844
|
+
required_checks_fingerprint: evidence.required_checks_fingerprint,
|
|
845
|
+
spec_hash: evidence.spec_hash ?? null,
|
|
846
|
+
});
|
|
847
|
+
return binding === null ? { ok: false, reason: "no_typed_stop" } : { ok: true, binding };
|
|
848
|
+
}
|
|
849
|
+
/** The fingerprint of a pinned operation's identity (the row version excluded). */
|
|
850
|
+
export function computeCiResumeFingerprint(ref) {
|
|
851
|
+
return canonicalSha256({
|
|
852
|
+
v: 1,
|
|
853
|
+
operation: "resume_ci",
|
|
854
|
+
target: { repo_name: ref.repoName, epic_run_id: ref.epicRunId, ticket_key: ref.ticketKey },
|
|
855
|
+
operation_key: ref.operationKey,
|
|
856
|
+
stop_binding: ref.stopBinding,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
function parseCiResumeReference(raw) {
|
|
860
|
+
if (!isRecordValue(raw) || raw.version !== 1 || raw.operation !== "resume_ci")
|
|
861
|
+
return null;
|
|
862
|
+
const binding = parseCiStopBinding(raw.stopBinding);
|
|
863
|
+
if (binding === null ||
|
|
864
|
+
typeof raw.repoName !== "string" || !raw.repoName ||
|
|
865
|
+
typeof raw.epicRunId !== "string" || !raw.epicRunId ||
|
|
866
|
+
typeof raw.ticketKey !== "string" || !raw.ticketKey ||
|
|
867
|
+
!isNonNegativeInteger(raw.expectedRowVersion) ||
|
|
868
|
+
typeof raw.operationKey !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(raw.operationKey) ||
|
|
869
|
+
typeof raw.fingerprint !== "string") {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const ref = {
|
|
873
|
+
version: 1,
|
|
874
|
+
operation: "resume_ci",
|
|
875
|
+
repoName: raw.repoName,
|
|
876
|
+
epicRunId: raw.epicRunId,
|
|
877
|
+
ticketKey: raw.ticketKey,
|
|
878
|
+
expectedRowVersion: raw.expectedRowVersion,
|
|
879
|
+
operationKey: raw.operationKey,
|
|
880
|
+
stopBinding: binding,
|
|
881
|
+
fingerprint: raw.fingerprint,
|
|
882
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : "",
|
|
883
|
+
};
|
|
884
|
+
// A reference whose fingerprint no longer matches its own identity was
|
|
885
|
+
// tampered with or truncated; it never authorizes a replay.
|
|
886
|
+
return computeCiResumeFingerprint(ref) === ref.fingerprint ? ref : null;
|
|
887
|
+
}
|
|
888
|
+
function sameBinding(a, b) {
|
|
889
|
+
return canonicalSha256(a) === canonicalSha256(b);
|
|
890
|
+
}
|
|
891
|
+
async function readCiResumeReference(key, store) {
|
|
892
|
+
return parseCiResumeReference(await readRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, key, store));
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Resume a ticket parked on a qualified CI stop, with its operation pinned.
|
|
896
|
+
*
|
|
897
|
+
* A pinned reference for this ticket is REUSED as-is — same operation key, same
|
|
898
|
+
* stop binding — so a lost response or a restarted process is answered from the
|
|
899
|
+
* server's durable receipt, and a later park is never silently targeted. Only
|
|
900
|
+
* when no reference exists is the binding read (once) from authenticated ticket
|
|
901
|
+
* state, and the reference is stored BEFORE the first POST.
|
|
902
|
+
*
|
|
903
|
+
* At most {@link RESUME_CI_POST_LIMIT} POSTs, all with the same key and binding.
|
|
904
|
+
* A row-version conflict refreshes the row version only while every original
|
|
905
|
+
* binding still matches; otherwise the result is `stale_reference` and nothing
|
|
906
|
+
* more is sent. There is no fallback to unpark, continue-child, or any other
|
|
907
|
+
* mutation.
|
|
908
|
+
*/
|
|
909
|
+
export async function resumeCiOnce(access, options, deps = {}) {
|
|
910
|
+
const fetchState = deps.fetchState ?? fetchEpicRunState;
|
|
911
|
+
const post = deps.post ?? resumeCiTicket;
|
|
912
|
+
const store = deps.store ?? {};
|
|
913
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
914
|
+
const callerKey = { epicRunId: options.epicRunId, ticketKey: options.ticketKey };
|
|
915
|
+
let reference = await readCiResumeReference(callerKey, store);
|
|
916
|
+
if (reference !== null && reference.repoName !== access.repoName)
|
|
917
|
+
reference = null;
|
|
918
|
+
const reusedReference = reference !== null;
|
|
919
|
+
const persist = async (ref) => {
|
|
920
|
+
await writeRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, callerKey, ref, store);
|
|
921
|
+
if (ref.epicRunId !== callerKey.epicRunId || ref.ticketKey !== callerKey.ticketKey) {
|
|
922
|
+
await writeRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, { epicRunId: ref.epicRunId, ticketKey: ref.ticketKey }, ref, store);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
const readTicket = async () => {
|
|
926
|
+
try {
|
|
927
|
+
const state = await fetchState(access, options.epicRunId);
|
|
928
|
+
return state.ticket_statuses.find((t) => t.ticket_key === options.ticketKey) ?? null;
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
return "unavailable";
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
if (reference === null) {
|
|
935
|
+
const ticket = await readTicket();
|
|
936
|
+
if (ticket === "unavailable") {
|
|
937
|
+
return { kind: "unavailable", message: "could not read the ticket's state, so nothing was sent" };
|
|
938
|
+
}
|
|
939
|
+
if (ticket === null)
|
|
940
|
+
return { kind: "not_eligible", reason: "ticket_not_found" };
|
|
941
|
+
const captured = captureCiStopBinding(ticket);
|
|
942
|
+
if (!captured.ok)
|
|
943
|
+
return { kind: "not_eligible", reason: captured.reason };
|
|
944
|
+
const identity = {
|
|
945
|
+
repoName: access.repoName,
|
|
946
|
+
epicRunId: ticket.epic_run_id,
|
|
947
|
+
ticketKey: ticket.ticket_key,
|
|
948
|
+
operationKey: (deps.newOperationKey ?? randomUUID)(),
|
|
949
|
+
stopBinding: captured.binding,
|
|
950
|
+
};
|
|
951
|
+
reference = {
|
|
952
|
+
version: 1,
|
|
953
|
+
operation: "resume_ci",
|
|
954
|
+
...identity,
|
|
955
|
+
expectedRowVersion: ticket.row_version,
|
|
956
|
+
fingerprint: computeCiResumeFingerprint(identity),
|
|
957
|
+
createdAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
958
|
+
};
|
|
959
|
+
try {
|
|
960
|
+
await persist(reference);
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
return {
|
|
964
|
+
kind: "unavailable",
|
|
965
|
+
message: "could not store the local recovery reference, so the request was not sent",
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const clear = async (ref) => {
|
|
970
|
+
await clearRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, callerKey, store);
|
|
971
|
+
await clearRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, { epicRunId: ref.epicRunId, ticketKey: ref.ticketKey }, store);
|
|
972
|
+
};
|
|
973
|
+
let ref = reference;
|
|
974
|
+
let last = null;
|
|
975
|
+
for (let attempt = 1; attempt <= RESUME_CI_POST_LIMIT; attempt += 1) {
|
|
976
|
+
if (attempt > 1)
|
|
977
|
+
await sleep(RESUME_CI_RETRY_DELAY_MS * (attempt - 1));
|
|
978
|
+
const outcome = await post(access, {
|
|
979
|
+
epicRunId: ref.epicRunId,
|
|
980
|
+
ticketKey: ref.ticketKey,
|
|
981
|
+
expectedRowVersion: ref.expectedRowVersion,
|
|
982
|
+
operationKey: ref.operationKey,
|
|
983
|
+
stopBinding: ref.stopBinding,
|
|
984
|
+
});
|
|
985
|
+
last = outcome;
|
|
986
|
+
if (outcome.kind === "result") {
|
|
987
|
+
await clear(ref);
|
|
988
|
+
return { kind: outcome.result.outcome, result: outcome.result, reusedReference };
|
|
989
|
+
}
|
|
990
|
+
if (outcome.kind === "uncertain")
|
|
991
|
+
continue;
|
|
992
|
+
if (outcome.kind === "refused" && outcome.errorCode === "CI_RESUME_BUSY")
|
|
993
|
+
continue;
|
|
994
|
+
if (outcome.kind === "refused" && outcome.errorCode === "ROW_VERSION_CONFLICT") {
|
|
995
|
+
// Refresh the row version ONLY while the stop is still the same one.
|
|
996
|
+
const ticket = await readTicket();
|
|
997
|
+
if (ticket === "unavailable" || ticket === null) {
|
|
998
|
+
return { kind: "refused", errorCode: outcome.errorCode, status: outcome.status, reusedReference };
|
|
999
|
+
}
|
|
1000
|
+
const current = captureCiStopBinding(ticket);
|
|
1001
|
+
if (!current.ok || !sameBinding(current.binding, ref.stopBinding)) {
|
|
1002
|
+
return { kind: "stale_reference", reusedReference };
|
|
1003
|
+
}
|
|
1004
|
+
ref = { ...ref, expectedRowVersion: ticket.row_version };
|
|
1005
|
+
try {
|
|
1006
|
+
await persist(ref);
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
return {
|
|
1010
|
+
kind: "unavailable",
|
|
1011
|
+
message: "could not update the local recovery reference, so the retry was not sent",
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
// Every other refusal or rejection is authoritative: nothing was written,
|
|
1017
|
+
// and this operation is finished.
|
|
1018
|
+
await clear(ref);
|
|
1019
|
+
return {
|
|
1020
|
+
kind: "refused",
|
|
1021
|
+
errorCode: outcome.errorCode ?? "REQUEST_REJECTED",
|
|
1022
|
+
status: outcome.status,
|
|
1023
|
+
reusedReference,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
if (last !== null && last.kind === "refused") {
|
|
1027
|
+
// Busy or row-version churn for the whole budget: nothing was written, and
|
|
1028
|
+
// the pinned reference is kept for the next invocation.
|
|
1029
|
+
return { kind: "refused", errorCode: last.errorCode, status: last.status, reusedReference };
|
|
1030
|
+
}
|
|
1031
|
+
const transport = last !== null && last.kind === "uncertain"
|
|
1032
|
+
? last.status !== undefined ? `${last.reason} (status ${last.status})` : last.reason
|
|
1033
|
+
: "indeterminate";
|
|
1034
|
+
return { kind: "unconfirmed", transport, reusedReference, attempts: RESUME_CI_POST_LIMIT };
|
|
1035
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared contract for the
|
|
3
|
-
* `scope-reparse`, `dispatch-remediation`, and `merge-child`,
|
|
4
|
-
* `scope-catch-up`, `patch-policy`, and `resume-run
|
|
2
|
+
* The shared contract for the eight operator repair verbs: BAPI-1154's
|
|
3
|
+
* `scope-reparse`, `dispatch-remediation`, and `merge-child`, BAPI-1176's
|
|
4
|
+
* `scope-catch-up`, `patch-policy`, and `resume-run`, BAPI-1326's
|
|
5
|
+
* ticket-addressed `resume-ci`, and BAPI-1388's ticket-addressed `rerun-ci`.
|
|
5
6
|
*
|
|
6
7
|
* CLI-only, exactly like the BAPI-872 recovery verbs: none of these names is ever
|
|
7
8
|
* registered as an MCP tool (`index-static.test.ts` pins that). This module is the
|
|
@@ -20,6 +21,8 @@ export const VERB_MERGE_CHILD = "merge-child";
|
|
|
20
21
|
export const VERB_SCOPE_CATCH_UP = "scope-catch-up";
|
|
21
22
|
export const VERB_PATCH_POLICY = "patch-policy";
|
|
22
23
|
export const VERB_RESUME_RUN = "resume-run";
|
|
24
|
+
export const VERB_RESUME_CI = "resume-ci";
|
|
25
|
+
export const VERB_RERUN_CI = "rerun-ci";
|
|
23
26
|
/** Every repair verb, in the order an operator meets them. */
|
|
24
27
|
export const OPERATOR_REPAIR_VERBS = [
|
|
25
28
|
VERB_SCOPE_REPARSE,
|
|
@@ -28,6 +31,8 @@ export const OPERATOR_REPAIR_VERBS = [
|
|
|
28
31
|
VERB_SCOPE_CATCH_UP,
|
|
29
32
|
VERB_PATCH_POLICY,
|
|
30
33
|
VERB_RESUME_RUN,
|
|
34
|
+
VERB_RESUME_CI,
|
|
35
|
+
VERB_RERUN_CI,
|
|
31
36
|
];
|
|
32
37
|
/**
|
|
33
38
|
* The ONE cap on driver context, in UTF-8 BYTES — the same number the server
|
|
@@ -92,9 +97,113 @@ export const OPERATOR_REPAIR_REFUSAL_CODES = [
|
|
|
92
97
|
"RUN_STOPPED",
|
|
93
98
|
"QUARANTINE_CLEARANCE_REQUIRED",
|
|
94
99
|
"RUN_CONTROL_CONFLICT",
|
|
100
|
+
// BAPI-1326
|
|
101
|
+
"CI_STOP_NOT_ELIGIBLE",
|
|
102
|
+
"CI_RESUME_STALE_OPERATION",
|
|
103
|
+
"CI_RESUME_OPERATION_CONFLICT",
|
|
104
|
+
"CI_RESUME_BINDING_CHANGED",
|
|
105
|
+
"CI_RESUME_POLICY_CHANGED",
|
|
106
|
+
"CI_RESUME_SPEC_CHANGED",
|
|
107
|
+
"CI_RESUME_CHECKS_NOT_GREEN",
|
|
108
|
+
"CI_RESUME_VERDICT_UNAVAILABLE",
|
|
109
|
+
"CI_RESUME_HELD",
|
|
110
|
+
"CI_RESUME_LIVE_WORK",
|
|
111
|
+
"CI_RESUME_BUDGET_EXHAUSTED",
|
|
112
|
+
"CI_RESUME_BUSY",
|
|
113
|
+
// BAPI-1333
|
|
114
|
+
"GUIDED_REPAIR_DISABLED",
|
|
115
|
+
"REPAIR_SUBJECT_STALE",
|
|
95
116
|
];
|
|
96
117
|
/** True when `code` is one of the closed repair refusal codes. */
|
|
97
118
|
export function isOperatorRepairRefusalCode(code) {
|
|
98
119
|
return (typeof code === "string" &&
|
|
99
120
|
OPERATOR_REPAIR_REFUSAL_CODES.includes(code));
|
|
100
121
|
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// BAPI-1326 — `resume-ci`
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
/**
|
|
126
|
+
* The closed `resume-ci` refusal subset, in the order the server checks them —
|
|
127
|
+
* mirrors `CI_RESUME_REFUSAL_CODES` in `operator_repair_contract.py`.
|
|
128
|
+
*/
|
|
129
|
+
export const CI_RESUME_REFUSAL_CODES = [
|
|
130
|
+
"TICKET_NOT_IN_RUN",
|
|
131
|
+
"CI_STOP_NOT_ELIGIBLE",
|
|
132
|
+
"CI_RESUME_STALE_OPERATION",
|
|
133
|
+
"CI_RESUME_OPERATION_CONFLICT",
|
|
134
|
+
"ROW_VERSION_CONFLICT",
|
|
135
|
+
"RUN_NOT_DISPATCHABLE",
|
|
136
|
+
"CI_RESUME_BUDGET_EXHAUSTED",
|
|
137
|
+
"CI_RESUME_HELD",
|
|
138
|
+
"CI_RESUME_LIVE_WORK",
|
|
139
|
+
"CI_RESUME_SPEC_CHANGED",
|
|
140
|
+
"CI_RESUME_POLICY_CHANGED",
|
|
141
|
+
"CI_RESUME_BINDING_CHANGED",
|
|
142
|
+
"CI_RESUME_CHECKS_NOT_GREEN",
|
|
143
|
+
"CI_RESUME_VERDICT_UNAVAILABLE",
|
|
144
|
+
"CI_RESUME_BUSY",
|
|
145
|
+
];
|
|
146
|
+
/**
|
|
147
|
+
* The closed outcome vocabulary. The server answers `committed` or `replayed`;
|
|
148
|
+
* `refused` is a structured error, and `unconfirmed` is minted only by the client
|
|
149
|
+
* when no durable answer arrived.
|
|
150
|
+
*/
|
|
151
|
+
export const CI_RESUME_OUTCOMES = ["committed", "replayed", "refused", "unconfirmed"];
|
|
152
|
+
/**
|
|
153
|
+
* Receipt retention: only the latest committed operation per ticket is kept. A
|
|
154
|
+
* request whose receipt was displaced is refused as stale, never replayed.
|
|
155
|
+
*/
|
|
156
|
+
export const CI_RESUME_RECEIPT_RETENTION = "latest_only";
|
|
157
|
+
/** True when `code` is one of the closed `resume-ci` refusal codes. */
|
|
158
|
+
export function isCiResumeRefusalCode(code) {
|
|
159
|
+
return (typeof code === "string" && CI_RESUME_REFUSAL_CODES.includes(code));
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// BAPI-1388 — `rerun-ci`
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
/**
|
|
165
|
+
* The closed `rerun-ci` refusal subset — mirrors `RERUN_CI_REFUSAL_CODES` in
|
|
166
|
+
* `operator_repair_contract.py`. It mints no new code: every member already
|
|
167
|
+
* exists in {@link OPERATOR_REPAIR_REFUSAL_CODES}.
|
|
168
|
+
*/
|
|
169
|
+
export const RERUN_CI_REFUSAL_CODES = [
|
|
170
|
+
"TICKET_NOT_IN_RUN",
|
|
171
|
+
"GUIDED_REPAIR_DISABLED",
|
|
172
|
+
"REPAIR_SUBJECT_STALE",
|
|
173
|
+
"RUN_NOT_DISPATCHABLE",
|
|
174
|
+
"TICKET_NOT_REMEDIABLE",
|
|
175
|
+
"REMEDIATION_JOB_IN_FLIGHT",
|
|
176
|
+
"REMEDIATION_LANE_BUSY",
|
|
177
|
+
"REMEDIATION_HELD",
|
|
178
|
+
"REMEDIATION_NO_PULL_REQUEST",
|
|
179
|
+
"ROW_VERSION_CONFLICT",
|
|
180
|
+
"DISPATCH_ENQUEUE_FAILED",
|
|
181
|
+
];
|
|
182
|
+
/** True when `code` is one of the closed `rerun-ci` refusal codes. */
|
|
183
|
+
export function isRerunCiRefusalCode(code) {
|
|
184
|
+
return (typeof code === "string" && RERUN_CI_REFUSAL_CODES.includes(code));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The bounded reason a `rerun-ci` `ROW_VERSION_CONFLICT` carries when releasing a
|
|
188
|
+
* reservation lost its guarded write. Whether the unenqueued workflow runs are
|
|
189
|
+
* still recorded is UNCONFIRMED; the CLI exits 3.
|
|
190
|
+
*/
|
|
191
|
+
export const RERUN_CI_UNCONFIRMED_REASON = "rerun_ci_unconfirmed";
|
|
192
|
+
/**
|
|
193
|
+
* The closed per-check outcome vocabulary. None of them says a check is green: a
|
|
194
|
+
* provider re-run is an action, not a verdict.
|
|
195
|
+
*/
|
|
196
|
+
export const RERUN_CI_CHECK_STATUSES = [
|
|
197
|
+
"enqueued",
|
|
198
|
+
"deduplicated",
|
|
199
|
+
"deferred",
|
|
200
|
+
"not_re_runnable",
|
|
201
|
+
];
|
|
202
|
+
/** Why one failed check is not re-run: a closed token. */
|
|
203
|
+
export const RERUN_CI_NOT_RUNNABLE_REASONS = [
|
|
204
|
+
"not_github_actions",
|
|
205
|
+
"no_workflow_run_id",
|
|
206
|
+
"claude_review_excluded",
|
|
207
|
+
];
|
|
208
|
+
/** The closed overall outcomes of an applied `rerun-ci`. */
|
|
209
|
+
export const RERUN_CI_OUTCOMES = ["enqueued", "partial", "nothing_to_enqueue"];
|