@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.
Files changed (127) hide show
  1. package/README.md +231 -113
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/agent-registry.js +15 -0
  5. package/build/agent-utils.js +16 -8
  6. package/build/claude-user-config-doctor.js +1 -35
  7. package/build/codex-bridge-profile.js +1 -1
  8. package/build/commands.generated.js +7 -8
  9. package/build/conductor/bridge-api-client.js +202 -2
  10. package/build/conductor/bring-up-facts.js +1 -1
  11. package/build/conductor/claude-hook.js +16 -16
  12. package/build/conductor/doctor.js +279 -72
  13. package/build/conductor/epic-implementer-cli.js +38 -5
  14. package/build/conductor/epic-reconcile.js +1 -1
  15. package/build/conductor/epic-runtime.js +21 -12
  16. package/build/conductor/errors.js +3 -3
  17. package/build/conductor/file-scope-guard.js +4 -4
  18. package/build/conductor/git-ci-types.js +1 -1
  19. package/build/conductor/git-inspection.js +2 -2
  20. package/build/conductor/install-doctor.js +28 -6
  21. package/build/conductor/local-merge.js +319 -26
  22. package/build/conductor/merge-execution-contract.js +61 -0
  23. package/build/conductor/merge-execution-eligibility.js +129 -0
  24. package/build/conductor/merge-ledger.js +2 -2
  25. package/build/conductor/pr-ci-producer.js +6 -6
  26. package/build/conductor/pr-review-producer.js +2 -2
  27. package/build/conductor/producer-ledger.js +2 -0
  28. package/build/conductor/readiness-sections.js +166 -9
  29. package/build/conductor/readiness.js +97 -16
  30. package/build/conductor/recovery-cli.js +243 -12
  31. package/build/conductor/recovery-operations.js +64 -6
  32. package/build/conductor/repair-contract.js +38 -4
  33. package/build/conductor/spec-review-producer.js +6 -6
  34. package/build/conductor/store.js +10 -10
  35. package/build/conductor/supervisor-config.js +14 -14
  36. package/build/conductor/supervisor-ledger.js +2 -2
  37. package/build/conductor/supervisor-message-relay.js +2 -2
  38. package/build/conductor/worker-ledger-cli.js +19 -19
  39. package/build/conductor-bundle-artifacts.js +2 -2
  40. package/build/connect-bitbucket-api.js +6 -1
  41. package/build/connect-bitbucket.js +398 -88
  42. package/build/credential-store.js +16 -0
  43. package/build/docs.generated.js +2 -2
  44. package/build/doctor.js +199 -303
  45. package/build/drive-epic.js +88 -71
  46. package/build/epic-implementer-bin.js +22 -17
  47. package/build/epic-implementer-claude-hook-bin.js +4 -2
  48. package/build/epic-implementer-env.js +184 -0
  49. package/build/epic-integration-pr.js +2 -2
  50. package/build/executor/ci-rerun-job.js +269 -0
  51. package/build/executor/cli.js +31 -0
  52. package/build/executor/credentials.js +11 -0
  53. package/build/executor/env.js +79 -8
  54. package/build/executor/heartbeat.js +1 -1
  55. package/build/executor/index.js +21 -4
  56. package/build/executor/job-errors.js +3 -3
  57. package/build/executor/job-runner.js +127 -1
  58. package/build/executor/job-types.js +12 -2
  59. package/build/executor/merge-job.js +46 -5
  60. package/build/executor/observation.js +1 -1
  61. package/build/executor/preflight.js +1 -1
  62. package/build/executor/prompt-spec.js +1 -1
  63. package/build/executor/reconcile-overlap-job.js +10 -130
  64. package/build/executor/reconcile-overlap-types.js +1 -1
  65. package/build/executor/runner.js +1 -1
  66. package/build/executor/service-lifecycle.js +136 -0
  67. package/build/executor/service-unit.js +427 -9
  68. package/build/executor/types.js +5 -0
  69. package/build/index.js +320 -342
  70. package/build/init.js +20 -28
  71. package/build/install-bridge-conductor.js +208 -15
  72. package/build/install-bridge.js +555 -37
  73. package/build/mcp-host-entry-adapter.js +6 -0
  74. package/build/mcp-host-targets.js +2 -0
  75. package/build/mcp-install-state.js +42 -4
  76. package/build/mcp-invoke.js +5 -5
  77. package/build/mcp-profile.js +2 -2
  78. package/build/mcp-provisioning.js +2 -2
  79. package/build/mcp-server-invocation.js +3 -3
  80. package/build/merge-pull-request.js +118 -10
  81. package/build/merge-workflow-codeowners.js +224 -0
  82. package/build/merge-workflow-config.js +133 -0
  83. package/build/merge-workflow-drift.js +66 -0
  84. package/build/merge-workflow-install.js +638 -0
  85. package/build/merge-workflow.js +572 -0
  86. package/build/model-routing-host.js +25 -0
  87. package/build/model-routing-preferences.js +99 -0
  88. package/build/pipelines.generated.js +10 -10
  89. package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
  90. package/build/plan-execution-ledger.js +8 -0
  91. package/build/plane/cli.js +24 -2
  92. package/build/plane/member-roster.js +5 -5
  93. package/build/plane/preflight.js +68 -0
  94. package/build/plane/supervisor.js +1 -1
  95. package/build/plane/types.js +5 -5
  96. package/build/readiness-check.js +1 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/review-tickets.js +4 -4
  99. package/build/run-unit-tests-launcher.js +5 -0
  100. package/build/setup-epic.js +171 -17
  101. package/build/start-tickets-conductor.js +32 -32
  102. package/build/start-tickets-prereqs.js +4 -9
  103. package/build/start-tickets.js +475 -91
  104. package/build/version.generated.js +3 -3
  105. package/docs/CONDUCTOR.md +69 -18
  106. package/docs/install/mcp-tool-integrations.md +2 -1
  107. package/package.json +4 -4
  108. package/pipelines/implement-ticket.json +1 -1
  109. package/pipelines/plan-epic.json +2 -2
  110. package/build/conduct-epic/bridge-client.js +0 -773
  111. package/build/conduct-epic/checkpoint-store.js +0 -542
  112. package/build/conduct-epic/cli.js +0 -3145
  113. package/build/conduct-epic/cut-protocol.js +0 -392
  114. package/build/conduct-epic/lock.js +0 -302
  115. package/build/conduct-epic/pr-state.js +0 -286
  116. package/build/conduct-epic/spawn.js +0 -113
  117. package/build/conductor/cli.js +0 -1278
  118. package/build/conductor-bundle-cli.js +0 -256
  119. package/build/direct-ticket-tools.js +0 -70
  120. package/build/executor/reconcile-overlap-governance.js +0 -129
  121. package/build/implement-epic/checkpoint-store.js +0 -542
  122. package/build/implement-epic/cli.js +0 -3158
  123. package/build/implement-epic/lock.js +0 -302
  124. package/build/implement-epic/pr-state.js +0 -286
  125. package/build/implement-epic/spawn.js +0 -113
  126. /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
  127. /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
