@bridge_gpt/mcp-server 0.2.65 → 0.2.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +25 -13
  2. package/build/agent-launchers/claude-executor-adapter.js +12 -18
  3. package/build/agent-launchers/executor-adapter.js +27 -0
  4. package/build/bridge-config.js +104 -0
  5. package/build/codex-bridge-profile.js +10 -1
  6. package/build/commands.generated.js +4 -4
  7. package/build/conductor/bridge-api-client.js +230 -10
  8. package/build/conductor/epic-child-continuation.js +61 -34
  9. package/build/conductor/epic-implementer-cli.js +12 -0
  10. package/build/conductor/guided-repair.js +323 -0
  11. package/build/conductor/readiness-cli.js +30 -9
  12. package/build/conductor/readiness.js +23 -1
  13. package/build/conductor/recovery-cli.js +507 -16
  14. package/build/conductor/recovery-operations.js +325 -2
  15. package/build/conductor/repair-contract.js +61 -3
  16. package/build/credential-store.js +5 -4
  17. package/build/diagnostic-text.js +36 -0
  18. package/build/docs.generated.js +1 -1
  19. package/build/doctor.js +70 -10
  20. package/build/drive-epic.js +395 -78
  21. package/build/epic-implementer-bin.js +24 -22
  22. package/build/epic-launch-diagnostic.js +31 -0
  23. package/build/epic-launch-profile.js +44 -14
  24. package/build/executor/completion-supervision.js +5 -0
  25. package/build/executor/correction-dispatch.js +60 -0
  26. package/build/executor/env.js +6 -0
  27. package/build/executor/http-client.js +15 -0
  28. package/build/executor/integration-job.js +6 -1
  29. package/build/executor/job-errors.js +13 -4
  30. package/build/executor/job-log-registry.js +340 -73
  31. package/build/executor/job-runner.js +260 -33
  32. package/build/executor/job-types.js +8 -13
  33. package/build/executor/preflight.js +91 -2
  34. package/build/executor/prompt-spec.js +113 -9
  35. package/build/executor/runner.js +23 -0
  36. package/build/executor/stale-artifacts.js +5 -4
  37. package/build/executor/types.js +1 -0
  38. package/build/executor/watch-cli.js +35 -8
  39. package/build/executor/worker-command.js +43 -43
  40. package/build/executor/worker-finalization.js +80 -15
  41. package/build/executor/worker-guard-hook.js +46 -10
  42. package/build/executor/worker-log.js +137 -70
  43. package/build/executor/worktree-gc.js +30 -6
  44. package/build/index.js +177 -329
  45. package/build/init.js +107 -4
  46. package/build/install-bridge.js +284 -20
  47. package/build/install-doctor.js +172 -30
  48. package/build/install-reexec.js +69 -7
  49. package/build/macos-terminal-permission-guidance.js +16 -0
  50. package/build/model-thresholds-tool.js +185 -0
  51. package/build/pipelines.generated.js +1 -1
  52. package/build/plane/status.js +4 -0
  53. package/build/plane/supervisor.js +4 -0
  54. package/build/plane/worker-exit-code.js +2 -0
  55. package/build/project-root.js +99 -0
  56. package/build/readme.generated.js +1 -1
  57. package/build/review-tickets.js +156 -13
  58. package/build/setup-epic.js +180 -108
  59. package/build/sfcc/setup-status.js +2 -0
  60. package/build/sfcc/tool-wrapper.js +2 -0
  61. package/build/sfcc/tools-configured.js +96 -0
  62. package/build/start-tickets.js +163 -58
  63. package/build/upgrade-cli.js +109 -24
  64. package/build/version.generated.js +3 -3
  65. package/build/windows-npm-entry.js +103 -0
  66. package/build/worker-guard-hook-bin.js +4 -3
  67. package/build/worktree-core.js +6 -5
  68. package/docs/install/mcp-tool-integrations.md +1 -0
  69. package/docs/install/sfcc-integration.md +12 -7
  70. package/package.json +1 -1
  71. package/pipelines/implement-ticket.json +1 -1
