@bridge_gpt/mcp-server 0.2.65 → 0.2.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +269 -82
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +55 -34
  4. package/build/agent-launchers/executor-adapter.js +34 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/bridge-config.js +104 -0
  8. package/build/codex-bridge-profile.js +10 -1
  9. package/build/commands.generated.js +12 -12
  10. package/build/conductor/bridge-api-client.js +372 -10
  11. package/build/conductor/bring-up-facts.js +1 -1
  12. package/build/conductor/doctor.js +3 -3
  13. package/build/conductor/epic-child-continuation.js +61 -34
  14. package/build/conductor/epic-implementer-cli.js +34 -6
  15. package/build/conductor/errors.js +4 -3
  16. package/build/conductor/guided-repair.js +327 -0
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/merge-execution-contract.js +27 -8
  19. package/build/conductor/merge-execution-eligibility.js +3 -2
  20. package/build/conductor/pr-ci-producer.js +1 -1
  21. package/build/conductor/readiness-cli.js +46 -9
  22. package/build/conductor/readiness-sections.js +41 -6
  23. package/build/conductor/readiness.js +204 -30
  24. package/build/conductor/recovery-cli.js +703 -20
  25. package/build/conductor/recovery-operations.js +392 -3
  26. package/build/conductor/repair-contract.js +112 -3
  27. package/build/conductor/repository-workflow-remedy.js +125 -0
  28. package/build/conductor/tools.js +2 -2
  29. package/build/conductor-bundle-artifacts.js +8 -0
  30. package/build/credential-store.js +5 -4
  31. package/build/diagnostic-text.js +36 -0
  32. package/build/docs.generated.js +8 -3
  33. package/build/doctor.js +70 -10
  34. package/build/drive-epic-key.js +6 -5
  35. package/build/drive-epic.js +479 -131
  36. package/build/epic-coherence-input.js +2 -1
  37. package/build/epic-implementer-bin.js +30 -26
  38. package/build/epic-implementer-bundle-cli.js +2 -2
  39. package/build/epic-launch-diagnostic.js +31 -0
  40. package/build/epic-launch-profile.js +44 -14
  41. package/build/executor/cli.js +3 -2
  42. package/build/executor/completion-contract.js +48 -5
  43. package/build/executor/completion-supervision.js +9 -0
  44. package/build/executor/correction-dispatch.js +60 -0
  45. package/build/executor/env.js +25 -0
  46. package/build/executor/heartbeat.js +1 -1
  47. package/build/executor/http-client.js +35 -0
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +38 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +485 -74
  52. package/build/executor/job-types.js +8 -13
  53. package/build/executor/observation.js +6 -1
  54. package/build/executor/preflight.js +136 -10
  55. package/build/executor/prompt-spec.js +113 -9
  56. package/build/executor/required-work-ledger.js +6 -1
  57. package/build/executor/resume-pre-spawn.js +2 -0
  58. package/build/executor/runner.js +31 -2
  59. package/build/executor/service-unit.js +7 -1
  60. package/build/executor/stale-artifacts.js +5 -4
  61. package/build/executor/types.js +1 -0
  62. package/build/executor/watch-cli.js +35 -8
  63. package/build/executor/worker-command.js +43 -43
  64. package/build/executor/worker-finalization.js +161 -43
  65. package/build/executor/worker-guard-hook.js +152 -12
  66. package/build/executor/worker-log.js +137 -70
  67. package/build/executor/worktree-gc.js +30 -6
  68. package/build/executor/worktree.js +82 -1
  69. package/build/index.js +482 -660
  70. package/build/init.js +107 -4
  71. package/build/install-bridge-conductor.js +8 -6
  72. package/build/install-bridge.js +287 -23
  73. package/build/install-doctor.js +172 -30
  74. package/build/install-reexec.js +69 -7
  75. package/build/macos-terminal-permission-guidance.js +16 -0
  76. package/build/model-thresholds-tool.js +185 -0
  77. package/build/phase-context-git-probe.js +68 -0
  78. package/build/phase-result-artifacts.js +173 -5
  79. package/build/pipelines.generated.js +19 -25
  80. package/build/plane/alembic-head.js +12 -3
  81. package/build/plane/base-worktree-guard.js +147 -0
  82. package/build/plane/cli.js +217 -17
  83. package/build/plane/code-snapshot.js +468 -0
  84. package/build/plane/defaults.js +48 -3
  85. package/build/plane/endpoint.js +41 -0
  86. package/build/plane/manifest.js +120 -1
  87. package/build/plane/member-roster.js +94 -19
  88. package/build/plane/preflight.js +30 -2
  89. package/build/plane/process-cwd.js +67 -0
  90. package/build/plane/restart.js +542 -0
  91. package/build/plane/shutdown.js +31 -0
  92. package/build/plane/status.js +145 -6
  93. package/build/plane/supervisor.js +588 -177
  94. package/build/plane/types.js +54 -2
  95. package/build/plane/worker-exit-code.js +2 -0
  96. package/build/printed-remedy-commands.js +37 -0
  97. package/build/project-root.js +99 -0
  98. package/build/readiness-check.js +1 -1
  99. package/build/readme.generated.js +1 -1
  100. package/build/review-generation.js +22 -0
  101. package/build/review-tickets.js +156 -13
  102. package/build/setup-epic.js +224 -127
  103. package/build/sfcc/config.js +9 -0
  104. package/build/sfcc/credentials.js +100 -38
  105. package/build/sfcc/hostname.js +67 -0
  106. package/build/sfcc/permissions.js +17 -13
  107. package/build/sfcc/register.js +1 -1
  108. package/build/sfcc/setup-status.js +2 -0
  109. package/build/sfcc/tool-wrapper.js +41 -43
  110. package/build/sfcc/tools-configured.js +96 -0
  111. package/build/sfcc/write-grants.js +17 -11
  112. package/build/sfcc/write-result.js +4 -3
  113. package/build/sfcc/writes-site-preference.js +20 -8
  114. package/build/start-tickets.js +332 -79
  115. package/build/tool-error-envelope.js +61 -0
  116. package/build/upgrade-cli.js +109 -24
  117. package/build/version.generated.js +3 -3
  118. package/build/visual-diff.js +22 -1
  119. package/build/windows-npm-entry.js +103 -0
  120. package/build/worker-guard-hook-bin.js +3 -5
  121. package/build/worker-guard-hook-main.js +47 -0
  122. package/build/worktree-core.js +136 -9
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  124. package/docs/EPIC-IMPLEMENTER.md +662 -0
  125. package/docs/install/mcp-tool-integrations.md +25 -15
  126. package/docs/install/sfcc-integration.md +21 -8
  127. package/package.json +1 -1
  128. package/pipelines/implement-ticket.json +1 -1
  129. package/pipelines/plan-epic.json +0 -5
  130. package/pipelines/review-ticket.json +1 -1
  131. package/docs/CONDUCTOR.md +0 -605
  132. package/pipelines/idea-to-pr.json +0 -49