@@ -1,19 +1,21 @@
1
1
  /**
2
- * `conductor` operator recovery and repair verbs — seven CLI-only commands.
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 three BAPI-1154 run-addressed repair
6
- * verbs: `scope-reparse`, `dispatch-remediation`, `merge-child`.
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 seven command names is ever registered as an MCP tool, tool
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`. This surface never accepts, echoes, or asks the
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 `committed` and `event
411
- * recorded` — a replay is a successful no-op (`committed: false`), never a
412
- * refusal. `--json` prints the server's bounded result object exactly, or a
413
- * bounded `{ok:false, kind, code, message}` failure. Nothing here can print a
414
- * prompt, driver context, row version, action key, head SHA, or credential: the
415
- * results carry none of them.
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
- * (BAPI-1154) the three run-addressed repair verbs — scope-reparse,
6
- * dispatch-remediation, merge-child — so
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; everything else (transport, timeout, 5xx
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) }, { env: deps.env ?? process.env });
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 (BAPI-1154):
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/epic_conductor/operator_repair_contract.py` —
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
- /** The closed refusal-code vocabulary, mirroring the server table. */
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 `BAPI_CONDUCTOR_RUN_ID`. The Epic Supervisor's
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 `BAPI_CONDUCTOR_RUN_ID`
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 `BAPI_CONDUCTOR_RUN_ID` (read from the
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?.BAPI_CONDUCTOR_RUN_ID?.trim() || null;
75
- const worker_id = deps.env?.BAPI_CONDUCTOR_WORKER_ID?.trim() || null;
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 BAPI_CONDUCTOR_RUN_ID — verdict uncorrelated)`,
86
+ reason: run_id ? reason : `${reason} (warning: no BAPI_EPIC_IMPLEMENTER_RUN_ID — verdict uncorrelated)`,
87
87
  };
88
88
  }
@@ -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
- * - `BAPI_CONDUCTOR_BUSY_TIMEOUT_MS` (default 10000, clamped [250, 120000]).
195
- * - `BAPI_CONDUCTOR_RETENTION_DAYS` (default 30). A value <= 0 DISABLES
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
- * - `BAPI_CONDUCTOR_RETENTION_MAX_ROWS` (default 50000). A value <= 0 DISABLES
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.BAPI_CONDUCTOR_BUSY_TIMEOUT_MS, BUSY_TIMEOUT_DEFAULT, BUSY_TIMEOUT_MIN, BUSY_TIMEOUT_MAX);
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.BAPI_CONDUCTOR_RETENTION_DAYS;
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.BAPI_CONDUCTOR_RETENTION_MAX_ROWS;
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.BAPI_CONDUCTOR_MESSAGE_COOLDOWN_MS;
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
- : "conductor-supervisor";
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: "conductor-worker",
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: "conductor-worker",
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.BAPI_CONDUCTOR_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_CONDUCTOR_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_CONDUCTOR_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_CONDUCTOR_QUIET_AFTER_MS, QUIET_AFTER_DEFAULT_MS, QUIET_AFTER_MIN_MS, QUIET_AFTER_MAX_MS);
72
- const liveness_stalled_after_ms = parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_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_CONDUCTOR_DEAD_AFTER_MS, DEAD_AFTER_DEFAULT_MS, DEAD_AFTER_MIN_MS, DEAD_AFTER_MAX_MS);
74
- const poll_limit = parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT, POLL_LIMIT_DEFAULT, POLL_LIMIT_MIN, POLL_LIMIT_MAX);
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.BAPI_CONDUCTOR_STALL_NOT_STARTED_MS, 15 * MINUTE_MS, MINUTE_MS, HOUR_MS),
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.BAPI_CONDUCTOR_STALL_ACTIVE_MS, 2 * HOUR_MS, 10 * MINUTE_MS, 12 * HOUR_MS),
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.BAPI_CONDUCTOR_STALL_STALLED_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
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.BAPI_CONDUCTOR_STALL_BLOCKED_MS, 24 * HOUR_MS, 30 * MINUTE_MS, 7 * 24 * HOUR_MS),
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.BAPI_CONDUCTOR_STALL_CANDIDATE_DONE_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
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.BAPI_CONDUCTOR_STALL_VERIFYING_MS, 2 * HOUR_MS, 10 * MINUTE_MS, 12 * HOUR_MS),
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.BAPI_CONDUCTOR_STALL_UNKNOWN_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
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,