@@ -18,9 +18,10 @@
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, 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, 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
26
  import { RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
26
27
  /**
@@ -453,6 +454,53 @@ 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
+ }
456
504
  const AUTHORITATIVE_OUTCOMES = new Set([
457
505
  "committed",
458
506
  "already_continued",
@@ -644,3 +692,278 @@ export async function lookupContinuationReceipt(access, options, deps = {}) {
644
692
  }
645
693
  return { kind: "not_found", requestId, status };
646
694
  }
695
+ // ---------------------------------------------------------------------------
696
+ // BAPI-1326 — `resume-ci`: a pinned, bounded, allowance-preserving CI resume
697
+ // ---------------------------------------------------------------------------
698
+ /** The recovery-reference namespace of pinned `resume-ci` operations. */
699
+ export const RESUME_CI_REFERENCE_NAMESPACE = "epic-ci-resumes";
700
+ /**
701
+ * Bounded POSTs per invocation. Every POST of one invocation carries the SAME
702
+ * operation key and stop binding; only the row version may be refreshed, and
703
+ * only while every original binding still matches.
704
+ */
705
+ export const RESUME_CI_POST_LIMIT = 3;
706
+ /** The pause before a retry. Injectable so tests never sleep. */
707
+ const RESUME_CI_RETRY_DELAY_MS = 250;
708
+ const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/;
709
+ const FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
710
+ const CI_STOP_REASON_VALUES = new Set(["wait_expired", "verdict_unavailable", "repair_exhausted"]);
711
+ function isRecordValue(value) {
712
+ return value !== null && typeof value === "object" && !Array.isArray(value);
713
+ }
714
+ function isNonNegativeInteger(value) {
715
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
716
+ }
717
+ /** Strictly validate a stop binding; `null` for anything short of the contract. */
718
+ export function parseCiStopBinding(value) {
719
+ if (!isRecordValue(value))
720
+ return null;
721
+ const checks = value.required_checks;
722
+ if (!isNonNegativeInteger(value.park_generation) ||
723
+ !CI_STOP_REASON_VALUES.has(value.reason) ||
724
+ typeof value.accepted_head_sha !== "string" || !FULL_SHA_PATTERN.test(value.accepted_head_sha) ||
725
+ !isNonNegativeInteger(value.pr_number) || value.pr_number < 1 ||
726
+ typeof value.base_ref !== "string" || !value.base_ref || value.base_ref.length > 200 ||
727
+ !Array.isArray(checks) || checks.length > 64 ||
728
+ !checks.every((c) => typeof c === "string" && c.length > 0 && c.length <= 200) ||
729
+ (value.required_checks_source !== "run" && value.required_checks_source !== "plan") ||
730
+ typeof value.required_checks_fingerprint !== "string" ||
731
+ !FINGERPRINT_PATTERN.test(value.required_checks_fingerprint) ||
732
+ !(value.spec_hash === null || value.spec_hash === undefined ||
733
+ (typeof value.spec_hash === "string" && value.spec_hash.length > 0 && value.spec_hash.length <= 128))) {
734
+ return null;
735
+ }
736
+ return {
737
+ park_generation: value.park_generation,
738
+ reason: value.reason,
739
+ accepted_head_sha: value.accepted_head_sha,
740
+ pr_number: value.pr_number,
741
+ base_ref: value.base_ref,
742
+ required_checks: [...checks],
743
+ required_checks_source: value.required_checks_source,
744
+ required_checks_fingerprint: value.required_checks_fingerprint,
745
+ spec_hash: (value.spec_hash ?? null),
746
+ };
747
+ }
748
+ /**
749
+ * Capture the ACTIVE typed CI stop's binding from authenticated ticket state,
750
+ * mirroring the server's selection: the ticket is `needs_human`, its single
751
+ * parked gate is `ci`, and `gates.ci.ci_stop` is a complete typed record for the
752
+ * gate's current park generation. Never derived from the park's reason text.
753
+ */
754
+ export function captureCiStopBinding(ticket) {
755
+ // The client union predates the v2 gate statuses; the server sends them anyway.
756
+ if (ticket.status !== "needs_human")
757
+ return { ok: false, reason: "not_parked" };
758
+ const gates = isRecordValue(ticket.gates) ? ticket.gates : {};
759
+ const parked = Object.entries(gates).filter(([, state]) => isRecordValue(state) && state.status === "needs_human");
760
+ if (parked.length !== 1 || parked[0][0] !== "ci")
761
+ return { ok: false, reason: "not_ci_park" };
762
+ const ci = parked[0][1];
763
+ const stop = ci.ci_stop;
764
+ if (!isRecordValue(stop) || !isRecordValue(stop.evidence))
765
+ return { ok: false, reason: "no_typed_stop" };
766
+ const generation = isNonNegativeInteger(ci.park_generation) ? ci.park_generation : 0;
767
+ if (stop.park_generation !== generation)
768
+ return { ok: false, reason: "stop_superseded" };
769
+ const evidence = stop.evidence;
770
+ const binding = parseCiStopBinding({
771
+ park_generation: stop.park_generation,
772
+ reason: stop.reason,
773
+ accepted_head_sha: stop.accepted_head_sha,
774
+ pr_number: evidence.pr_number,
775
+ base_ref: evidence.base_ref,
776
+ required_checks: evidence.required_checks,
777
+ required_checks_source: evidence.required_checks_source,
778
+ required_checks_fingerprint: evidence.required_checks_fingerprint,
779
+ spec_hash: evidence.spec_hash ?? null,
780
+ });
781
+ return binding === null ? { ok: false, reason: "no_typed_stop" } : { ok: true, binding };
782
+ }
783
+ /** The fingerprint of a pinned operation's identity (the row version excluded). */
784
+ export function computeCiResumeFingerprint(ref) {
785
+ return canonicalSha256({
786
+ v: 1,
787
+ operation: "resume_ci",
788
+ target: { repo_name: ref.repoName, epic_run_id: ref.epicRunId, ticket_key: ref.ticketKey },
789
+ operation_key: ref.operationKey,
790
+ stop_binding: ref.stopBinding,
791
+ });
792
+ }
793
+ function parseCiResumeReference(raw) {
794
+ if (!isRecordValue(raw) || raw.version !== 1 || raw.operation !== "resume_ci")
795
+ return null;
796
+ const binding = parseCiStopBinding(raw.stopBinding);
797
+ if (binding === null ||
798
+ typeof raw.repoName !== "string" || !raw.repoName ||
799
+ typeof raw.epicRunId !== "string" || !raw.epicRunId ||
800
+ typeof raw.ticketKey !== "string" || !raw.ticketKey ||
801
+ !isNonNegativeInteger(raw.expectedRowVersion) ||
802
+ typeof raw.operationKey !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(raw.operationKey) ||
803
+ typeof raw.fingerprint !== "string") {
804
+ return null;
805
+ }
806
+ const ref = {
807
+ version: 1,
808
+ operation: "resume_ci",
809
+ repoName: raw.repoName,
810
+ epicRunId: raw.epicRunId,
811
+ ticketKey: raw.ticketKey,
812
+ expectedRowVersion: raw.expectedRowVersion,
813
+ operationKey: raw.operationKey,
814
+ stopBinding: binding,
815
+ fingerprint: raw.fingerprint,
816
+ createdAt: typeof raw.createdAt === "string" ? raw.createdAt : "",
817
+ };
818
+ // A reference whose fingerprint no longer matches its own identity was
819
+ // tampered with or truncated; it never authorizes a replay.
820
+ return computeCiResumeFingerprint(ref) === ref.fingerprint ? ref : null;
821
+ }
822
+ function sameBinding(a, b) {
823
+ return canonicalSha256(a) === canonicalSha256(b);
824
+ }
825
+ async function readCiResumeReference(key, store) {
826
+ return parseCiResumeReference(await readRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, key, store));
827
+ }
828
+ /**
829
+ * Resume a ticket parked on a qualified CI stop, with its operation pinned.
830
+ *
831
+ * A pinned reference for this ticket is REUSED as-is — same operation key, same
832
+ * stop binding — so a lost response or a restarted process is answered from the
833
+ * server's durable receipt, and a later park is never silently targeted. Only
834
+ * when no reference exists is the binding read (once) from authenticated ticket
835
+ * state, and the reference is stored BEFORE the first POST.
836
+ *
837
+ * At most {@link RESUME_CI_POST_LIMIT} POSTs, all with the same key and binding.
838
+ * A row-version conflict refreshes the row version only while every original
839
+ * binding still matches; otherwise the result is `stale_reference` and nothing
840
+ * more is sent. There is no fallback to unpark, continue-child, or any other
841
+ * mutation.
842
+ */
843
+ export async function resumeCiOnce(access, options, deps = {}) {
844
+ const fetchState = deps.fetchState ?? fetchEpicRunState;
845
+ const post = deps.post ?? resumeCiTicket;
846
+ const store = deps.store ?? {};
847
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
848
+ const callerKey = { epicRunId: options.epicRunId, ticketKey: options.ticketKey };
849
+ let reference = await readCiResumeReference(callerKey, store);
850
+ if (reference !== null && reference.repoName !== access.repoName)
851
+ reference = null;
852
+ const reusedReference = reference !== null;
853
+ const persist = async (ref) => {
854
+ await writeRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, callerKey, ref, store);
855
+ if (ref.epicRunId !== callerKey.epicRunId || ref.ticketKey !== callerKey.ticketKey) {
856
+ await writeRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, { epicRunId: ref.epicRunId, ticketKey: ref.ticketKey }, ref, store);
857
+ }
858
+ };
859
+ const readTicket = async () => {
860
+ try {
861
+ const state = await fetchState(access, options.epicRunId);
862
+ return state.ticket_statuses.find((t) => t.ticket_key === options.ticketKey) ?? null;
863
+ }
864
+ catch {
865
+ return "unavailable";
866
+ }
867
+ };
868
+ if (reference === null) {
869
+ const ticket = await readTicket();
870
+ if (ticket === "unavailable") {
871
+ return { kind: "unavailable", message: "could not read the ticket's state, so nothing was sent" };
872
+ }
873
+ if (ticket === null)
874
+ return { kind: "not_eligible", reason: "ticket_not_found" };
875
+ const captured = captureCiStopBinding(ticket);
876
+ if (!captured.ok)
877
+ return { kind: "not_eligible", reason: captured.reason };
878
+ const identity = {
879
+ repoName: access.repoName,
880
+ epicRunId: ticket.epic_run_id,
881
+ ticketKey: ticket.ticket_key,
882
+ operationKey: (deps.newOperationKey ?? randomUUID)(),
883
+ stopBinding: captured.binding,
884
+ };
885
+ reference = {
886
+ version: 1,
887
+ operation: "resume_ci",
888
+ ...identity,
889
+ expectedRowVersion: ticket.row_version,
890
+ fingerprint: computeCiResumeFingerprint(identity),
891
+ createdAt: (deps.now ?? (() => new Date()))().toISOString(),
892
+ };
893
+ try {
894
+ await persist(reference);
895
+ }
896
+ catch {
897
+ return {
898
+ kind: "unavailable",
899
+ message: "could not store the local recovery reference, so the request was not sent",
900
+ };
901
+ }
902
+ }
903
+ const clear = async (ref) => {
904
+ await clearRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, callerKey, store);
905
+ await clearRecoveryReferenceFile(RESUME_CI_REFERENCE_NAMESPACE, { epicRunId: ref.epicRunId, ticketKey: ref.ticketKey }, store);
906
+ };
907
+ let ref = reference;
908
+ let last = null;
909
+ for (let attempt = 1; attempt <= RESUME_CI_POST_LIMIT; attempt += 1) {
910
+ if (attempt > 1)
911
+ await sleep(RESUME_CI_RETRY_DELAY_MS * (attempt - 1));
912
+ const outcome = await post(access, {
913
+ epicRunId: ref.epicRunId,
914
+ ticketKey: ref.ticketKey,
915
+ expectedRowVersion: ref.expectedRowVersion,
916
+ operationKey: ref.operationKey,
917
+ stopBinding: ref.stopBinding,
918
+ });
919
+ last = outcome;
920
+ if (outcome.kind === "result") {
921
+ await clear(ref);
922
+ return { kind: outcome.result.outcome, result: outcome.result, reusedReference };
923
+ }
924
+ if (outcome.kind === "uncertain")
925
+ continue;
926
+ if (outcome.kind === "refused" && outcome.errorCode === "CI_RESUME_BUSY")
927
+ continue;
928
+ if (outcome.kind === "refused" && outcome.errorCode === "ROW_VERSION_CONFLICT") {
929
+ // Refresh the row version ONLY while the stop is still the same one.
930
+ const ticket = await readTicket();
931
+ if (ticket === "unavailable" || ticket === null) {
932
+ return { kind: "refused", errorCode: outcome.errorCode, status: outcome.status, reusedReference };
933
+ }
934
+ const current = captureCiStopBinding(ticket);
935
+ if (!current.ok || !sameBinding(current.binding, ref.stopBinding)) {
936
+ return { kind: "stale_reference", reusedReference };
937
+ }
938
+ ref = { ...ref, expectedRowVersion: ticket.row_version };
939
+ try {
940
+ await persist(ref);
941
+ }
942
+ catch {
943
+ return {
944
+ kind: "unavailable",
945
+ message: "could not update the local recovery reference, so the retry was not sent",
946
+ };
947
+ }
948
+ continue;
949
+ }
950
+ // Every other refusal or rejection is authoritative: nothing was written,
951
+ // and this operation is finished.
952
+ await clear(ref);
953
+ return {
954
+ kind: "refused",
955
+ errorCode: outcome.errorCode ?? "REQUEST_REJECTED",
956
+ status: outcome.status,
957
+ reusedReference,
958
+ };
959
+ }
960
+ if (last !== null && last.kind === "refused") {
961
+ // Busy or row-version churn for the whole budget: nothing was written, and
962
+ // the pinned reference is kept for the next invocation.
963
+ return { kind: "refused", errorCode: last.errorCode, status: last.status, reusedReference };
964
+ }
965
+ const transport = last !== null && last.kind === "uncertain"
966
+ ? last.status !== undefined ? `${last.reason} (status ${last.status})` : last.reason
967
+ : "indeterminate";
968
+ return { kind: "unconfirmed", transport, reusedReference, attempts: RESUME_CI_POST_LIMIT };
969
+ }
@@ -1,7 +1,8 @@
1
1
  /**
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`.
2
+ * The shared contract for the seven 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`, and BAPI-1326's
5
+ * ticket-addressed `resume-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,7 @@ 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";
23
25
  /** Every repair verb, in the order an operator meets them. */
