@bridge_gpt/mcp-server 0.2.69 → 0.2.70

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 (151) hide show
  1. package/README.md +140 -292
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +23 -4
  7. package/build/claude-review-workflow-drift.js +3 -3
  8. package/build/codex-bridge-profile.js +1 -1
  9. package/build/commands.generated.js +6 -6
  10. package/build/conductor/bridge-api-client.js +598 -19
  11. package/build/conductor/bring-up-facts.js +9 -9
  12. package/build/conductor/doctor.js +466 -18
  13. package/build/conductor/epic-child-continuation.js +54 -3
  14. package/build/conductor/epic-implementer-cli.js +132 -34
  15. package/build/conductor/epic-runtime.js +2 -2
  16. package/build/conductor/epic-state.js +1 -1
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/install-doctor.js +4 -2
  19. package/build/conductor/launch-need-probes.js +337 -0
  20. package/build/conductor/local-merge.js +241 -56
  21. package/build/conductor/merge-credential.js +172 -0
  22. package/build/conductor/merge-execution-eligibility.js +1 -1
  23. package/build/conductor/merged-child-check.js +95 -0
  24. package/build/conductor/plan-trim.js +30 -0
  25. package/build/conductor/pr-ci-producer.js +1 -1
  26. package/build/conductor/readiness-cli.js +11 -6
  27. package/build/conductor/readiness-prerequisites.js +41 -0
  28. package/build/conductor/readiness-sections.js +345 -55
  29. package/build/conductor/readiness.js +99 -87
  30. package/build/conductor/recovery-cli.js +606 -21
  31. package/build/conductor/recovery-operations.js +306 -7
  32. package/build/conductor/repair-contract.js +47 -2
  33. package/build/conductor/repository-workflow-remedy.js +8 -6
  34. package/build/conductor/stored-merge-credential.js +76 -0
  35. package/build/conductor/stored-merge-token-probe.js +28 -0
  36. package/build/conductor/temporary-plan.js +50 -0
  37. package/build/conductor/wait-run-cli.js +759 -0
  38. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  39. package/build/conductor-bin.js +2 -2
  40. package/build/conductor-claude-hook-bin.js +2 -2
  41. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  42. package/build/conductor-removed-stub-bin.js +2 -5
  43. package/build/credential-store.js +356 -13
  44. package/build/docs.generated.js +26 -13
  45. package/build/doctor.js +94 -32
  46. package/build/drive-epic-help.js +212 -0
  47. package/build/drive-epic.js +1840 -373
  48. package/build/epic-ci-workflow-drift.js +77 -0
  49. package/build/epic-ci-workflow-install.js +183 -0
  50. package/build/epic-ci-workflow.js +157 -0
  51. package/build/epic-contract-context.js +326 -0
  52. package/build/epic-implementer-bin.js +57 -41
  53. package/build/epic-implementer-claude-hook-bin.js +2 -2
  54. package/build/escalation-reachability.js +134 -0
  55. package/build/executor/cleanup-report.js +99 -0
  56. package/build/executor/cli.js +12 -1
  57. package/build/executor/completion-contract.js +66 -11
  58. package/build/executor/completion-supervision.js +99 -0
  59. package/build/executor/delivery-outcome.js +179 -0
  60. package/build/executor/deps.js +3 -0
  61. package/build/executor/env.js +71 -0
  62. package/build/executor/failure-reason-codes.js +126 -0
  63. package/build/executor/heartbeat.js +38 -0
  64. package/build/executor/http-client.js +72 -0
  65. package/build/executor/integration-job.js +298 -5
  66. package/build/executor/job-errors.js +36 -6
  67. package/build/executor/job-runner.js +720 -36
  68. package/build/executor/likely-tests.js +36 -2
  69. package/build/executor/merge-job.js +145 -2
  70. package/build/executor/observation.js +73 -4
  71. package/build/executor/permissions.js +193 -97
  72. package/build/executor/preflight.js +14 -3
  73. package/build/executor/process-containment.js +28 -2
  74. package/build/executor/prompt-spec.js +50 -5
  75. package/build/executor/rebase-finalization.js +38 -3
  76. package/build/executor/reconcile-overlap-job.js +35 -7
  77. package/build/executor/required-work-ledger.js +236 -81
  78. package/build/executor/resume-pre-spawn.js +38 -1
  79. package/build/executor/runner.js +91 -3
  80. package/build/executor/test-clock.js +8 -0
  81. package/build/executor/types.js +37 -2
  82. package/build/executor/worker-cli-launcher.js +259 -0
  83. package/build/executor/worker-command.js +41 -4
  84. package/build/executor/worker-database-target.js +91 -0
  85. package/build/executor/worker-database.js +908 -0
  86. package/build/executor/worker-finalization.js +106 -0
  87. package/build/executor/worker-guard-hook.js +203 -0
  88. package/build/executor/worker-resources.js +47 -0
  89. package/build/executor/worker-verification-guidance.js +15 -2
  90. package/build/executor/worktree.js +303 -15
  91. package/build/executor-launch.js +11 -1
  92. package/build/generated-assets-doctor.js +9 -1
  93. package/build/generated-assets-health.js +13 -4
  94. package/build/generated-assets-legacy.js +39 -0
  95. package/build/generated-assets-paths.js +12 -1
  96. package/build/generated-assets-repair.js +4 -1
  97. package/build/generated-assets-test-support.js +12 -0
  98. package/build/github-remote.js +33 -0
  99. package/build/github-sign-in.js +62 -0
  100. package/build/index.js +1067 -536
  101. package/build/init.js +39 -1
  102. package/build/install-bridge-conductor-gate.js +78 -0
  103. package/build/install-bridge-conductor-setup.js +413 -0
  104. package/build/install-bridge-conductor.js +1226 -267
  105. package/build/install-bridge.js +376 -60
  106. package/build/mcp-install-state.js +8 -2
  107. package/build/merge-pull-request.js +6 -0
  108. package/build/merge-workflow-install.js +763 -345
  109. package/build/phase-result-artifacts.js +75 -20
  110. package/build/pipeline-orchestrator.js +247 -4
  111. package/build/pipelines.generated.js +35 -35
  112. package/build/plan-discovery.js +15 -2
  113. package/build/plan-execution-ledger.js +306 -17
  114. package/build/plan-phase-routing.js +48 -13
  115. package/build/plane/cli.js +105 -51
  116. package/build/plane/defaults.js +3 -0
  117. package/build/plane/plane-freshness.js +294 -0
  118. package/build/plane/schema-compatibility.js +137 -0
  119. package/build/plane/shutdown-deps.js +38 -0
  120. package/build/plane/shutdown.js +83 -8
  121. package/build/plane/terminal-run-cleanup.js +303 -0
  122. package/build/printed-remedy-commands.js +26 -4
  123. package/build/readiness-check.js +6 -2
  124. package/build/readiness-prerequisites.generated.js +532 -0
  125. package/build/readme.generated.js +1 -1
  126. package/build/setup-epic.js +400 -30
  127. package/build/shared-resources.generated.js +5 -0
  128. package/build/start-tickets.js +28 -15
  129. package/build/test-command-detection.js +247 -0
  130. package/build/test-command-validation.js +73 -0
  131. package/build/testing-config-client.js +150 -0
  132. package/build/ticket-delivery-artifacts.js +258 -0
  133. package/build/ticket-delivery-enrollment.js +306 -0
  134. package/build/ticket-delivery.js +205 -0
  135. package/build/ticket-review-summary.js +14 -0
  136. package/build/upgrade-cli.js +13 -0
  137. package/build/v2-readiness-requirements.js +552 -0
  138. package/build/version.generated.js +3 -3
  139. package/build/worker-guard-hook-bin.js +2 -2
  140. package/build/worktree-core.js +22 -7
  141. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  142. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  143. package/docs/EPIC-IMPLEMENTER.md +104 -63
  144. package/docs/epic-implementer/getting-started.md +131 -0
  145. package/docs/epic-implementer/self-hosting.md +133 -0
  146. package/package.json +3 -3
  147. package/pipelines/idea-to-ticket.json +1 -1
  148. package/pipelines/implement-ticket.json +8 -8
  149. package/pipelines/review-ticket.json +2 -2
  150. package/design-assets/favicon/favicon-32x32.png +0 -0
  151. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -77,9 +77,25 @@ export function canonicalJson(value) {
77
77
  export function canonicalSha256(value) {
78
78
  return createHash("sha256").update(canonicalJson(value), "utf8").digest("hex");
79
79
  }
80
- /** The fingerprint input document (identical to the Python builder). */
80
+ /** BAPI-1514 — lowercase SHA-256 hex of a driver context's exact UTF-8 bytes. */
81
+ export function driverContextSha256(context) {
82
+ return createHash("sha256").update(context, "utf8").digest("hex");
83
+ }
84
+ /** The context hash a binding set carries, or `undefined` when it has no context. */
85
+ export function bindingsDriverContextSha256(b) {
86
+ if (b.driverContext !== undefined)
87
+ return driverContextSha256(b.driverContext);
88
+ return b.driverContextSha256;
89
+ }
90
+ /**
91
+ * The fingerprint input document (identical to the Python builder). The
92
+ * top-level `driver_context_sha256` key is present ONLY when the request carries
93
+ * driver context, so a context-free document is byte-identical to before.
94
+ */
81
95
  export function continuationFingerprintInput(b) {
96
+ const contextSha = bindingsDriverContextSha256(b);
82
97
  return {
98
+ ...(contextSha !== undefined ? { driver_context_sha256: contextSha } : {}),
83
99
  v: CONTINUATION_WIRE_VERSION,
84
100
  operation: CONTINUATION_OPERATION,
85
101
  target: {
@@ -222,6 +238,25 @@ export function parseContinuationStatusResult(value) {
222
238
  receipt,
223
239
  };
224
240
  }
241
+ /**
242
+ * BAPI-1514 — retryable CI-waiting refusals. The server committed nothing and
243
+ * asks for a retry with the SAME request id, so the local recovery reference is
244
+ * KEPT (not cleared) and the next continue-child re-sends that same request.
245
+ */
246
+ export const CONTINUATION_CI_WAITING_REASONS = new Set([
247
+ "ci_pending_at_bound_head",
248
+ "ci_provider_read_unavailable",
249
+ "ci_observation_stale",
250
+ ]);
251
+ /**
252
+ * BAPI-1514 — whether a continuation result is a REAL dispatch (a committed
253
+ * receipt for this request). Only this may be counted against an intervention
254
+ * budget or continuation allowance; `ci_green_adopted` and every other refusal
255
+ * dispatched nothing and charged nothing.
256
+ */
257
+ export function continuationDispatchedJob(result) {
258
+ return result.outcome === "committed" && result.receipt !== null;
259
+ }
225
260
  /** The namespace (subdirectory) of the parked-child continuation references. */
226
261
  export const CONTINUATION_REFERENCE_NAMESPACE = "epic-child-continuations";
227
262
  /**
@@ -304,6 +339,11 @@ function parseReference(raw) {
304
339
  expectedHeadSha: (b.expectedHeadSha ?? null),
305
340
  sourceParkGeneration: b.sourceParkGeneration,
306
341
  };
342
+ if (b.driverContextSha256 !== undefined) {
343
+ if (typeof b.driverContextSha256 !== "string" || !FINGERPRINT_RE.test(b.driverContextSha256))
344
+ return null;
345
+ bindings.driverContextSha256 = b.driverContextSha256;
346
+ }
307
347
  if (!bindings.repoName || !bindings.epicRunId || !bindings.ticketKey || !bindings.parkedGate ||
308
348
  !nonNegInt(bindings.expectedRowVersion) || !nonNegInt(bindings.sourceParkGeneration) ||
309
349
  (bindings.expectedHeadSha !== null && !FULL_SHA_RE.test(bindings.expectedHeadSha))) {
@@ -332,11 +372,22 @@ export async function readContinuationReference(epicRunId, ticketKey, deps = {})
332
372
  const raw = await readRecoveryReferenceFile(CONTINUATION_REFERENCE_NAMESPACE, { epicRunId, ticketKey }, deps);
333
373
  return raw === null ? null : parseReference(raw);
334
374
  }
375
+ /**
376
+ * BAPI-1514 — the bindings as stored: the raw context replaced by its hash. A
377
+ * context-free binding set is returned unchanged, so its file is byte-identical.
378
+ */
379
+ function persistableBindings(b) {
380
+ const contextSha = bindingsDriverContextSha256(b);
381
+ const { driverContext: _omitContext, driverContextSha256: _omitSha, ...rest } = b;
382
+ return contextSha === undefined ? rest : { ...rest, driverContextSha256: contextSha };
383
+ }
335
384
  /** Durably write a reference (0700 directory, 0600 file, write-then-rename). */
336
385
  export async function writeContinuationReference(reference, deps = {}, key = reference.bindings) {
337
386
  // ``key`` only names the file (the caller may address the run by its epic key);
338
- // the stored bindings are always the exact, fingerprinted ones.
339
- await writeRecoveryReferenceFile(CONTINUATION_REFERENCE_NAMESPACE, key, reference, deps);
387
+ // the stored bindings are always the exact, fingerprinted ones. BAPI-1514: the
388
+ // raw driver context is never written — only its SHA-256, which is all a
389
+ // reload needs to recompute the same fingerprint.
390
+ await writeRecoveryReferenceFile(CONTINUATION_REFERENCE_NAMESPACE, key, { ...reference, bindings: persistableBindings(reference.bindings) }, deps);
340
391
  }
341
392
  /** Remove a child's reference once its outcome is authoritative. Never throws. */
342
393
  export async function clearContinuationReference(epicRunId, ticketKey, deps = {}) {
@@ -8,8 +8,10 @@
8
8
  * epic-implementer doctor [--json]
9
9
  * epic-implementer purge [--json]
10
10
  *
11
- * Every command talks only to the local SQLite ledger. Errors are surfaced as
12
- * sanitized messages (no stack traces, no secrets, no raw payloads) and
11
+ * The ledger commands talk only to the local SQLite ledger; the run-control,
12
+ * status, readiness, and read-only wait commands (for example `wait-run`) are
13
+ * authenticated Bridge API calls, each lazily imported so a local-only command
14
+ * never loads the HTTP graph. Errors are surfaced as sanitized messages (no stack traces, no secrets, no raw payloads) and
13
15
  * {@link runEpicImplementerCli} returns a process exit code rather than calling
14
16
  * `process.exit` itself (the bin wrapper owns that).
15
17
  *
@@ -28,13 +30,18 @@ import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
28
30
  import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
29
31
  import { CONDUCTOR_BIN_UNRESOLVED_ERROR, installConductorGitHooks, resolveConductorHookBin, } from "./git-hooks.js";
30
32
  import { HOOK_RUNTIME_PROBE_ACTION, HOOK_RUNTIME_PROBE_MARKER } from "./hook-runtime-qualification.js";
31
- import { runFileScopeGuardCli } from "./file-scope-guard.js";
33
+ import { DECLARED_TOUCHED_FILES_ENV, runFileScopeGuardCli } from "./file-scope-guard.js";
32
34
  import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
33
35
  import { buildConductorDoctorReport, formatConductorDoctorReport } from "./doctor.js";
36
+ import { nodeGithubMergeCredentialReadDeps, readLocalGithubRemote } from "./stored-merge-credential.js";
34
37
  import { createCommandDriftInspector } from "./command-drift-production.js";
38
+ import { createWorkerDatabaseDoctorInspector } from "./worker-database-doctor-inspector.js";
35
39
  import { writeJsonDocument } from "./json-output.js";
36
40
  import { resolveSupervisorConfig } from "./supervisor-config.js";
37
41
  import { VERSION } from "../version.generated.js";
42
+ // BAPI-1496: the inspector factory moved to its own module; re-exported so
43
+ // existing importers of this module keep working.
44
+ export { createWorkerDatabaseDoctorInspector };
38
45
  // `runSupervisor` is imported LAZILY inside runSuperviseCommand: it transitively
39
46
  // pulls in the SQLite store, and a static import here would force every
40
47
  // epic-implementer CLI invocation (and every test that mocks ./store.js for the
@@ -62,7 +69,7 @@ export function getConductorUsage() {
62
69
  "Local append-only event ledger for multi-agent coordination, plus run-control",
63
70
  "commands. The ledger commands talk ONLY to the local SQLite store",
64
71
  "(~/.config/bridge/events.db); the run-control commands (stop-run through",
65
- "rerun-ci), epic-status, and readiness call the Bridge API.",
72
+ "resolve-handoff), epic-status, readiness, and the read-only wait-run call the Bridge API.",
66
73
  "",
67
74
  "Commands:",
68
75
  " emit-event Append one semantic event to the ledger",
@@ -113,7 +120,7 @@ export function getConductorUsage() {
113
120
  " resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
114
121
  " Resume a paused run (a quarantined one only with --clear-quarantine);",
115
122
  " activation is re-checked server-side. Active/no-control is a no-op.",
116
- " continue-child --epic-run-id <id> --ticket-key <key> [--reason <text> --worker-exit-proof exited|fenced] [--json]",
123
+ " continue-child --epic-run-id <id> --ticket-key <key> [--reason <text> --worker-exit-proof exited|fenced] [--context-file <path>] [--json]",
117
124
  " grant-continuation --epic-run-id <id> --ticket-key <key> --reason <text> [--extra-continuations 1] [--extra-seconds <seconds>] [--json]",
118
125
  " Continue a parked child's CURRENT episode WITHOUT a reset: same attempt,",
119
126
  " retained work, only the allowance left. At most one dispatch per episode.",
@@ -127,6 +134,20 @@ export function getConductorUsage() {
127
134
  " Ask CI to re-run a ticket's FAILED checks at its bound head (guided: copy",
128
135
  " the command explain-run rendered). Charges NO allowance and never re-runs",
129
136
  " claude-review. A re-run is not a verdict: after CI is green, use resume-ci.",
137
+ " regenerate-plan --epic-run-id <id> --ticket-key <key> --route test|implementation --reason <text> [--json]",
138
+ " Answer a child parked on a NAMED plan failure with ONE forced plan",
139
+ " regeneration on the chosen route. Success means the override and gate",
140
+ " reset committed, not that a usable plan exists. Prints \"server too old\"",
141
+ " and exits nonzero on a server without the route; no fallback is tried.",
142
+ " resolve-handoff --epic-run-id <id> --ticket-key <key> --steps 27,36 --kind verified|risk_accepted",
143
+ " --evidence <ref> --reason <text> [--json]",
144
+ " Record the OWNER's decision on verification hand-offs (a human decision,",
145
+ " never automatic). Also remove .bridge/epic-workarounds/<KEY>.md in a new",
146
+ " epic-head commit: the PR leaves draft only when both holds are clear.",
147
+ " wait-run --epic-run-id <id> [--ticket-key <key>] [--since <token>] [--timeout <seconds>] [--json]",
148
+ " Read-only: wait until the run's observable state changes, the run finishes,",
149
+ " or the window closes (default 110 s; values above 240 are clamped to 240).",
150
+ " Pass the previous result's token as --since. Changes nothing.",
130
151
  "",
131
152
  "supervise options:",
132
153
  " --run-id <id> Run/session identifier to supervise (required)",
@@ -198,7 +219,7 @@ export function getConductorUsage() {
198
219
  " explicit skipped state (enforcement UNVERIFIED, never enforced)",
199
220
  "",
200
221
  "Examples:",
201
- " epic-implementer emit-event --type run.started --source git-hook --run-id BAPI-393 \\",
222
+ " epic-implementer emit-event --type run.started --source git-hook --run-id PROJ-393 \\",
202
223
  " --data-json '{\"summary\":\"run started\"}'",
203
224
  " epic-implementer emit-event --type git.commit_created --source git-hook \\",
204
225
  " --raw-json '{\"branch\":\"feature/x\",\"sha\":\"abc123\"}'",
@@ -265,17 +286,25 @@ export function getConductorUsage() {
265
286
  " # The run is done for good — close it out:",
266
287
  " epic-implementer abandon-run --epic-run-id 3f9c2b7e-...",
267
288
  " # A ticket parked itself (needs_human) and the cause is now fixed:",
268
- " epic-implementer unpark --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
289
+ " epic-implementer unpark --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852",
269
290
  " # A human pushed directly to the PR and the ticket parked on head drift:",
270
- " epic-implementer adopt-current-head-and-unpark --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
291
+ " epic-implementer adopt-current-head-and-unpark --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852",
271
292
  " # A child stopped mid-attempt and should carry on WITHOUT resetting its budget:",
272
- " epic-implementer continue-child --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
293
+ " epic-implementer continue-child --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852",
273
294
  " # The continue-child response was lost — did it commit?",
274
- " epic-implementer continuation-receipt --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
295
+ " epic-implementer continuation-receipt --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852",
275
296
  " # CI finished green after the child parked on an expired CI wait:",
276
- " epic-implementer resume-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
297
+ " epic-implementer resume-ci --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852",
277
298
  " # CI failed on infrastructure: re-run it (never claude-review); then resume-ci once green:",
278
- " epic-implementer rerun-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852 --binding <token>",
299
+ " epic-implementer rerun-ci --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852 --binding <token>",
300
+ " # A child parked on a named plan failure (see explain-run for the route and reason):",
301
+ " epic-implementer regenerate-plan --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852 \\",
302
+ " --route implementation --reason \"The test plan named a target that is not indexed\"",
303
+ " # The owner accepted the risk of a step a worker could not verify (verification incomplete):",
304
+ " epic-implementer resolve-handoff --epic-run-id 3f9c2b7e-... --ticket-key PROJ-852 --steps 27 \\",
305
+ " --kind risk_accepted --evidence owner-review-2026-10-11 --reason \"Owner accepts the risk\"",
306
+ " # Wait for the next change before the next full pass (read-only):",
307
+ " epic-implementer wait-run --epic-run-id 3f9c2b7e-... --since v1:0123456789abcdef",
279
308
  ].join("\n");
280
309
  }
281
310
  /**
@@ -334,6 +363,12 @@ const VALID_COMMANDS = new Set([
334
363
  "resume-ci",
335
364
  // BAPI-1388: CLI-only guided provider re-run of failed CI workflow runs — never an MCP tool.
336
365
  "rerun-ci",
366
+ // BAPI-1494: CLI-only owner decision on verification hand-offs — never an MCP tool.
367
+ "resolve-handoff",
368
+ // BAPI-1532: CLI-only answer to a named plan failure — never an MCP tool.
369
+ "regenerate-plan",
370
+ // BAPI-1469: CLI-only read-only bounded wait on a run's state — never an MCP tool.
371
+ "wait-run",
337
372
  // Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
338
373
  // the bundled-artifact regression guard for BAPI-772 (mirroring `plane
339
374
  // __entrypoint`), not as a supported operator workflow.
@@ -741,11 +776,28 @@ export async function runDoctorCommand(argv, deps = {}) {
741
776
  // injected inspector wins so tests stay hermetic.
742
777
  const doctorDeps = {
743
778
  inspectCommandDrift: createCommandDriftInspector({ env: deps.env ?? process.env, cwd: process.cwd() }),
779
+ // BAPI-1479: advisory, read-only worker-database rows; runs under
780
+ // --no-deny-probe too. A caller's injected inspector wins.
781
+ inspectWorkerDatabase: createWorkerDatabaseDoctorInspector({ env: deps.env ?? process.env, cwd: process.cwd() }),
782
+ // BAPI-1461: a merge token stored once for this repository counts as
783
+ // configured — after the bounded capability probe confirms GitHub accepts
784
+ // it. Doctor runs in the person's own session, so the stored rung applies.
785
+ storedMergeCredential: {
786
+ allowStoredEntry: true,
787
+ localRemote: await readLocalGithubRemote(process.cwd()),
788
+ storeDeps: nodeGithubMergeCredentialReadDeps(deps.env ?? process.env),
789
+ // Loaded only when a stored token exists: the probe brings the merge
790
+ // executor's module graph, which an ordinary doctor run never needs.
791
+ probe: async (args) => {
792
+ const { probeStoredMergeToken } = await import("./stored-merge-token-probe.js");
793
+ return probeStoredMergeToken({ ...args, env: deps.env ?? process.env });
794
+ },
795
+ },
744
796
  ...deps,
745
797
  };
746
798
  const report = await buildConductorDoctorReport(bools.has("--no-deny-probe") ? { ...doctorDeps, skipDenyProbe: true } : doctorDeps);
747
799
  if (bools.has("--json")) {
748
- const written = await writeJsonDocument(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement, ...(report.command_drift ? { command_drift: report.command_drift } : {}) }));
800
+ const written = await writeJsonDocument(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement, ...(report.command_drift ? { command_drift: report.command_drift } : {}), ...(report.worker_database ? { worker_database: report.worker_database } : {}) }));
749
801
  return written ? 0 : 1;
750
802
  }
751
803
  console.log(formatConductorDoctorReport(report));
@@ -1235,10 +1287,15 @@ export async function runEpicStatusCommand(argv) {
1235
1287
  const dispatchRef = t.dispatch_run_id ? `dispatch_run_id=${t.dispatch_run_id}` : "-";
1236
1288
  lines.push(` ${t.ticket_key} [${t.status}] ${dispatchRef} remediation=${t.remediation_attempts}/${t.remediation_no_progress_attempts}`);
1237
1289
  }
1238
- lines.push(`\nDispatches (${state.dispatches.length}):`);
1239
- for (const d of state.dispatches) {
1240
- const runRef = d.run_id ? `run_id=${d.run_id}` : "-";
1241
- lines.push(` ${d.dispatch_key} [${d.status}] ${runRef}`);
1290
+ if (state.dispatches === null) {
1291
+ lines.push("\nDispatches: not applicable to this run");
1292
+ }
1293
+ else {
1294
+ lines.push(`\nDispatches (${state.dispatches.length}):`);
1295
+ for (const d of state.dispatches) {
1296
+ const runRef = d.run_id ? `run_id=${d.run_id}` : "-";
1297
+ lines.push(` ${d.dispatch_key} [${d.status}] ${runRef}`);
1298
+ }
1242
1299
  }
1243
1300
  console.log(lines.join("\n"));
1244
1301
  return 0;
@@ -1356,31 +1413,49 @@ export async function runPurgeCommand(argv) {
1356
1413
  ].join("\n"));
1357
1414
  return 0;
1358
1415
  }
1416
+ const LIKELY_TESTS_FAILURE_NOTE = "Likely-tests discovery unavailable: internal discovery failure (not an empty result).\n";
1359
1417
  export async function runLikelyTestsCommand(argv, deps = {}) {
1360
- const { resolveLikelyTests, isFullCommitSha, validTestReference, DISCOVERY_NOTES } = await import("../executor/likely-tests.js");
1361
1418
  const stdout = deps.stdout ?? ((text) => { process.stdout.write(text); });
1362
1419
  const stderr = deps.stderr ?? ((text) => { process.stderr.write(text); });
1363
- let base;
1364
- if (argv.length === 2 && argv[0] === "--base")
1365
- base = argv[1];
1366
- if (argv.length === 1 && argv[0].startsWith("--base="))
1367
- base = argv[0].slice(7);
1368
- if (!base || !isFullCommitSha(base)) {
1369
- stderr(DISCOVERY_NOTES.invalid_arguments + "\n");
1370
- return 0;
1371
- }
1372
1420
  try {
1373
- const result = await (deps.resolve ?? resolveLikelyTests)({ cwd: deps.cwd ?? process.cwd(), comparisonSha: base });
1374
- for (const reference of result.references)
1375
- if (validTestReference(reference))
1421
+ const { resolveLikelyTests, isFullCommitSha, validTestReference, DISCOVERY_NOTES } = await import("../executor/likely-tests.js");
1422
+ let base;
1423
+ if (argv.length === 2 && argv[0] === "--base")
1424
+ base = argv[1];
1425
+ if (argv.length === 1 && argv[0].startsWith("--base="))
1426
+ base = argv[0].slice(7);
1427
+ if (!base || !isFullCommitSha(base)) {
1428
+ stderr(DISCOVERY_NOTES.invalid_arguments + "\n");
1429
+ return 1;
1430
+ }
1431
+ const declared = (deps.env ?? process.env)[DECLARED_TOUCHED_FILES_ENV];
1432
+ const result = await (deps.resolve ?? resolveLikelyTests)({
1433
+ cwd: deps.cwd ?? process.cwd(),
1434
+ comparisonSha: base,
1435
+ ...(declared !== undefined && declared.trim().length > 0 ? { declaredTouchedFilesJson: declared } : {}),
1436
+ });
1437
+ let printed = 0;
1438
+ for (const reference of result.references) {
1439
+ if (validTestReference(reference)) {
1376
1440
  stdout(reference + "\n");
1377
- for (const diagnostic of result.diagnostics)
1378
- stderr(DISCOVERY_NOTES[diagnostic] + "\n");
1441
+ printed += 1;
1442
+ }
1443
+ }
1444
+ const failed = result.diagnostics.some(d => d === "git_unavailable" || d === "invalid_arguments");
1445
+ for (const diagnostic of new Set(result.diagnostics)) {
1446
+ if (diagnostic !== "empty")
1447
+ stderr(DISCOVERY_NOTES[diagnostic] + "\n");
1448
+ }
1449
+ if (failed)
1450
+ return 1;
1451
+ if (printed === 0)
1452
+ stderr(DISCOVERY_NOTES.empty + "\n");
1453
+ return 0;
1379
1454
  }
1380
1455
  catch {
1381
- stderr(DISCOVERY_NOTES.git_unavailable + "\n");
1456
+ stderr(LIKELY_TESTS_FAILURE_NOTE);
1457
+ return 1;
1382
1458
  }
1383
- return 0;
1384
1459
  }
1385
1460
  /**
1386
1461
  * Dispatch the epic-implementer CLI. Returns (asynchronously) a process exit
@@ -1521,6 +1596,29 @@ export async function runEpicImplementerCli(argv, likelyTestsDeps) {
1521
1596
  const { runRerunCiCommand } = await import("./recovery-cli.js");
1522
1597
  return await runRerunCiCommand(parsed.argv);
1523
1598
  }
1599
+ case "resolve-handoff": {
1600
+ // BAPI-1494: delegates only to the owner-decision recovery operation.
1601
+ const { runResolveHandoffCommand } = await import("./recovery-cli.js");
1602
+ return await runResolveHandoffCommand(parsed.argv);
1603
+ }
1604
+ case "regenerate-plan": {
1605
+ // BAPI-1532: delegates only to the pinned plan-regeneration operation.
1606
+ const { runRegeneratePlanCommand } = await import("./recovery-cli.js");
1607
+ return await runRegeneratePlanCommand(parsed.argv);
1608
+ }
1609
+ case "wait-run": {
1610
+ // BAPI-1469: read-only bounded wait. Lazily imported like the recovery
1611
+ // verbs; a module-load failure gets fixed wording, never exception text.
1612
+ let runWaitRunCommand;
1613
+ try {
1614
+ ({ runWaitRunCommand } = await import("./wait-run-cli.js"));
1615
+ }
1616
+ catch {
1617
+ console.error("Error: wait-run could not be loaded; no run state was read.");
1618
+ return 1;
1619
+ }
1620
+ return await runWaitRunCommand(parsed.argv);
1621
+ }
1524
1622
  default:
1525
1623
  console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
1526
1624
  return 1;
@@ -668,7 +668,7 @@ export async function runEpicTick(options, deps = {}) {
668
668
  // on the machine; rebuildObservedState only folds signals whose run_id maps
669
669
  // to one of these dispatches, so scoping the read here avoids loading the
670
670
  // entire (up to 50K-row) ledger on every tick.
671
- const dispatchedRunIds = epicRunState.dispatches
671
+ const dispatchedRunIds = (epicRunState.dispatches ?? [])
672
672
  .map((d) => d.run_id)
673
673
  .filter((rid) => typeof rid === "string" && rid.length > 0);
674
674
  const localEvents = await fetchLocalEvents(epic_key, dispatchedRunIds);
@@ -1005,7 +1005,7 @@ export async function runEpicTick(options, deps = {}) {
1005
1005
  const latestDispatchByTicket = new Map();
1006
1006
  const reviewLatestDispatchByTicket = new Map();
1007
1007
  const reviewAttemptCounts = new Map();
1008
- for (const d of epicRunState.dispatches) {
1008
+ for (const d of epicRunState.dispatches ?? []) {
1009
1009
  const isReview = d.dispatch_key.endsWith(":review");
1010
1010
  if (isReview) {
1011
1011
  reviewAttemptCounts.set(d.ticket_key, (reviewAttemptCounts.get(d.ticket_key) ?? 0) + 1);
@@ -423,7 +423,7 @@ export function rebuildObservedState(postgresState, events, _now) {
423
423
  // complete → proceed) instead of mis-folding it as implementation completion
424
424
  // ("ready_for_review", awaiting merge), which would skip implementation.
425
425
  const runIdToTicketKey = new Map();
426
- for (const dispatch of dispatches) {
426
+ for (const dispatch of dispatches ?? []) {
427
427
  if (dispatch.run_id) {
428
428
  runIdToTicketKey.set(dispatch.run_id, {
429
429
  ticketKey: dispatch.ticket_key,
@@ -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/EPIC-IMPLEMENTER.md.`,
92
+ `Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See docs/EPIC-IMPLEMENTER.md in the ${MCP_PACKAGE_NAME} package.`,
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 " +
@@ -129,7 +129,9 @@ export async function runConductorInstallDoctor(deps) {
129
129
  }
130
130
  else {
131
131
  try {
132
- readiness = await fetchConductorReadiness(deps.access, deps.fetch);
132
+ readiness = deps.epicRunId
133
+ ? await fetchConductorReadiness(deps.access, deps.fetch, { epicRunId: deps.epicRunId })
134
+ : await fetchConductorReadiness(deps.access, deps.fetch);
133
135
  }
134
136
  catch (err) {
135
137
  const kind = err instanceof ConductorBridgeApiError ? err.kind : "network";
@@ -158,7 +160,7 @@ export async function runConductorInstallDoctor(deps) {
158
160
  // BAPI-1405 — the three warn-only epic-branch CI rows, beside the gate
159
161
  // profile they qualify. Never fatal, so they cannot change the exit code or
160
162
  // the installer's pre-write abort. An older server renders them as
161
- // "Unreported — upgrade the server", never as "no warning".
163
+ // "Unreported by the server", never as "no warning".
162
164
  sections.push(...epicBranchCiSections(readiness.epic_branch_ci));
163
165
  sections.push(githubCredentialsSection(readiness));
164
166
  sections.push(githubActionsSection(readiness));