@bridge_gpt/mcp-server 0.2.58 → 0.2.60
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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `conductor` operator recovery and repair verbs —
|
|
2
|
+
* `conductor` operator recovery and repair verbs — ten CLI-only commands.
|
|
3
3
|
*
|
|
4
4
|
* The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
|
|
5
|
-
* `adopt-current-head-and-unpark`. The
|
|
6
|
-
*
|
|
5
|
+
* `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
|
|
6
|
+
* `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
|
|
7
|
+
* `scope-catch-up`, `patch-policy`, `resume-run`.
|
|
7
8
|
*
|
|
8
9
|
* CLI-only by design — this is an explicit ticket non-goal, not an oversight:
|
|
9
|
-
* none of these
|
|
10
|
+
* none of these ten command names is ever registered as an MCP tool, tool
|
|
10
11
|
* description, or generated tool-schema input (`index-static.test.ts` guards
|
|
11
12
|
* this). They exist so an operator (or `plane down`, via
|
|
12
13
|
* `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
|
|
13
14
|
* hand-rolling an HTTP request, a DAL call, or a code edit.
|
|
14
15
|
*
|
|
15
16
|
* Every ticket command's `row_version` CAS handling is entirely internal to
|
|
16
|
-
* `recovery-operations.ts
|
|
17
|
+
* `recovery-operations.ts`, and `resume-run`'s control-generation CAS is entirely
|
|
18
|
+
* the server's. This surface never accepts, echoes, or asks the
|
|
17
19
|
* operator for a version counter — see {@link REJECTED_VERSION_FLAGS}. Every
|
|
18
20
|
* repair verb takes a mandatory `--epic-run-id`; the SERVER decides whether its
|
|
19
21
|
* target belongs to that run, and a foreign target is refused with nothing
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
*/
|
|
22
24
|
import { readFile } from "node:fs/promises";
|
|
23
25
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
24
|
-
import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
|
|
26
|
+
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Flag parsing
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
@@ -32,12 +34,15 @@ const RECOVERY_BOOL_FLAGS = new Set(["--json", "--help"]);
|
|
|
32
34
|
* Every spelling of an operator-supplied version counter this surface must
|
|
33
35
|
* refuse outright (BAPI-872) — row_version handling is internal to
|
|
34
36
|
* {@link module:./recovery-operations}, and no ticket command may accept one.
|
|
37
|
+
* BAPI-1176 adds the run-control generation: `resume-run`'s CAS is the server's.
|
|
35
38
|
*/
|
|
36
39
|
const REJECTED_VERSION_FLAGS = new Set([
|
|
37
40
|
"--row-version",
|
|
38
41
|
"--expected-row-version",
|
|
39
42
|
"--row_version",
|
|
40
43
|
"--expected_row_version",
|
|
44
|
+
"--expected-generation",
|
|
45
|
+
"--expected_generation",
|
|
41
46
|
]);
|
|
42
47
|
/**
|
|
43
48
|
* Tokenize `--flag value` / `--flag=value` / boolean flags for the recovery
|
|
@@ -64,6 +69,10 @@ function tokenizeRecoveryFlags(argv, valueFlags, boolFlags) {
|
|
|
64
69
|
"current version itself.");
|
|
65
70
|
}
|
|
66
71
|
if (boolFlags.has(name)) {
|
|
72
|
+
if (bools.has(name)) {
|
|
73
|
+
// Refused like a repeated value flag rather than silently collapsed.
|
|
74
|
+
throw new ConductorValidationError(`Flag "${name}" may be given only once.`);
|
|
75
|
+
}
|
|
67
76
|
bools.add(name);
|
|
68
77
|
continue;
|
|
69
78
|
}
|
|
@@ -407,12 +416,13 @@ export function decodeDriverContextFile(bytes) {
|
|
|
407
416
|
}
|
|
408
417
|
/**
|
|
409
418
|
* Render a repair outcome. Human mode prints the verb and run first, the target
|
|
410
|
-
* second, ONE labeled `Before → After` pair third, then
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
419
|
+
* second, ONE labeled `Before → After` pair third, then whether this call
|
|
420
|
+
* `applied` a change or found it `already current / no change`, then `committed`
|
|
421
|
+
* and `event recorded` — a replay is a successful no-op (`committed: false`),
|
|
422
|
+
* never a refusal. `--json` prints the server's bounded result object exactly, or
|
|
423
|
+
* a bounded `{ok:false, kind, code, message}` failure. Nothing here can print a
|
|
424
|
+
* prompt, driver context, policy value, row version, generation, action key, head
|
|
425
|
+
* SHA, or credential: the results carry none of them.
|
|
416
426
|
*/
|
|
417
427
|
function renderRepairResult(outcome, input, json, extraLines = () => []) {
|
|
418
428
|
if (outcome.ok && outcome.result.committed && !outcome.result.event_recorded) {
|
|
@@ -449,6 +459,7 @@ function renderRepairResult(outcome, input, json, extraLines = () => []) {
|
|
|
449
459
|
`${input.verb} — run ${input.epicRunId}`,
|
|
450
460
|
` target: ${input.targetLabel}`,
|
|
451
461
|
` ${input.stateLabel}: ${result.before ?? "none"} → ${result.after ?? "none"}`,
|
|
462
|
+
` result: ${result.committed ? "applied" : "already current / no change"}`,
|
|
452
463
|
...extraLines(result),
|
|
453
464
|
` committed: ${result.committed}${result.committed ? "" : " (already in this state; no change made)"}`,
|
|
454
465
|
` event recorded: ${result.event_recorded}`,
|
|
@@ -590,3 +601,223 @@ export async function runMergeChildCommand(argv) {
|
|
|
590
601
|
return 1;
|
|
591
602
|
}
|
|
592
603
|
}
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// BAPI-1176 — repair verbs: scope-catch-up, patch-policy, resume-run
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
const SCOPE_CATCH_UP_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id"]);
|
|
608
|
+
const PATCH_POLICY_VALUE_FLAGS = new Set(["--epic-run-id", "--patch-file"]);
|
|
609
|
+
const RESUME_RUN_VALUE_FLAGS = new Set(["--epic-run-id"]);
|
|
610
|
+
/** `--clear-quarantine` is a `resume-run` switch only; no other verb accepts it. */
|
|
611
|
+
const RESUME_RUN_BOOL_FLAGS = new Set(["--json", "--help", "--clear-quarantine"]);
|
|
612
|
+
export function parseScopeCatchUpArgs(argv) {
|
|
613
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, SCOPE_CATCH_UP_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
614
|
+
if (bools.has("--help")) {
|
|
615
|
+
return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
619
|
+
scopeId: requireFlag(values, "--scope-id"),
|
|
620
|
+
json: bools.has("--json"),
|
|
621
|
+
help: false,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
export function parsePatchPolicyArgs(argv) {
|
|
625
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, PATCH_POLICY_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
|
|
626
|
+
if (bools.has("--help")) {
|
|
627
|
+
return { epicRunId: "", patchFile: "", json: bools.has("--json"), help: true };
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
631
|
+
patchFile: requireFlag(values, "--patch-file"),
|
|
632
|
+
json: bools.has("--json"),
|
|
633
|
+
help: false,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
export function parseResumeRunArgs(argv) {
|
|
637
|
+
const { values, bools } = tokenizeRecoveryFlags(argv, RESUME_RUN_VALUE_FLAGS, RESUME_RUN_BOOL_FLAGS);
|
|
638
|
+
if (bools.has("--help")) {
|
|
639
|
+
return {
|
|
640
|
+
epicRunId: "",
|
|
641
|
+
clearQuarantine: bools.has("--clear-quarantine"),
|
|
642
|
+
json: bools.has("--json"),
|
|
643
|
+
help: true,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
epicRunId: requireFlag(values, "--epic-run-id"),
|
|
648
|
+
clearQuarantine: bools.has("--clear-quarantine"),
|
|
649
|
+
json: bools.has("--json"),
|
|
650
|
+
help: false,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Validate and decode a `patch-policy` file's BYTES into a merge-patch object.
|
|
655
|
+
*
|
|
656
|
+
* Checked in the order that keeps a rejected file from being decoded, parsed,
|
|
657
|
+
* resolved against credentials, or transmitted: the raw byte length first (against
|
|
658
|
+
* the SAME `POLICY_PATCH_MAX_BYTES` the server enforces), then a FATAL UTF-8
|
|
659
|
+
* decode, then `JSON.parse`, then the shape — a non-empty JSON object. Every error
|
|
660
|
+
* names its category and never echoes the file's content. A `JSON.parse` message
|
|
661
|
+
* is never forwarded, because V8 quotes the offending text in it.
|
|
662
|
+
*/
|
|
663
|
+
export function decodePolicyPatchFile(bytes) {
|
|
664
|
+
if (bytes.byteLength > POLICY_PATCH_MAX_BYTES) {
|
|
665
|
+
throw new ConductorValidationError(`--patch-file is ${bytes.byteLength} bytes, over POLICY_PATCH_MAX_BYTES ` +
|
|
666
|
+
`(${POLICY_PATCH_MAX_BYTES} UTF-8 bytes). Shorten it and retry; nothing was sent.`);
|
|
667
|
+
}
|
|
668
|
+
let text;
|
|
669
|
+
try {
|
|
670
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
throw new ConductorValidationError("--patch-file must be valid UTF-8 text; nothing was sent.");
|
|
674
|
+
}
|
|
675
|
+
let value;
|
|
676
|
+
try {
|
|
677
|
+
value = JSON.parse(text);
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
throw new ConductorValidationError("--patch-file is not valid JSON; nothing was sent.");
|
|
681
|
+
}
|
|
682
|
+
if (value === null) {
|
|
683
|
+
throw new ConductorValidationError("--patch-file must hold a JSON object, not null; nothing was sent.");
|
|
684
|
+
}
|
|
685
|
+
if (Array.isArray(value)) {
|
|
686
|
+
throw new ConductorValidationError("--patch-file must hold a JSON object, not an array; nothing was sent.");
|
|
687
|
+
}
|
|
688
|
+
if (typeof value !== "object") {
|
|
689
|
+
throw new ConductorValidationError("--patch-file must hold a JSON object, not a scalar value; nothing was sent.");
|
|
690
|
+
}
|
|
691
|
+
if (Object.keys(value).length === 0) {
|
|
692
|
+
throw new ConductorValidationError("--patch-file is an empty object; a merge patch must change at least one key. " +
|
|
693
|
+
"Nothing was sent.");
|
|
694
|
+
}
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
const SCOPE_CATCH_UP_USAGE = [
|
|
698
|
+
"Usage: epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope> [--json]",
|
|
699
|
+
"",
|
|
700
|
+
"Replay merges the run's OWN index scope did not observe, through the same guarded",
|
|
701
|
+
"advance gate the merge webhook uses. There is no force: a blocked advance is",
|
|
702
|
+
"refused (SCOPE_CATCH_UP_REFUSED). The scope must be the one bound to the named",
|
|
703
|
+
"run, or the server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints",
|
|
704
|
+
"the scope lifecycle before → after; it does not wait for a scheduled parse.",
|
|
705
|
+
].join("\n");
|
|
706
|
+
export async function runScopeCatchUpCommand(argv) {
|
|
707
|
+
try {
|
|
708
|
+
const parsed = parseScopeCatchUpArgs(argv);
|
|
709
|
+
if (parsed.help) {
|
|
710
|
+
console.log(SCOPE_CATCH_UP_USAGE);
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
714
|
+
if (!access)
|
|
715
|
+
return 1;
|
|
716
|
+
const { scopeCatchUpRepair } = await import("./recovery-operations.js");
|
|
717
|
+
const outcome = await scopeCatchUpRepair(access, {
|
|
718
|
+
epicRunId: parsed.epicRunId,
|
|
719
|
+
scopeId: parsed.scopeId,
|
|
720
|
+
});
|
|
721
|
+
return renderRepairResult(outcome, {
|
|
722
|
+
verb: "scope-catch-up",
|
|
723
|
+
epicRunId: parsed.epicRunId,
|
|
724
|
+
targetLabel: `scope ${parsed.scopeId}`,
|
|
725
|
+
stateLabel: "scope lifecycle",
|
|
726
|
+
}, parsed.json, (result) => [
|
|
727
|
+
` outcome: ${result.outcome}${result.parse_scheduled ? " (parse scheduled)" : ""}`,
|
|
728
|
+
]);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
732
|
+
return 1;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const PATCH_POLICY_USAGE = [
|
|
736
|
+
"Usage: epic-implementer patch-policy --epic-run-id <id> --patch-file <path> [--json]",
|
|
737
|
+
"",
|
|
738
|
+
"Apply a JSON merge patch (RFC 7396) to the run's policy ON THE SERVER: it reads",
|
|
739
|
+
"the stored policy, merges your patch, validates the result, and writes it. You",
|
|
740
|
+
"never send or see the whole policy, so a value you cannot see is never dropped.",
|
|
741
|
+
"A null deletes a key. Protected paths (base_branch, notify.webhook_url, and",
|
|
742
|
+
"server-retained keys) are refused. For a whole replacement use",
|
|
743
|
+
"`setup-epic --replace-policy`.",
|
|
744
|
+
"",
|
|
745
|
+
"--patch-file must hold a non-empty JSON object of at most POLICY_PATCH_MAX_BYTES",
|
|
746
|
+
`(${POLICY_PATCH_MAX_BYTES} bytes). Its contents are never printed; the result names only`,
|
|
747
|
+
"the changed dotted paths and before/after policy digest tokens.",
|
|
748
|
+
].join("\n");
|
|
749
|
+
export async function runPatchPolicyCommand(argv, deps = {}) {
|
|
750
|
+
try {
|
|
751
|
+
const parsed = parsePatchPolicyArgs(argv);
|
|
752
|
+
if (parsed.help) {
|
|
753
|
+
console.log(PATCH_POLICY_USAGE);
|
|
754
|
+
return 0;
|
|
755
|
+
}
|
|
756
|
+
// Read and validate the patch BEFORE credentials are resolved or the HTTP
|
|
757
|
+
// graph is loaded, so an invalid file costs nothing and reaches no one.
|
|
758
|
+
let bytes;
|
|
759
|
+
try {
|
|
760
|
+
bytes = await (deps.readFile ?? ((p) => readFile(p)))(parsed.patchFile);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
throw new ConductorValidationError("--patch-file could not be read; nothing was sent.");
|
|
764
|
+
}
|
|
765
|
+
const mergePatch = decodePolicyPatchFile(bytes);
|
|
766
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
767
|
+
if (!access)
|
|
768
|
+
return 1;
|
|
769
|
+
const { patchPolicyRepair } = await import("./recovery-operations.js");
|
|
770
|
+
const outcome = await patchPolicyRepair(access, {
|
|
771
|
+
epicRunId: parsed.epicRunId,
|
|
772
|
+
mergePatch,
|
|
773
|
+
});
|
|
774
|
+
return renderRepairResult(outcome, {
|
|
775
|
+
verb: "patch-policy",
|
|
776
|
+
epicRunId: parsed.epicRunId,
|
|
777
|
+
targetLabel: "run policy",
|
|
778
|
+
stateLabel: "policy digest",
|
|
779
|
+
}, parsed.json, (result) => [
|
|
780
|
+
` policy paths: ${result.policy_paths.length > 0 ? result.policy_paths.join(", ") : "none"}`,
|
|
781
|
+
]);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
785
|
+
return 1;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const RESUME_RUN_USAGE = [
|
|
789
|
+
"Usage: epic-implementer resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
|
|
790
|
+
"",
|
|
791
|
+
"Clear a paused run's control record and return it to active, after the server",
|
|
792
|
+
"re-checks the run's activation requirements. A quarantined run resumes only with",
|
|
793
|
+
"--clear-quarantine; a stopped run never resumes (start a new run). A run that is",
|
|
794
|
+
"already active with no control record is a successful no-op. The server retries",
|
|
795
|
+
"a concurrent control change itself — never supply a generation.",
|
|
796
|
+
].join("\n");
|
|
797
|
+
export async function runResumeRunCommand(argv) {
|
|
798
|
+
try {
|
|
799
|
+
const parsed = parseResumeRunArgs(argv);
|
|
800
|
+
if (parsed.help) {
|
|
801
|
+
console.log(RESUME_RUN_USAGE);
|
|
802
|
+
return 0;
|
|
803
|
+
}
|
|
804
|
+
const access = await resolveRecoveryAccess(parsed.json);
|
|
805
|
+
if (!access)
|
|
806
|
+
return 1;
|
|
807
|
+
const { resumeRunRepair } = await import("./recovery-operations.js");
|
|
808
|
+
const outcome = await resumeRunRepair(access, {
|
|
809
|
+
epicRunId: parsed.epicRunId,
|
|
810
|
+
clearQuarantine: parsed.clearQuarantine,
|
|
811
|
+
});
|
|
812
|
+
return renderRepairResult(outcome, {
|
|
813
|
+
verb: "resume-run",
|
|
814
|
+
epicRunId: parsed.epicRunId,
|
|
815
|
+
targetLabel: parsed.clearQuarantine ? "run control (clear quarantine)" : "run control",
|
|
816
|
+
stateLabel: "run control",
|
|
817
|
+
}, parsed.json);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
|
|
821
|
+
return 1;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Shared Bridge API operator-recovery operations (BAPI-872).
|
|
3
3
|
*
|
|
4
4
|
* Centralizes stop, abandon, ticket unpark/adopt-current-head-and-unpark, and
|
|
5
|
-
*
|
|
6
|
-
* dispatch-remediation, merge-child
|
|
5
|
+
* the six run-addressed repair verbs — (BAPI-1154) scope-reparse,
|
|
6
|
+
* dispatch-remediation, merge-child and (BAPI-1176) scope-catch-up,
|
|
7
|
+
* patch-policy, resume-run — so
|
|
7
8
|
* the conductor CLI recovery verbs (`recovery-cli.ts`) and `plane down`
|
|
8
9
|
* (`../plane/shutdown.ts`, wired through `../plane/cli.ts`) share EXACTLY the
|
|
9
10
|
* same stop semantics, and so this module — never a caller — owns every
|
|
@@ -17,9 +18,9 @@
|
|
|
17
18
|
* callers: `recovery-cli.ts` and `plane/shutdown.ts`.
|
|
18
19
|
*/
|
|
19
20
|
import { randomUUID } from "crypto";
|
|
20
|
-
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, safeDiagnosticMessage, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
21
|
+
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
21
22
|
import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
|
|
22
|
-
import { resolveLocalMergeMethod, runApprovedLocalMerge } from "./local-merge.js";
|
|
23
|
+
import { resolveLocalMergeCredential, resolveLocalMergeMethod, runApprovedLocalMerge, } from "./local-merge.js";
|
|
23
24
|
/**
|
|
24
25
|
* Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
|
|
25
26
|
* begins with a fresh, authoritative state read — so this bounds full
|
|
@@ -223,7 +224,11 @@ export async function adoptCurrentHeadAndUnparkWithRetry(access, options) {
|
|
|
223
224
|
}
|
|
224
225
|
/**
|
|
225
226
|
* Classify a thrown client error. A 4xx carrying a server `error_code` is a
|
|
226
|
-
* NAMED refusal and keeps that code
|
|
227
|
+
* NAMED refusal and keeps that code — whether it is one of the closed repair
|
|
228
|
+
* codes in `repair-contract.ts` or an established pass-through code the server's
|
|
229
|
+
* own posture, activation, or run-control checks emit (`RUN_CONTROL_MALFORMED`,
|
|
230
|
+
* for example). Both are rendered from server-owned error data; this layer never
|
|
231
|
+
* rewrites or re-infers a code. Everything else (transport, timeout, 5xx
|
|
227
232
|
* without a code, an unparseable body) is `unavailable` with a bounded
|
|
228
233
|
* diagnostic. No raw body, URL, header, or credential ever reaches either shape.
|
|
229
234
|
*/
|
|
@@ -307,7 +312,13 @@ export async function mergeChildRepair(access, options, deps = {}) {
|
|
|
307
312
|
// which is discarded — never the server's action key, which the server
|
|
308
313
|
// recomposes itself at completion.
|
|
309
314
|
action_key: `merge-child:${options.epicRunId}:${options.prNumber}`,
|
|
310
|
-
}, { method: resolveLocalMergeMethod(local.merge_method) },
|
|
315
|
+
}, { method: resolveLocalMergeMethod(local.merge_method) },
|
|
316
|
+
// BAPI-1136: an explicitly configured local credential, never the ambient
|
|
317
|
+
// login. `env` remains the source of the allowlisted operational base only.
|
|
318
|
+
{
|
|
319
|
+
env: deps.env ?? process.env,
|
|
320
|
+
credential: resolveLocalMergeCredential(deps.env ?? process.env, "local"),
|
|
321
|
+
});
|
|
311
322
|
succeeded = localResult.status === "succeeded";
|
|
312
323
|
alreadyMerged = succeeded && localResult.reason === "already_merged";
|
|
313
324
|
failureReason = boundedLocalMergeReason(localResult.reason);
|
|
@@ -340,3 +351,50 @@ export async function mergeChildRepair(access, options, deps = {}) {
|
|
|
340
351
|
return classifyRepairError(err, "merge-child completion failed");
|
|
341
352
|
}
|
|
342
353
|
}
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// BAPI-1176 — scope-catch-up, patch-policy, resume-run
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Thin like scope-reparse: one mutation call, the server's result returned
|
|
358
|
+
// verbatim, every failure through the shared classifier.
|
|
359
|
+
/**
|
|
360
|
+
* Catch the run's own index scope up with merges it did not observe. The server
|
|
361
|
+
* decides ownership, and a blocked advance is a refusal — never a forced repair.
|
|
362
|
+
*/
|
|
363
|
+
export async function scopeCatchUpRepair(access, options, deps = {}) {
|
|
364
|
+
try {
|
|
365
|
+
const call = deps.scopeCatchUp ?? scopeCatchUp;
|
|
366
|
+
return { ok: true, result: await call(access, options) };
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
return classifyRepairError(err, "scope-catch-up request failed");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Apply a merge patch to the run's policy. `mergePatch` must already be a
|
|
374
|
+
* validated non-empty object within `POLICY_PATCH_MAX_BYTES`; the server does the
|
|
375
|
+
* read, merge, validation, and guarded write, and reports only path names and
|
|
376
|
+
* digest tokens.
|
|
377
|
+
*/
|
|
378
|
+
export async function patchPolicyRepair(access, options, deps = {}) {
|
|
379
|
+
try {
|
|
380
|
+
const call = deps.patchPolicy ?? patchPolicy;
|
|
381
|
+
return { ok: true, result: await call(access, options) };
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
return classifyRepairError(err, "patch-policy request failed");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Resume a paused run, or a quarantined one with `clearQuarantine`. The server
|
|
389
|
+
* re-evaluates activation and owns the control-generation CAS retry; nothing here
|
|
390
|
+
* reads or sends a generation.
|
|
391
|
+
*/
|
|
392
|
+
export async function resumeRunRepair(access, options, deps = {}) {
|
|
393
|
+
try {
|
|
394
|
+
const call = deps.resumeRun ?? resumeRun;
|
|
395
|
+
return { ok: true, result: await call(access, options) };
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
return classifyRepairError(err, "resume-run request failed");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared contract for the run-addressed operator repair verbs
|
|
3
|
-
* `scope-reparse`, `dispatch-remediation`, and `merge-child
|
|
2
|
+
* The shared contract for the six run-addressed operator repair verbs: BAPI-1154's
|
|
3
|
+
* `scope-reparse`, `dispatch-remediation`, and `merge-child`, and BAPI-1176's
|
|
4
|
+
* `scope-catch-up`, `patch-policy`, and `resume-run`.
|
|
4
5
|
*
|
|
5
6
|
* CLI-only, exactly like the BAPI-872 recovery verbs: none of these names is ever
|
|
6
7
|
* registered as an MCP tool (`index-static.test.ts` pins that). This module is the
|
|
7
|
-
* TypeScript mirror of `api/library/
|
|
8
|
+
* TypeScript mirror of `api/library/epic_implementer/operator_repair_contract.py` —
|
|
8
9
|
* the verb names, the CLOSED refusal-code table, the driver-context byte cap, and
|
|
9
10
|
* the bounded result shapes the Bridge client returns. A parity test keeps the two
|
|
10
11
|
* aligned; the CLI renders the server's code and never infers ownership or state
|
|
@@ -16,11 +17,17 @@
|
|
|
16
17
|
export const VERB_SCOPE_REPARSE = "scope-reparse";
|
|
17
18
|
export const VERB_DISPATCH_REMEDIATION = "dispatch-remediation";
|
|
18
19
|
export const VERB_MERGE_CHILD = "merge-child";
|
|
20
|
+
export const VERB_SCOPE_CATCH_UP = "scope-catch-up";
|
|
21
|
+
export const VERB_PATCH_POLICY = "patch-policy";
|
|
22
|
+
export const VERB_RESUME_RUN = "resume-run";
|
|
19
23
|
/** Every repair verb, in the order an operator meets them. */
|
|
20
24
|
export const OPERATOR_REPAIR_VERBS = [
|
|
21
25
|
VERB_SCOPE_REPARSE,
|
|
22
26
|
VERB_DISPATCH_REMEDIATION,
|
|
23
27
|
VERB_MERGE_CHILD,
|
|
28
|
+
VERB_SCOPE_CATCH_UP,
|
|
29
|
+
VERB_PATCH_POLICY,
|
|
30
|
+
VERB_RESUME_RUN,
|
|
24
31
|
];
|
|
25
32
|
/**
|
|
26
33
|
* The ONE cap on driver context, in UTF-8 BYTES — the same number the server
|
|
@@ -29,7 +36,24 @@ export const OPERATOR_REPAIR_VERBS = [
|
|
|
29
36
|
* see the operator's complete instruction or none of it.
|
|
30
37
|
*/
|
|
31
38
|
export const DRIVER_CONTEXT_MAX_BYTES = 16384;
|
|
32
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The ONE cap on a `patch-policy` merge patch, in UTF-8 BYTES — the same number the
|
|
41
|
+
* server enforces (`POLICY_PATCH_MAX_BYTES` in `operator_repair_contract.py`). The
|
|
42
|
+
* CLI checks the patch file's raw bytes before decoding it, so an over-cap patch is
|
|
43
|
+
* never parsed, printed, or transmitted.
|
|
44
|
+
*/
|
|
45
|
+
export const POLICY_PATCH_MAX_BYTES = 16384;
|
|
46
|
+
/**
|
|
47
|
+
* The closed refusal-code vocabulary, mirroring the server table.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately NOT every code a repair verb can render. `patch-policy` and
|
|
50
|
+
* `resume-run` run the server's established posture and unattended-activation
|
|
51
|
+
* checks, and `resume-run` its run-control validation; those refuse with their OWN
|
|
52
|
+
* structured codes (for example `RUN_CONTROL_MALFORMED`). Such pass-through codes
|
|
53
|
+
* are not members of this table, yet the CLI renders them exactly like the closed
|
|
54
|
+
* codes: every refusal is classified from the server's structured `error_code`,
|
|
55
|
+
* never from this list and never from error prose.
|
|
56
|
+
*/
|
|
33
57
|
export const OPERATOR_REPAIR_REFUSAL_CODES = [
|
|
34
58
|
"TICKET_NOT_IN_RUN",
|
|
35
59
|
"DRIVER_CONTEXT_INVALID",
|
|
@@ -50,6 +74,16 @@ export const OPERATOR_REPAIR_REFUSAL_CODES = [
|
|
|
50
74
|
"MERGE_GATE_UNRESOLVABLE",
|
|
51
75
|
"MERGE_NOT_COMPLETED",
|
|
52
76
|
"MERGE_COMPLETION_REFUSED",
|
|
77
|
+
// BAPI-1176
|
|
78
|
+
"SCOPE_CATCH_UP_REFUSED",
|
|
79
|
+
"SCOPE_CATCH_UP_UNAVAILABLE",
|
|
80
|
+
"POLICY_PATCH_INVALID",
|
|
81
|
+
"POLICY_PATH_NOT_PATCHABLE",
|
|
82
|
+
"POLICY_CHANGED_CONCURRENTLY",
|
|
83
|
+
"RUN_NOT_RESUMABLE",
|
|
84
|
+
"RUN_STOPPED",
|
|
85
|
+
"QUARANTINE_CLEARANCE_REQUIRED",
|
|
86
|
+
"RUN_CONTROL_CONFLICT",
|
|
53
87
|
];
|
|
54
88
|
/** True when `code` is one of the closed repair refusal codes. */
|
|
55
89
|
export function isOperatorRepairRefusalCode(code) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Emits the spec re-review verdict (`spec_review.passed` /
|
|
5
5
|
* `spec_review.changes_requested`) into the LOCAL conductor ledger, correlated
|
|
6
|
-
* to the spec-review run via `
|
|
6
|
+
* to the spec-review run via `BAPI_EPIC_IMPLEMENTER_RUN_ID`. The Epic Supervisor's
|
|
7
7
|
* reconcile pass folds these signals: `passed` → `reviewing → ready` (proceed to
|
|
8
8
|
* implementation), `changes_requested` → `blocked` (escalate; do NOT implement).
|
|
9
9
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ledger is a per-worker SQLite file (`~/.config/bridge/events.db`). The
|
|
12
12
|
* Heroku-hosted Bridge API backend has no access to it. So — exactly like the
|
|
13
13
|
* PR-review producer ({@link ./pr-review-producer}) — the verdict is emitted by
|
|
14
|
-
* the LOCAL spec-review run, whose environment carries `
|
|
14
|
+
* the LOCAL spec-review run, whose environment carries `BAPI_EPIC_IMPLEMENTER_RUN_ID`
|
|
15
15
|
* (injected at dispatch time by {@link ../review-tickets}.orchestrateReviewTickets
|
|
16
16
|
* when an epic identity is present). This corrects the original BAPI-445 plan,
|
|
17
17
|
* which proposed emitting from the backend review webhook
|
|
@@ -64,15 +64,15 @@ export function buildSpecReviewVerdictEventInput(ticketKey, verdict, reason, run
|
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Emit a spec re-review verdict for `ticketKey` into the local conductor ledger,
|
|
67
|
-
* correlated to the spec-review run via `
|
|
67
|
+
* correlated to the spec-review run via `BAPI_EPIC_IMPLEMENTER_RUN_ID` (read from the
|
|
68
68
|
* injected env). Idempotent: deduped by `event_type` + `run_id`, so re-emitting
|
|
69
69
|
* the same run's verdict is a no-op. A missing run id still emits (uncorrelated)
|
|
70
70
|
* but is logged in the result so callers can detect the misconfiguration.
|
|
71
71
|
*/
|
|
72
72
|
export async function emitSpecReviewVerdict(ticketKey, verdict, reason, deps = {}) {
|
|
73
73
|
const emitIfNew = deps.emitIfNew ?? emitConductorEventIfNew;
|
|
74
|
-
const run_id = deps.env?.
|
|
75
|
-
const worker_id = deps.env?.
|
|
74
|
+
const run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
|
|
75
|
+
const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
|
|
76
76
|
const eventType = verdict === "passed" ? SPEC_REVIEW_PASSED : SPEC_REVIEW_CHANGES_REQUESTED;
|
|
77
77
|
const event = buildSpecReviewVerdictEventInput(ticketKey, verdict, reason, run_id, worker_id);
|
|
78
78
|
const decision = await emitIfNew(event, {
|
|
@@ -83,6 +83,6 @@ export async function emitSpecReviewVerdict(ticketKey, verdict, reason, deps = {
|
|
|
83
83
|
emitted: decision.emitted,
|
|
84
84
|
event_type: eventType,
|
|
85
85
|
run_id,
|
|
86
|
-
reason: run_id ? reason : `${reason} (warning: no
|
|
86
|
+
reason: run_id ? reason : `${reason} (warning: no BAPI_EPIC_IMPLEMENTER_RUN_ID — verdict uncorrelated)`,
|
|
87
87
|
};
|
|
88
88
|
}
|
package/build/conductor/store.js
CHANGED
|
@@ -191,16 +191,16 @@ function parseBoundedInt(raw, fallback, min, max) {
|
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
193
|
* Resolve store configuration from environment with safe defaults and bounds.
|
|
194
|
-
* - `
|
|
195
|
-
* - `
|
|
194
|
+
* - `BAPI_EPIC_IMPLEMENTER_BUSY_TIMEOUT_MS` (default 10000, clamped [250, 120000]).
|
|
195
|
+
* - `BAPI_EPIC_IMPLEMENTER_RETENTION_DAYS` (default 30). A value <= 0 DISABLES
|
|
196
196
|
* age-based retention; positive values are clamped to [1, 3650].
|
|
197
|
-
* - `
|
|
197
|
+
* - `BAPI_EPIC_IMPLEMENTER_RETENTION_MAX_ROWS` (default 50000). A value <= 0 DISABLES
|
|
198
198
|
* the row cap; positive values are clamped to [100, 10000000].
|
|
199
199
|
*/
|
|
200
200
|
export function resolveConductorStoreConfig(env = process.env) {
|
|
201
|
-
const busy_timeout_ms = parseBoundedInt(env.
|
|
201
|
+
const busy_timeout_ms = parseBoundedInt(env.BAPI_EPIC_IMPLEMENTER_BUSY_TIMEOUT_MS, BUSY_TIMEOUT_DEFAULT, BUSY_TIMEOUT_MIN, BUSY_TIMEOUT_MAX);
|
|
202
202
|
// Retention days: <= 0 disables (sentinel 0); positive values clamped.
|
|
203
|
-
const rawDays = env.
|
|
203
|
+
const rawDays = env.BAPI_EPIC_IMPLEMENTER_RETENTION_DAYS;
|
|
204
204
|
let retention_days = RETENTION_DAYS_DEFAULT;
|
|
205
205
|
if (rawDays !== undefined && rawDays.trim().length > 0) {
|
|
206
206
|
const parsed = Number.parseInt(rawDays.trim(), 10);
|
|
@@ -215,7 +215,7 @@ export function resolveConductorStoreConfig(env = process.env) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
// Retention max rows: <= 0 disables (sentinel 0); positive values clamped.
|
|
218
|
-
const rawRows = env.
|
|
218
|
+
const rawRows = env.BAPI_EPIC_IMPLEMENTER_RETENTION_MAX_ROWS;
|
|
219
219
|
let retention_max_rows = RETENTION_MAX_ROWS_DEFAULT;
|
|
220
220
|
if (rawRows !== undefined && rawRows.trim().length > 0) {
|
|
221
221
|
const parsed = Number.parseInt(rawRows.trim(), 10);
|
|
@@ -230,7 +230,7 @@ export function resolveConductorStoreConfig(env = process.env) {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
// Message relay cooldown: <= 0 disables (sentinel 0); positive values clamped.
|
|
233
|
-
const rawCooldown = env.
|
|
233
|
+
const rawCooldown = env.BAPI_EPIC_IMPLEMENTER_MESSAGE_COOLDOWN_MS;
|
|
234
234
|
let message_cooldown_ms = MESSAGE_COOLDOWN_DEFAULT_MS;
|
|
235
235
|
if (rawCooldown !== undefined && rawCooldown.trim().length > 0) {
|
|
236
236
|
const parsed = Number.parseInt(rawCooldown.trim(), 10);
|
|
@@ -1211,7 +1211,7 @@ export async function sendWorkerMessage(input, config = resolveConductorStoreCon
|
|
|
1211
1211
|
: config.message_cooldown_ms;
|
|
1212
1212
|
const source = typeof input.source === "string" && input.source.trim().length > 0
|
|
1213
1213
|
? input.source.trim()
|
|
1214
|
-
: "
|
|
1214
|
+
: "epic-implementer-supervisor";
|
|
1215
1215
|
const producer = typeof input.producer === "string" && input.producer.trim().length > 0
|
|
1216
1216
|
? input.producer.trim()
|
|
1217
1217
|
: "worker-message-relay";
|
|
@@ -1334,7 +1334,7 @@ export async function checkWorkerMessages(input, config = resolveConductorStoreC
|
|
|
1334
1334
|
continue;
|
|
1335
1335
|
insertRelayAuditEvent(db, {
|
|
1336
1336
|
id: `message.delivered:${row.id}`,
|
|
1337
|
-
source: "
|
|
1337
|
+
source: "epic-implementer-worker",
|
|
1338
1338
|
type: "message.delivered",
|
|
1339
1339
|
run_id: row.run_id,
|
|
1340
1340
|
worker_id: row.worker_id,
|
|
@@ -1349,7 +1349,7 @@ export async function checkWorkerMessages(input, config = resolveConductorStoreC
|
|
|
1349
1349
|
toAcked.run({ seq: row.seq });
|
|
1350
1350
|
insertRelayAuditEvent(db, {
|
|
1351
1351
|
id: `message.acked:${row.id}`,
|
|
1352
|
-
source: "
|
|
1352
|
+
source: "epic-implementer-worker",
|
|
1353
1353
|
type: "message.acked",
|
|
1354
1354
|
run_id: row.run_id,
|
|
1355
1355
|
worker_id: row.worker_id,
|
|
@@ -65,13 +65,13 @@ const POLL_LIMIT_MAX = 1000;
|
|
|
65
65
|
* tests/builds.
|
|
66
66
|
*/
|
|
67
67
|
export function resolveSupervisorConfig(overrides = {}, env = process.env) {
|
|
68
|
-
const wake_interval_ms = clampOverride(overrides.wake_interval_ms, parseBoundedSupervisorInt(env.
|
|
69
|
-
const global_timeout_ms = clampOverride(overrides.global_timeout_ms, parseBoundedSupervisorInt(env.
|
|
70
|
-
const escalation_cooldown_ms = clampOverride(overrides.escalation_cooldown_ms, parseBoundedSupervisorInt(env.
|
|
71
|
-
const quiet_after_ms = parseBoundedSupervisorInt(env.
|
|
72
|
-
const liveness_stalled_after_ms = parseBoundedSupervisorInt(env.
|
|
73
|
-
const dead_after_ms = parseBoundedSupervisorInt(env.
|
|
74
|
-
const poll_limit = parseBoundedSupervisorInt(env.
|
|
68
|
+
const wake_interval_ms = clampOverride(overrides.wake_interval_ms, parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_WAKE_INTERVAL_MS, WAKE_INTERVAL_DEFAULT_MS, WAKE_INTERVAL_MIN_MS, WAKE_INTERVAL_MAX_MS), WAKE_INTERVAL_MIN_MS, WAKE_INTERVAL_MAX_MS);
|
|
69
|
+
const global_timeout_ms = clampOverride(overrides.global_timeout_ms, parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_GLOBAL_TIMEOUT_MS, GLOBAL_TIMEOUT_DEFAULT_MS, GLOBAL_TIMEOUT_MIN_MS, GLOBAL_TIMEOUT_MAX_MS), GLOBAL_TIMEOUT_MIN_MS, GLOBAL_TIMEOUT_MAX_MS);
|
|
70
|
+
const escalation_cooldown_ms = clampOverride(overrides.escalation_cooldown_ms, parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_ESCALATION_COOLDOWN_MS, ESCALATION_COOLDOWN_DEFAULT_MS, ESCALATION_COOLDOWN_MIN_MS, ESCALATION_COOLDOWN_MAX_MS), ESCALATION_COOLDOWN_MIN_MS, ESCALATION_COOLDOWN_MAX_MS);
|
|
71
|
+
const quiet_after_ms = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_QUIET_AFTER_MS, QUIET_AFTER_DEFAULT_MS, QUIET_AFTER_MIN_MS, QUIET_AFTER_MAX_MS);
|
|
72
|
+
const liveness_stalled_after_ms = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_LIVENESS_STALLED_AFTER_MS, LIVENESS_STALLED_AFTER_DEFAULT_MS, LIVENESS_STALLED_AFTER_MIN_MS, LIVENESS_STALLED_AFTER_MAX_MS);
|
|
73
|
+
const dead_after_ms = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_DEAD_AFTER_MS, DEAD_AFTER_DEFAULT_MS, DEAD_AFTER_MIN_MS, DEAD_AFTER_MAX_MS);
|
|
74
|
+
const poll_limit = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_SUPERVISOR_POLL_LIMIT, POLL_LIMIT_DEFAULT, POLL_LIMIT_MIN, POLL_LIMIT_MAX);
|
|
75
75
|
return {
|
|
76
76
|
wake_interval_ms,
|
|
77
77
|
global_timeout_ms,
|
|
@@ -95,19 +95,19 @@ function clampOverride(override, base, min, max) {
|
|
|
95
95
|
function resolveStallThresholds(env) {
|
|
96
96
|
return {
|
|
97
97
|
// A worker that never starts should surface quickly.
|
|
98
|
-
not_started: parseBoundedSupervisorInt(env.
|
|
98
|
+
not_started: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_NOT_STARTED_MS, 15 * MINUTE_MS, MINUTE_MS, HOUR_MS),
|
|
99
99
|
// Active work (tests/builds) is given a long, tolerant budget.
|
|
100
|
-
active: parseBoundedSupervisorInt(env.
|
|
100
|
+
active: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_ACTIVE_MS, 2 * HOUR_MS, 10 * MINUTE_MS, 12 * HOUR_MS),
|
|
101
101
|
// Already-stalled workers re-escalate on a slower cadence.
|
|
102
|
-
stalled: parseBoundedSupervisorInt(env.
|
|
102
|
+
stalled: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_STALLED_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
|
|
103
103
|
// Blocked workers may legitimately wait a long time (human input, deps).
|
|
104
|
-
blocked: parseBoundedSupervisorInt(env.
|
|
104
|
+
blocked: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_BLOCKED_MS, 24 * HOUR_MS, 30 * MINUTE_MS, 7 * 24 * HOUR_MS),
|
|
105
105
|
// A done candidate that never gets verified should surface.
|
|
106
|
-
candidate_done: parseBoundedSupervisorInt(env.
|
|
106
|
+
candidate_done: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_CANDIDATE_DONE_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
|
|
107
107
|
// Verification (CI) gets a long, tolerant budget like active work.
|
|
108
|
-
verifying: parseBoundedSupervisorInt(env.
|
|
108
|
+
verifying: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_VERIFYING_MS, 2 * HOUR_MS, 10 * MINUTE_MS, 12 * HOUR_MS),
|
|
109
109
|
// Unknown-state workers surface on a moderate cadence.
|
|
110
|
-
unknown: parseBoundedSupervisorInt(env.
|
|
110
|
+
unknown: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_UNKNOWN_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
|
|
111
111
|
// Terminal states never "stall"; large sentinels keep arithmetic uniform.
|
|
112
112
|
complete: Number.MAX_SAFE_INTEGER,
|
|
113
113
|
failed: Number.MAX_SAFE_INTEGER,
|