24
26
  export const OPERATOR_REPAIR_VERBS = [
25
27
  VERB_SCOPE_REPARSE,
@@ -28,6 +30,7 @@ export const OPERATOR_REPAIR_VERBS = [
28
30
  VERB_SCOPE_CATCH_UP,
29
31
  VERB_PATCH_POLICY,
30
32
  VERB_RESUME_RUN,
33
+ VERB_RESUME_CI,
31
34
  ];
32
35
  /**
33
36
  * The ONE cap on driver context, in UTF-8 BYTES — the same number the server
@@ -92,9 +95,64 @@ export const OPERATOR_REPAIR_REFUSAL_CODES = [
92
95
  "RUN_STOPPED",
93
96
  "QUARANTINE_CLEARANCE_REQUIRED",
94
97
  "RUN_CONTROL_CONFLICT",
98
+ // BAPI-1326
99
+ "CI_STOP_NOT_ELIGIBLE",
100
+ "CI_RESUME_STALE_OPERATION",
101
+ "CI_RESUME_OPERATION_CONFLICT",
102
+ "CI_RESUME_BINDING_CHANGED",
103
+ "CI_RESUME_POLICY_CHANGED",
104
+ "CI_RESUME_SPEC_CHANGED",
105
+ "CI_RESUME_CHECKS_NOT_GREEN",
106
+ "CI_RESUME_VERDICT_UNAVAILABLE",
107
+ "CI_RESUME_HELD",
108
+ "CI_RESUME_LIVE_WORK",
109
+ "CI_RESUME_BUDGET_EXHAUSTED",
110
+ "CI_RESUME_BUSY",
111
+ // BAPI-1333
112
+ "GUIDED_REPAIR_DISABLED",
113
+ "REPAIR_SUBJECT_STALE",
95
114
  ];
96
115
  /** True when `code` is one of the closed repair refusal codes. */
97
116
  export function isOperatorRepairRefusalCode(code) {
98
117
  return (typeof code === "string" &&
99
118
  OPERATOR_REPAIR_REFUSAL_CODES.includes(code));
100
119
  }
120
+ // ---------------------------------------------------------------------------
121
+ // BAPI-1326 — `resume-ci`
122
+ // ---------------------------------------------------------------------------
123
+ /**
124
+ * The closed `resume-ci` refusal subset, in the order the server checks them —
125
+ * mirrors `CI_RESUME_REFUSAL_CODES` in `operator_repair_contract.py`.
126
+ */
127
+ export const CI_RESUME_REFUSAL_CODES = [
128
+ "TICKET_NOT_IN_RUN",
129
+ "CI_STOP_NOT_ELIGIBLE",
130
+ "CI_RESUME_STALE_OPERATION",
131
+ "CI_RESUME_OPERATION_CONFLICT",
132
+ "ROW_VERSION_CONFLICT",
133
+ "RUN_NOT_DISPATCHABLE",
134
+ "CI_RESUME_BUDGET_EXHAUSTED",
135
+ "CI_RESUME_HELD",
136
+ "CI_RESUME_LIVE_WORK",
137
+ "CI_RESUME_SPEC_CHANGED",
138
+ "CI_RESUME_POLICY_CHANGED",
139
+ "CI_RESUME_BINDING_CHANGED",
140
+ "CI_RESUME_CHECKS_NOT_GREEN",
141
+ "CI_RESUME_VERDICT_UNAVAILABLE",
142
+ "CI_RESUME_BUSY",
143
+ ];
144
+ /**
145
+ * The closed outcome vocabulary. The server answers `committed` or `replayed`;
146
+ * `refused` is a structured error, and `unconfirmed` is minted only by the client
147
+ * when no durable answer arrived.
148
+ */
149
+ export const CI_RESUME_OUTCOMES = ["committed", "replayed", "refused", "unconfirmed"];
150
+ /**
151
+ * Receipt retention: only the latest committed operation per ticket is kept. A
152
+ * request whose receipt was displaced is refused as stale, never replayed.
153
+ */
154
+ export const CI_RESUME_RECEIPT_RETENTION = "latest_only";
155
+ /** True when `code` is one of the closed `resume-ci` refusal codes. */
156
+ export function isCiResumeRefusalCode(code) {
157
+ return (typeof code === "string" && CI_RESUME_REFUSAL_CODES.includes(code));
158
+ }
@@ -38,6 +38,7 @@
38
38
  * user-scoped `~/.config/bridge/credentials.json` store written here.
39
39
  */
40
40
  import path from "path";
41
+ import { diagnosticComponent } from "./epic-launch-diagnostic.js";
41
42
  // ---------------------------------------------------------------------------
42
43
  // Path resolution
43
44
  // ---------------------------------------------------------------------------
@@ -1497,9 +1498,9 @@ export async function resolveEpicLaunchCredential(repoName, targetBaseUrl, deps,
1497
1498
  return {
1498
1499
  ok: false,
1499
1500
  kind: "binding-unbound",
1500
- error: `The stored credential "${bundleKey}" has not been approved for this Bridge API endpoint ` +
1501
- `(${primaryPath}). Production needs no approval; any other endpoint does. Run guided setup ` +
1502
- `once to bind it: install conductor --launch-profile <name> --base-url ${target}.`,
1501
+ error: `The stored credential "${diagnosticComponent(bundleKey)}" has not been approved for this Bridge API endpoint ` +
1502
+ `(${diagnosticComponent(primaryPath)}). Production needs no approval; any other endpoint does. Run guided setup ` +
1503
+ `once to bind it: install conductor --launch-profile <name> --base-url ${diagnosticComponent(target)}.`,
1503
1504
  };
1504
1505
  }
1505
1506
  // An unreadable read-back is refused like a mismatch: the key was read a
@@ -1508,7 +1509,7 @@ export async function resolveEpicLaunchCredential(repoName, targetBaseUrl, deps,
1508
1509
  return {
1509
1510
  ok: false,
1510
1511
  kind: "binding-mismatch",
1511
- error: `The stored credential "${bundleKey}" is approved for a different Bridge API endpoint than the ` +
1512
+ error: `The stored credential "${diagnosticComponent(bundleKey)}" is approved for a different Bridge API endpoint than the ` +
1512
1513
  "selected target. It was not sent. A key approved for a local server is never sent to " +
1513
1514
  "production: pass that server's --launch-profile <name> or --base-url <url>, or bind the key " +
1514
1515
  "once with install conductor --launch-profile <name> --base-url <url>.",
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Single-line rendering for untrusted values in diagnostics (BAPI-1318).
3
+ *
4
+ * Project names read from `.bridge/config` or an MCP config env block, and paths
5
+ * read from `BAPI_PROJECT_ROOT`, are user-controlled. A newline or other control
6
+ * character in one of them could forge an extra line in a stderr diagnostic or a
7
+ * doctor report. This renderer escapes every ASCII control character and bounds
8
+ * the length, so each value always occupies exactly one line of output.
9
+ */
10
+ /** Longest rendered value; anything longer is cut and marked with an ellipsis. */
11
+ export const DIAGNOSTIC_VALUE_MAX_LENGTH = 200;
12
+ /**
13
+ * Render `value` for a one-line diagnostic: control characters become `\xNN`
14
+ * (with `\n`, `\r`, and `\t` spelled out), and the result is bounded to
15
+ * {@link DIAGNOSTIC_VALUE_MAX_LENGTH} characters.
16
+ */
17
+ export function renderDiagnosticValue(value) {
18
+ let out = "";
19
+ for (const ch of value) {
20
+ const code = ch.charCodeAt(0);
21
+ if (ch === "\n")
22
+ out += "\\n";
23
+ else if (ch === "\r")
24
+ out += "\\r";
25
+ else if (ch === "\t")
26
+ out += "\\t";
27
+ else if (code <= 0x1f || code === 0x7f)
28
+ out += `\\x${code.toString(16).padStart(2, "0")}`;
29
+ else
30
+ out += ch;
31
+ }
32
+ if (out.length > DIAGNOSTIC_VALUE_MAX_LENGTH) {
33
+ out = `${out.slice(0, DIAGNOSTIC_VALUE_MAX_LENGTH)}…`;
34
+ }
35
+ return out;
36
+ }