@@ -57,18 +57,21 @@ export function getConductorUsage() {
57
57
  return [
58
58
  "Usage: epic-implementer <command> [options]",
59
59
  "",
60
- "Local append-only event ledger for multi-agent coordination.",
61
- "Talks ONLY to the local SQLite store (~/.config/bridge/events.db); no Bridge API calls.",
60
+ "Local append-only event ledger for multi-agent coordination, plus run-control",
61
+ "commands. The ledger commands talk ONLY to the local SQLite store",
62
+ "(~/.config/bridge/events.db); the run-control commands (stop-run through",
63
+ "rerun-ci), epic-status, and readiness call the Bridge API.",
62
64
  "",
63
65
  "Commands:",
64
66
  " emit-event Append one semantic event to the ledger",
65
67
  " supervise --run-id <id> Run the foreground, run-scoped supervisor loop",
66
- " epic-tick Run one stateless reconciliation pass for an Epic",
68
+ " epic-tick RETIRED (v1): always exits 1 with EPIC_TICK_V1_FROZEN; the",
69
+ " server-side reconciler replaced it",
67
70
  " epic-status Print read-only health summary of an Epic Run",
68
71
  " send-message Enqueue ONE typed supervisor->worker relay message (idempotent)",
69
72
  " check-messages Read + ACK pending relay messages for a worker (no redelivery)",
70
73
  " doctor Read-only health/diagnostics report (ledger + git hooks)",
71
- " readiness Read-only ADVISORY report of every conductor prerequisite",
74
+ " readiness Read-only ADVISORY report of every Epic Implementer prerequisite",
72
75
  " (install + epic-implementer doctor + plane preflight + server),",
73
76
  " each with pass/warn/fail/skip and one named remediation.",
74
77
  " Remediations are NOT run automatically; always exits 0.",
@@ -112,6 +115,13 @@ export function getConductorUsage() {
112
115
  " continuation-receipt --epic-run-id <id> --ticket-key <key> [--request-id <id>] [--json]",
113
116
  " Read-only: the child's current episode and whether a continuation",
114
117
  " request's receipt proves it committed (use after a lost response).",
118
+ " resume-ci --epic-run-id <id> --ticket-key <key> [--json]",
119
+ " Resume a child parked on a qualified CI stop once CI is green: CI",
120
+ " returns to evaluation with NO new wait, job, or repair allowance.",
121
+ " rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
122
+ " Ask CI to re-run a ticket's FAILED checks at its bound head (guided: copy",
123
+ " the command explain-run rendered). Charges NO allowance and never re-runs",
124
+ " claude-review. A re-run is not a verdict: after CI is green, use resume-ci.",
115
125
  "",
116
126
  "supervise options:",
117
127
  " --run-id <id> Run/session identifier to supervise (required)",
@@ -193,11 +203,11 @@ export function getConductorUsage() {
193
203
  " epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
194
204
  " # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
195
205
  " epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
196
- " # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"conductor-ci / gate\"]}",
206
+ " # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"epic-implementer-ci / gate\"]}",
197
207
  " epic-implementer resume-run --epic-run-id <id>",
198
208
  " # a cap_wall_clock pause whose cause is now cleared",
199
209
  "",
200
- "epic-tick options:",
210
+ "epic-tick options (retired; parsed by no live path):",
201
211
  " --epic-key <KEY> Epic key to supervise (required, non-empty)",
202
212
  " --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)",
203
213
  " --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)",
@@ -254,6 +264,10 @@ export function getConductorUsage() {
254
264
  " epic-implementer continue-child --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
255
265
  " # The continue-child response was lost — did it commit?",
256
266
  " epic-implementer continuation-receipt --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
267
+ " # CI finished green after the child parked on an expired CI wait:",
268
+ " epic-implementer resume-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
269
+ " # CI failed on infrastructure: re-run it (never claude-review); then resume-ci once green:",
270
+ " epic-implementer rerun-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852 --binding <token>",
257
271
  ].join("\n");
258
272
  }
259
273
  /**
@@ -305,6 +319,10 @@ const VALID_COMMANDS = new Set([
305
319
  // BAPI-1300: CLI-only parked-child continuation pair — never registered as MCP tools.
306
320
  "continue-child",
307
321
  "continuation-receipt",
322
+ // BAPI-1326: CLI-only allowance-preserving CI resume — never an MCP tool.
323
+ "resume-ci",
324
+ // BAPI-1388: CLI-only guided provider re-run of failed CI workflow runs — never an MCP tool.
325
+ "rerun-ci",
308
326
  // Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
309
327
  // the bundled-artifact regression guard for BAPI-772 (mirroring `plane
310
328
  // __entrypoint`), not as a supported operator workflow.
@@ -1439,6 +1457,16 @@ export async function runEpicImplementerCli(argv) {
1439
1457
  const { runContinuationReceiptCommand } = await import("./recovery-cli.js");
1440
1458
  return await runContinuationReceiptCommand(parsed.argv);
1441
1459
  }
1460
+ case "resume-ci": {
1461
+ // BAPI-1326: delegates only to the pinned CI recovery operation.
1462
+ const { runResumeCiCommand } = await import("./recovery-cli.js");
1463
+ return await runResumeCiCommand(parsed.argv);
1464
+ }
1465
+ case "rerun-ci": {
1466
+ // BAPI-1388: delegates only to the guided, pinned rerun-ci operation.
1467
+ const { runRerunCiCommand } = await import("./recovery-cli.js");
1468
+ return await runRerunCiCommand(parsed.argv);
1469
+ }
1442
1470
  default:
1443
1471
  console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
1444
1472
  return 1;
@@ -70,9 +70,10 @@ export class ConductorStoreError extends Error {
70
70
  */
71
71
  export class ConductorEpicTickV1FrozenError extends Error {
72
72
  constructor() {
73
- super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). Epic " +
74
- "Conductor v2 replaces the local epic tick with a server-side reconciler " +
75
- "plus the local executor — run `mcp-server executor` instead.");
73
+ super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). The " +
74
+ "Epic Implementer replaced the local epic tick with a server-side reconciler " +
75
+ "plus the local executor — run `mcp-server executor --repo <name> " +
76
+ "--epic-run-id <run-id>` instead.");
76
77
  this.name = "ConductorEpicTickV1FrozenError";
77
78
  }
78
79
  }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * BAPI-1333 — guided operator repairs: the displayed binding and its local
3
+ * recovery reference.
4
+ *
5
+ * ExplainRun renders a guided command with one `--binding v1.<base64url>` token.
6
+ * The token names the verb, the run, and the bounded subject the command was
7
+ * rendered against (a run repair revision plus its control, block, or policy
8
+ * subject; a scope episode; or a ticket version and head). This module:
9
+ *
10
+ * - decodes and strictly validates that token (identifiers only, closed keys);
11
+ * - turns it into the typed `guard` the server enforces in its guarded write;
12
+ * - persists a small local reference BEFORE the first POST — the request id, the
13
+ * binding, and digests of the binding and the payload — so a transport retry
14
+ * or a restarted CLI process re-sends the SAME request instead of re-reading
15
+ * the run and approving whatever subject exists now.
16
+ *
17
+ * The stored reference never holds a payload value: a policy patch or a driver
18
+ * context is represented only by its SHA-256 digest. Nothing here performs HTTP.
19
+ */
20
+ import { createHash, randomUUID } from "node:crypto";
21
+ import { canonicalSha256, clearRecoveryReferenceFile, readRecoveryReferenceFile, writeRecoveryReferenceFile, } from "./epic-child-continuation.js";
22
+ export const GUIDED_BINDING_TOKEN_PREFIX = "v1.";
23
+ export const GUIDED_REPAIR_REFERENCE_NAMESPACE = "epic-guided-repairs";
24
+ /** The fixed message when the reference cannot be stored; nothing is sent. */
25
+ export const GUIDED_REFERENCE_STORE_FAILED_MESSAGE = "could not store the local recovery reference, so the request was not sent";
26
+ /** A rerun of the same displayed command with a DIFFERENT payload. */
27
+ export const GUIDED_PAYLOAD_CONFLICT_MESSAGE = "a different request was already issued for this displayed command; nothing " +
28
+ "was sent. Re-read the run explanation before choosing another action";
29
+ export const GUIDED_REPAIR_VERBS = [
30
+ "resume-run",
31
+ "patch-policy",
32
+ "scope-reparse",
33
+ "scope-catch-up",
34
+ "dispatch-remediation",
35
+ // BAPI-1388 — ticket-addressed, pinned to the same version-and-head subject as
36
+ // `dispatch-remediation`, and consuming the binding ExplainRun renders.
37
+ "rerun-ci",
38
+ ];
39
+ /** A malformed or mismatched binding. The message never echoes the token. */
40
+ export class GuidedBindingError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "GuidedBindingError";
44
+ }
45
+ }
46
+ const TOKEN_RE = /^[a-z][a-z0-9_]{0,63}$/;
47
+ const SHA_RE = /^[0-9a-f]{40}$/;
48
+ const REQUEST_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
49
+ const FINGERPRINT_RE = /^[0-9a-f]{64}$/;
50
+ /** Keys each verb's token may carry, beyond `v`, `verb`, and `epic_run_id`. */
51
+ const ALLOWED_KEYS = {
52
+ "resume-run": [
53
+ "repair_revision",
54
+ "subject",
55
+ "control_state",
56
+ "control_generation",
57
+ "block_reason",
58
+ ],
59
+ "patch-policy": ["repair_revision", "subject"],
60
+ "scope-reparse": ["scope_id", "lifecycle_state", "required_commit_sha"],
61
+ "scope-catch-up": ["scope_id", "lifecycle_state", "required_commit_sha"],
62
+ "dispatch-remediation": ["ticket_key", "expected_row_version", "expected_head_sha"],
63
+ "rerun-ci": ["ticket_key", "expected_row_version", "expected_head_sha"],
64
+ };
65
+ function invalid() {
66
+ return new GuidedBindingError("--binding is not a valid guided-repair binding; nothing was sent. Copy the " +
67
+ "command exactly as the run explanation rendered it.");
68
+ }
69
+ function nonNegativeInt(value) {
70
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
71
+ throw invalid();
72
+ return value;
73
+ }
74
+ function boundedString(value, max) {
75
+ if (typeof value !== "string" || value.length === 0 || value.length > max)
76
+ throw invalid();
77
+ return value;
78
+ }
79
+ function optionalSha(value) {
80
+ if (value === undefined || value === null)
81
+ return null;
82
+ if (typeof value !== "string" || !SHA_RE.test(value))
83
+ throw invalid();
84
+ return value;
85
+ }
86
+ function decodeBase64Url(text) {
87
+ if (!/^[A-Za-z0-9_-]+$/.test(text) || text.length > 2048)
88
+ throw invalid();
89
+ return Buffer.from(text, "base64url").toString("utf8");
90
+ }
91
+ /** Decode and strictly validate a `--binding` token. Throws {@link GuidedBindingError}. */
92
+ export function decodeGuidedBinding(token) {
93
+ if (typeof token !== "string" || !token.startsWith(GUIDED_BINDING_TOKEN_PREFIX)) {
94
+ throw invalid();
95
+ }
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(decodeBase64Url(token.slice(GUIDED_BINDING_TOKEN_PREFIX.length)));
99
+ }
100
+ catch (err) {
101
+ if (err instanceof GuidedBindingError)
102
+ throw err;
103
+ throw invalid();
104
+ }
105
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
106
+ throw invalid();
107
+ const raw = parsed;
108
+ if (raw.v !== 1)
109
+ throw invalid();
110
+ const verb = raw.verb;
111
+ if (typeof verb !== "string" || !GUIDED_REPAIR_VERBS.includes(verb)) {
112
+ throw invalid();
113
+ }
114
+ const guidedVerb = verb;
115
+ const allowed = new Set(["v", "verb", "epic_run_id", ...ALLOWED_KEYS[guidedVerb]]);
116
+ for (const key of Object.keys(raw)) {
117
+ if (!allowed.has(key))
118
+ throw invalid();
119
+ }
120
+ const binding = {
121
+ verb: guidedVerb,
122
+ epicRunId: boundedString(raw.epic_run_id, 200),
123
+ };
124
+ if (guidedVerb === "resume-run" || guidedVerb === "patch-policy") {
125
+ binding.repairRevision = nonNegativeInt(raw.repair_revision);
126
+ const subject = raw.subject;
127
+ if (guidedVerb === "patch-policy") {
128
+ if (subject !== "policy")
129
+ throw invalid();
130
+ binding.subject = "policy";
131
+ }
132
+ else if (subject === "control") {
133
+ if (raw.control_state !== "paused" && raw.control_state !== "quarantined")
134
+ throw invalid();
135
+ const generation = nonNegativeInt(raw.control_generation);
136
+ if (generation < 1 || raw.block_reason !== undefined)
137
+ throw invalid();
138
+ binding.subject = "control";
139
+ binding.controlState = raw.control_state;
140
+ binding.controlGeneration = generation;
141
+ }
142
+ else if (subject === "integration_block") {
143
+ const reason = raw.block_reason;
144
+ if (typeof reason !== "string" || !TOKEN_RE.test(reason))
145
+ throw invalid();
146
+ if (raw.control_state !== undefined || raw.control_generation !== undefined)
147
+ throw invalid();
148
+ binding.subject = "integration_block";
149
+ binding.blockReason = reason;
150
+ }
151
+ else {
152
+ throw invalid();
153
+ }
154
+ }
155
+ else if (guidedVerb === "dispatch-remediation" || guidedVerb === "rerun-ci") {
156
+ binding.ticketKey = boundedString(raw.ticket_key, 64);
157
+ binding.expectedRowVersion = nonNegativeInt(raw.expected_row_version);
158
+ binding.expectedHeadSha = optionalSha(raw.expected_head_sha);
159
+ }
160
+ else {
161
+ binding.scopeId = boundedString(raw.scope_id, 200);
162
+ const lifecycle = raw.lifecycle_state;
163
+ if (typeof lifecycle !== "string" || !TOKEN_RE.test(lifecycle))
164
+ throw invalid();
165
+ binding.lifecycleState = lifecycle;
166
+ binding.requiredCommitSha = optionalSha(raw.required_commit_sha);
167
+ }
168
+ return binding;
169
+ }
170
+ /** The canonical, snake_case identity of a binding (what its digest covers). */
171
+ export function bindingIdentity(binding) {
172
+ const out = { verb: binding.verb, epic_run_id: binding.epicRunId };
173
+ const pairs = [
174
+ ["repair_revision", binding.repairRevision],
175
+ ["subject", binding.subject],
176
+ ["control_state", binding.controlState],
177
+ ["control_generation", binding.controlGeneration],
178
+ ["block_reason", binding.blockReason],
179
+ ["scope_id", binding.scopeId],
180
+ ["lifecycle_state", binding.lifecycleState],
181
+ ["required_commit_sha", binding.requiredCommitSha],
182
+ ["ticket_key", binding.ticketKey],
183
+ ["expected_row_version", binding.expectedRowVersion],
184
+ ["expected_head_sha", binding.expectedHeadSha],
185
+ ];
186
+ for (const [key, value] of pairs) {
187
+ if (value !== undefined)
188
+ out[key] = value;
189
+ }
190
+ return out;
191
+ }
192
+ /** The typed body `guard` for this binding and request id. */
193
+ export function guardForBinding(binding, requestId) {
194
+ if (binding.verb === "resume-run" || binding.verb === "patch-policy") {
195
+ const guard = {
196
+ request_id: requestId,
197
+ epic_run_id: binding.epicRunId,
198
+ repair_revision: binding.repairRevision ?? 0,
199
+ subject: binding.subject ?? "policy",
200
+ };
201
+ if (binding.controlState !== undefined)
202
+ guard.control_state = binding.controlState;
203
+ if (binding.controlGeneration !== undefined)
204
+ guard.control_generation = binding.controlGeneration;
205
+ if (binding.blockReason !== undefined)
206
+ guard.block_reason = binding.blockReason;
207
+ return guard;
208
+ }
209
+ if (binding.verb === "dispatch-remediation" || binding.verb === "rerun-ci") {
210
+ return {
211
+ request_id: requestId,
212
+ epic_run_id: binding.epicRunId,
213
+ expected_row_version: binding.expectedRowVersion ?? 0,
214
+ expected_head_sha: binding.expectedHeadSha ?? null,
215
+ };
216
+ }
217
+ return {
218
+ request_id: requestId,
219
+ epic_run_id: binding.epicRunId,
220
+ lifecycle_state: binding.lifecycleState ?? "",
221
+ required_commit_sha: binding.requiredCommitSha ?? null,
222
+ };
223
+ }
224
+ function stableStringify(value) {
225
+ if (Array.isArray(value))
226
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
227
+ if (value !== null && typeof value === "object") {
228
+ const record = value;
229
+ return `{${Object.keys(record)
230
+ .sort()
231
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
232
+ .join(",")}}`;
233
+ }
234
+ return JSON.stringify(value) ?? "null";
235
+ }
236
+ /**
237
+ * SHA-256 of a request payload in canonical form (sorted keys; any JSON number).
238
+ * Only this digest is persisted, so a patch or context never reaches the disk.
239
+ */
240
+ export function guidedPayloadDigest(payload) {
241
+ return createHash("sha256").update(stableStringify(payload), "utf8").digest("hex");
242
+ }
243
+ function referenceTarget(binding) {
244
+ return binding.ticketKey ?? binding.scopeId ?? "run";
245
+ }
246
+ function referenceKey(binding) {
247
+ return { epicRunId: binding.epicRunId, ticketKey: `${binding.verb}:${referenceTarget(binding)}` };
248
+ }
249
+ function referenceFingerprint(ref) {
250
+ return canonicalSha256({
251
+ v: 1,
252
+ verb: ref.verb,
253
+ epic_run_id: ref.epicRunId,
254
+ target: ref.target,
255
+ request_id: ref.requestId,
256
+ binding_digest: ref.bindingDigest,
257
+ payload_digest: ref.payloadDigest,
258
+ });
259
+ }
260
+ function parseReference(raw) {
261
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
262
+ return null;
263
+ const ref = raw;
264
+ if (ref.version !== 1)
265
+ return null;
266
+ if (typeof ref.verb !== "string" || !GUIDED_REPAIR_VERBS.includes(ref.verb)) {
267
+ return null;
268
+ }
269
+ for (const key of ["epicRunId", "target", "createdAt"]) {
270
+ if (typeof ref[key] !== "string" || ref[key].length === 0)
271
+ return null;
272
+ }
273
+ if (typeof ref.requestId !== "string" || !REQUEST_ID_RE.test(ref.requestId))
274
+ return null;
275
+ for (const key of ["bindingDigest", "payloadDigest", "fingerprint"]) {
276
+ if (typeof ref[key] !== "string" || !FINGERPRINT_RE.test(ref[key]))
277
+ return null;
278
+ }
279
+ const parsed = raw;
280
+ // A reference whose fingerprint no longer matches its own identity was
281
+ // tampered with or truncated; it never authorizes a re-send.
282
+ return referenceFingerprint(parsed) === parsed.fingerprint ? parsed : null;
283
+ }
284
+ /**
285
+ * Reuse the persisted request for this exact binding and payload, or mint and
286
+ * durably persist a new one BEFORE anything is sent.
287
+ *
288
+ * - Same binding, same payload → the SAME request id (transport retry, restart).
289
+ * - Same binding, different payload → refused locally; nothing is sent.
290
+ * - Different binding (a newer rendered command) → a new request replaces it.
291
+ */
292
+ export async function prepareGuidedRequest(binding, payloadDigest, deps = {}) {
293
+ const key = referenceKey(binding);
294
+ const bindingDigest = canonicalSha256(bindingIdentity(binding));
295
+ const existing = parseReference(await readRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, key, deps.store));
296
+ if (existing !== null && existing.bindingDigest === bindingDigest) {
297
+ if (existing.payloadDigest !== payloadDigest)
298
+ return { kind: "payload_conflict" };
299
+ return { kind: "ready", requestId: existing.requestId, referenceReused: true };
300
+ }
301
+ const requestId = (deps.newRequestId ?? randomUUID)();
302
+ const base = {
303
+ version: 1,
304
+ verb: binding.verb,
305
+ epicRunId: binding.epicRunId,
306
+ target: referenceTarget(binding),
307
+ requestId,
308
+ bindingDigest,
309
+ payloadDigest,
310
+ };
311
+ const reference = {
312
+ ...base,
313
+ createdAt: (deps.now ?? (() => new Date()))().toISOString(),
314
+ fingerprint: referenceFingerprint(base),
315
+ };
316
+ try {
317
+ await writeRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, key, reference, deps.store);
318
+ }
319
+ catch {
320
+ return { kind: "store_failed" };
321
+ }
322
+ return { kind: "ready", requestId, referenceReused: false };
323
+ }
324
+ /** Remove the reference once the server returned a definitive answer. Never throws. */
325
+ export async function clearGuidedReference(binding, deps = {}) {
326
+ await clearRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, referenceKey(binding), deps.store);
327
+ }
@@ -89,7 +89,7 @@ export const HOOK_RUNTIME_ACTIONS = {
89
89
  "from a persistent install.",
90
90
  "unqualified-runtime": "the hook runtime is not a qualified persistent installation (supported: a persistent npm " +
91
91
  `install of ${MCP_PACKAGE_NAME} ${HOOK_RUNTIME_COMPATIBLE_RANGE} on macOS or Linux with an even ` +
92
- `Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/CONDUCTOR.md.`,
92
+ `Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/EPIC-IMPLEMENTER.md.`,
93
93
  "node-unavailable": "`node` could not be launched from PATH, so the hook cannot run. Put a qualified Node on PATH " +
94
94
  "and re-run `epic-implementer doctor`.",
95
95
  "native-unavailable": "better-sqlite3 does not load for the hook's Node, so no event can be recorded. Reinstall the " +
@@ -23,14 +23,13 @@
23
23
  * inspected a local checkout to decide merge eligibility would be answering a
24
24
  * different question from the one the server's before-merge preflight answers.
25
25
  *
26
- * ## Deferred remedy
26
+ * ## Remedy command
27
27
  *
28
- * {@link MERGE_WORKFLOW_REMEDY_COMMAND} names the generate-and-PR command epic
29
- * child 4 ships. Until child 4 lands, a refusal may name a command that is not
30
- * yet an executable entry point on the operator's machine. That residual risk,
31
- * its three containments, and who owns escalation are recorded once, in the
28
+ * {@link MERGE_WORKFLOW_REMEDY_COMMAND} is the complete generate-and-PR command that
29
+ * BAPI-1136 shipped (BAPI-1376 made it a full launcher command). Its history and containments are recorded once, in the
32
30
  * Python module's docstring; this mirror does not restate them.
33
31
  */
32
+ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
34
33
  /** The one `RunPolicy.merge_execution_mode` value this contract governs. */
35
34
  export const MERGE_EXECUTION_MODE_GITHUB_ACTIONS = "github_actions";
36
35
  /**
@@ -80,7 +79,27 @@ export function mergeWorkflowVersionSupportsIntegrationActions(version) {
80
79
  INTEGRATION_ACTION_WORKFLOW_VERSIONS.includes(version));
81
80
  }
82
81
  /**
83
- * The single command that generates the merge workflow and opens its pull
84
- * request. Every refusal, remediation, and readiness line imports this constant.
82
+ * The dispatched subcommand that generates the merge workflow and opens its pull
83
+ * request. This is the part the CLI's own dispatcher (`index.ts`) matches on
84
+ * (`install`), not the launcher that starts it. The Python mirror derives its
85
+ * command head from this value.
85
86
  */
86
- export const MERGE_WORKFLOW_REMEDY_COMMAND = "install-bridge conductor --merge-workflow";
87
+ export const MERGE_WORKFLOW_SUBCOMMAND = "install conductor --merge-workflow";
88
+ /**
89
+ * The complete, copy-ready command that generates the merge workflow and opens
90
+ * its pull request. Every refusal, remediation, and readiness line imports this
91
+ * constant.
92
+ *
93
+ * The package name comes from `mcp-identity.ts`, the sole source of that
94
+ * literal (BAPI-806). That module is itself dependency-free, so this import can
95
+ * never close a cycle. The `<@org/team|@user>` placeholder is deliberately NOT a valid owner — the
96
+ * installer refuses it — so an operator must replace it before running.
97
+ */
98
+ export const MERGE_WORKFLOW_REMEDY_COMMAND = `npx -y ${MCP_PACKAGE_NAME} ${MERGE_WORKFLOW_SUBCOMMAND} --codeowner <@org/team|@user>`;
99
+ /**
100
+ * One reusable instruction to print beside {@link MERGE_WORKFLOW_REMEDY_COMMAND}.
101
+ * Bridge cannot infer who should review the workflow file, so the placeholder
102
+ * must be replaced by hand.
103
+ */
104
+ export const MERGE_WORKFLOW_CODEOWNER_INSTRUCTION = "Bridge cannot infer the code owner. Before running, replace <@org/team|@user> " +
105
+ "with the one @user or @org/team who should review the workflow file.";
@@ -31,7 +31,7 @@
31
31
  * not eligible: it is a workflow this build has never been taught to reason
32
32
  * about, and "newer is probably fine" is a guess about a merge actuator.
33
33
  */
34
- import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
34
+ import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
35
35
  /**
36
36
  * Remediation for a verdict that could not be established.
37
37
  *
@@ -51,7 +51,8 @@ export const MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION = "re-run once the Bridge
51
51
  * second way.
52
52
  */
53
53
  export const MERGE_EXECUTION_INELIGIBLE_REMEDIATION = `run \`${MERGE_WORKFLOW_REMEDY_COMMAND}\` to generate the merge workflow and open its ` +
54
- "pull request, then re-run once it is merged to the default branch.";
54
+ `pull request. ${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION} ` +
55
+ "Then re-run once it is merged to the default branch.";
55
56
  function unverifiable(reason, detail) {
56
57
  return {
57
58
  status: "could_not_verify",
@@ -531,7 +531,7 @@ export async function observePrCiFromPollResponse(commitRef, pollResponse, deps
531
531
  function extractTicketKeyFromRef(headRef) {
532
532
  if (!headRef)
533
533
  return null;
534
- const match = /([A-Z][A-Z0-9]+-\d+)/i.exec(headRef);
534
+ const match = /([A-Z][A-Z0-9_]{0,19}-\d+)/i.exec(headRef);
535
535
  return match ? match[1].toUpperCase() : null;
536
536
  }
537
537
  /**
@@ -35,15 +35,18 @@
35
35
  import os from "node:os";
36
36
  import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
37
37
  import { collectConductorReadinessGate, formatConductorReadinessGateReport, } from "./readiness.js";
38
- import { resolveConductorBridgeApiAccess } from "./bridge-api-client.js";
38
+ import { readinessEndpointEnvironments, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess } from "./bridge-api-client.js";
39
+ import { validateHttpBaseUrl } from "../base-url.js";
39
40
  import { claudeReviewWorkflowPath } from "../claude-review-workflow.js";
40
41
  import { runDefaultPlanePreflight } from "../plane/cli.js";
42
+ import { planeNotApplicableForTarget } from "../plane/endpoint.js";
43
+ import { resolvePlaneServerEndpoint } from "../plane/member-roster.js";
41
44
  /** Fixed notice emitted to stderr when a source could not be collected. */
42
45
  export const READINESS_COLLECTION_NOTICE = "one or more readiness sources could not be collected; the affected prerequisites are reported as failures.";
43
46
  /** Usage text for `epic-implementer readiness`. */
44
47
  export function getReadinessUsage() {
45
48
  return [
46
- "Usage: epic-implementer readiness [--json] [--no-deny-probe]",
49
+ "Usage: epic-implementer readiness [--json] [--no-deny-probe] [--base-url <url>]",
47
50
  "",
48
51
  "Read-only ADVISORY report of every conductor prerequisite, consolidated from the",
49
52
  "Bridge install checklist, the epic-implementer doctor, the plane preflight, and the",
@@ -58,19 +61,31 @@ export function getReadinessUsage() {
58
61
  " --json Print the versioned structured report as JSON",
59
62
  " --no-deny-probe Accepted for parity with `epic-implementer doctor`; inert here, because",
60
63
  " this command never spawns the deny-enforcement probe",
64
+ " --base-url <url> Select a read-only Bridge API endpoint for this report;",
65
+ " this does not authorize or supply a credential",
61
66
  " --help Print this usage message",
62
67
  ].join("\n");
63
68
  }
64
69
  /** Parse `epic-implementer readiness` argv. Unknown flags and positionals are errors. */
65
70
  export function parseReadinessArgs(argv) {
66
71
  const parsed = { json: false, help: false, noDenyProbe: false };
67
- for (const token of argv) {
72
+ for (let index = 0; index < argv.length; index++) {
73
+ const token = argv[index];
68
74
  if (token === "--json")
69
75
  parsed.json = true;
70
76
  else if (token === "--no-deny-probe")
71
77
  parsed.noDenyProbe = true;
72
78
  else if (token === "--help" || token === "-h")
73
79
  parsed.help = true;
80
+ else if (token === "--base-url") {
81
+ const raw = argv[++index];
82
+ if (!raw || raw.startsWith("--"))
83
+ return { error: "--base-url requires an HTTP(S) URL." };
84
+ const valid = validateHttpBaseUrl(raw);
85
+ if (!valid.ok)
86
+ return { error: "--base-url requires a valid HTTP(S) URL." };
87
+ parsed.baseUrl = valid.baseUrl;
88
+ }
74
89
  else
75
90
  return { error: `Unknown argument "${token}". Run "epic-implementer readiness --help" for usage.` };
76
91
  }
@@ -83,8 +98,24 @@ export function parseReadinessArgs(argv) {
83
98
  * epic-implementer doctor's local inspections, and the same plane preflight
84
99
  * `plane up` and `drive-epic` run. Nothing is re-implemented here.
85
100
  */
86
- async function collectProductionReport(repoRoot, env) {
87
- const accessResult = await resolveConductorBridgeApiAccess({ env, cwd: repoRoot });
101
+ async function collectProductionReport(repoRoot, env, baseUrl) {
102
+ const { reportEnv, resolverEnv } = readinessEndpointEnvironments(env, baseUrl);
103
+ const accessResult = baseUrl === undefined
104
+ ? await resolveConductorBridgeApiAccess({ env: reportEnv, cwd: repoRoot })
105
+ : await resolveEpicLaunchBridgeApiAccess({ baseUrl }, { env: resolverEnv, cwd: repoRoot });
106
+ // BAPI-1360: the plane checks apply only when the selected access target names
107
+ // the local plane. Resolved from the SAME env the plane preflight reads, through
108
+ // the shared leaf predicate `drive-epic` uses, and never guessed: a target that
109
+ // is remote, or that cannot be resolved at all, is an explicit skip.
110
+ const planeNotApplicable = planeNotApplicableForTarget(resolvePlaneServerEndpoint(reportEnv), accessResult.ok ? accessResult.access.baseUrl : null);
111
+ if (baseUrl !== undefined && !accessResult.ok) {
112
+ // A refused override is never permission for a downstream doctor collector
113
+ // to resolve credentials afresh from the general CLI environment.
114
+ return collectConductorReadinessGate({
115
+ repoName: null,
116
+ ...(planeNotApplicable !== null ? { planeNotApplicable } : {}),
117
+ });
118
+ }
88
119
  const access = accessResult.ok ? accessResult.access : null;
89
120
  return collectConductorReadinessGate({
90
121
  // Never guessed: an unresolved identity is reported as such rather than
@@ -101,7 +132,7 @@ async function collectProductionReport(repoRoot, env) {
101
132
  readWorkflowFile: () => fsReadFile(claudeReviewWorkflowPath(repoRoot), "utf-8"),
102
133
  projectRoot: repoRoot,
103
134
  installDoctorDeps: {
104
- env,
135
+ env: reportEnv,
105
136
  cwd: repoRoot,
106
137
  platform: process.platform,
107
138
  homedir: os.homedir,
@@ -112,8 +143,14 @@ async function collectProductionReport(repoRoot, env) {
112
143
  },
113
144
  // The deny-enforcement probe is never spawned from this command; the check
114
145
  // is reported as an explicit skip naming `epic-implementer doctor` instead.
115
- conductorDoctorDeps: { env, skipDenyProbe: true },
116
- runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, env),
146
+ conductorDoctorDeps: { env: reportEnv, skipDenyProbe: true },
147
+ // Provided ONLY for a target that names the local plane; otherwise the rows
148
+ // are skipped without any probe.
149
+ ...(planeNotApplicable === null
150
+ ? {
151
+ runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, reportEnv),
152
+ }
153
+ : { planeNotApplicable }),
117
154
  });
118
155
  }
119
156
  /**
@@ -138,7 +175,7 @@ export async function runConductorReadinessCommand(argv, deps = {}) {
138
175
  }
139
176
  const cwd = deps.cwd ?? process.cwd();
140
177
  const env = deps.env ?? process.env;
141
- const collect = deps.collect ?? ((repoRoot) => collectProductionReport(repoRoot, env));
178
+ const collect = deps.collect ?? ((repoRoot) => collectProductionReport(repoRoot, env, parsed.baseUrl));
142
179
  const report = await collect(cwd);
143
180
  // Fixed, sanitized, and stderr-only: never a credential, exception, header,
144
181
  // body, subprocess output, or absolute path. It fires on a COLLECTION gap,