@bridge_gpt/mcp-server 0.2.68 → 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 (166) hide show
  1. package/README.md +226 -320
  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 +650 -98
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-review-workflow-drift.js +3 -3
  9. package/build/claude-settings.js +84 -2
  10. package/build/cli-help.js +7 -1
  11. package/build/codex-bridge-profile.js +1 -1
  12. package/build/codex-home.js +9 -1
  13. package/build/codex-launcher-pin.js +598 -0
  14. package/build/commands.generated.js +8 -8
  15. package/build/conductor/bridge-api-client.js +598 -19
  16. package/build/conductor/bring-up-facts.js +9 -9
  17. package/build/conductor/doctor.js +466 -18
  18. package/build/conductor/epic-child-continuation.js +54 -3
  19. package/build/conductor/epic-implementer-cli.js +132 -34
  20. package/build/conductor/epic-runtime.js +2 -2
  21. package/build/conductor/epic-state.js +1 -1
  22. package/build/conductor/hook-runtime-qualification.js +1 -1
  23. package/build/conductor/install-doctor.js +4 -2
  24. package/build/conductor/launch-need-probes.js +337 -0
  25. package/build/conductor/local-merge.js +241 -56
  26. package/build/conductor/merge-credential.js +172 -0
  27. package/build/conductor/merge-execution-eligibility.js +1 -1
  28. package/build/conductor/merged-child-check.js +95 -0
  29. package/build/conductor/plan-trim.js +30 -0
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +11 -6
  32. package/build/conductor/readiness-prerequisites.js +41 -0
  33. package/build/conductor/readiness-sections.js +345 -55
  34. package/build/conductor/readiness.js +99 -87
  35. package/build/conductor/recovery-cli.js +606 -21
  36. package/build/conductor/recovery-operations.js +306 -7
  37. package/build/conductor/repair-contract.js +47 -2
  38. package/build/conductor/repository-workflow-remedy.js +8 -6
  39. package/build/conductor/stored-merge-credential.js +76 -0
  40. package/build/conductor/stored-merge-token-probe.js +28 -0
  41. package/build/conductor/temporary-plan.js +50 -0
  42. package/build/conductor/wait-run-cli.js +759 -0
  43. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  44. package/build/conductor-bin.js +2 -2
  45. package/build/conductor-claude-hook-bin.js +2 -2
  46. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  47. package/build/conductor-removed-stub-bin.js +2 -5
  48. package/build/connect-github-api.js +33 -14
  49. package/build/credential-store.js +356 -13
  50. package/build/docs.generated.js +27 -14
  51. package/build/doctor.js +203 -38
  52. package/build/drive-epic-help.js +212 -0
  53. package/build/drive-epic.js +1840 -373
  54. package/build/epic-ci-workflow-drift.js +77 -0
  55. package/build/epic-ci-workflow-install.js +183 -0
  56. package/build/epic-ci-workflow.js +157 -0
  57. package/build/epic-contract-context.js +326 -0
  58. package/build/epic-implementer-bin.js +131 -120
  59. package/build/epic-implementer-claude-hook-bin.js +2 -2
  60. package/build/escalation-reachability.js +134 -0
  61. package/build/executor/cleanup-report.js +99 -0
  62. package/build/executor/cli.js +12 -1
  63. package/build/executor/completion-contract.js +68 -11
  64. package/build/executor/completion-supervision.js +99 -0
  65. package/build/executor/delivery-outcome.js +179 -0
  66. package/build/executor/deps.js +3 -0
  67. package/build/executor/env.js +71 -0
  68. package/build/executor/failure-reason-codes.js +126 -0
  69. package/build/executor/heartbeat.js +38 -0
  70. package/build/executor/http-client.js +115 -4
  71. package/build/executor/integration-job.js +298 -5
  72. package/build/executor/job-errors.js +36 -6
  73. package/build/executor/job-runner.js +720 -36
  74. package/build/executor/likely-tests.js +36 -2
  75. package/build/executor/merge-job.js +145 -2
  76. package/build/executor/observation.js +73 -4
  77. package/build/executor/permissions.js +193 -97
  78. package/build/executor/preflight.js +14 -3
  79. package/build/executor/process-containment.js +28 -2
  80. package/build/executor/prompt-spec.js +50 -5
  81. package/build/executor/rebase-finalization.js +38 -3
  82. package/build/executor/reconcile-overlap-job.js +35 -7
  83. package/build/executor/required-work-ledger.js +236 -81
  84. package/build/executor/resume-pre-spawn.js +38 -1
  85. package/build/executor/runner.js +91 -3
  86. package/build/executor/test-clock.js +8 -0
  87. package/build/executor/types.js +37 -2
  88. package/build/executor/worker-cli-launcher.js +259 -0
  89. package/build/executor/worker-command.js +41 -4
  90. package/build/executor/worker-database-target.js +91 -0
  91. package/build/executor/worker-database.js +908 -0
  92. package/build/executor/worker-finalization.js +106 -0
  93. package/build/executor/worker-guard-hook.js +203 -0
  94. package/build/executor/worker-resources.js +47 -0
  95. package/build/executor/worker-verification-guidance.js +15 -2
  96. package/build/executor/worktree.js +303 -15
  97. package/build/executor-launch.js +11 -1
  98. package/build/generated-assets-doctor.js +9 -1
  99. package/build/generated-assets-health.js +13 -4
  100. package/build/generated-assets-legacy.js +39 -0
  101. package/build/generated-assets-paths.js +12 -1
  102. package/build/generated-assets-repair.js +4 -1
  103. package/build/generated-assets-test-support.js +12 -0
  104. package/build/github-remote.js +33 -0
  105. package/build/github-sign-in.js +62 -0
  106. package/build/index.js +1214 -465
  107. package/build/init.js +39 -1
  108. package/build/install-bridge-conductor-gate.js +78 -0
  109. package/build/install-bridge-conductor-setup.js +413 -0
  110. package/build/install-bridge-conductor.js +1226 -267
  111. package/build/install-bridge.js +629 -350
  112. package/build/launcher-config-inspection.js +184 -19
  113. package/build/mcp-host-targets.js +4 -0
  114. package/build/mcp-identity.js +7 -0
  115. package/build/mcp-install-state.js +8 -2
  116. package/build/merge-pull-request.js +6 -0
  117. package/build/merge-workflow-install.js +763 -345
  118. package/build/phase-result-artifacts.js +75 -20
  119. package/build/pipeline-orchestrator.js +247 -4
  120. package/build/pipelines.generated.js +35 -35
  121. package/build/plan-discovery.js +15 -2
  122. package/build/plan-execution-ledger.js +306 -17
  123. package/build/plan-phase-routing.js +48 -13
  124. package/build/plane/cli.js +105 -51
  125. package/build/plane/defaults.js +3 -0
  126. package/build/plane/plane-freshness.js +294 -0
  127. package/build/plane/schema-compatibility.js +137 -0
  128. package/build/plane/shutdown-deps.js +38 -0
  129. package/build/plane/shutdown.js +83 -8
  130. package/build/plane/terminal-run-cleanup.js +303 -0
  131. package/build/printed-remedy-commands.js +26 -4
  132. package/build/readiness-check.js +6 -2
  133. package/build/readiness-prerequisites.generated.js +532 -0
  134. package/build/readme.generated.js +1 -1
  135. package/build/session-upgrade-notice.js +86 -0
  136. package/build/setup-epic.js +400 -30
  137. package/build/shared-resources.generated.js +5 -0
  138. package/build/start-tickets.js +43 -15
  139. package/build/test-command-detection.js +247 -0
  140. package/build/test-command-validation.js +73 -0
  141. package/build/testing-config-client.js +150 -0
  142. package/build/ticket-delivery-artifacts.js +258 -0
  143. package/build/ticket-delivery-enrollment.js +306 -0
  144. package/build/ticket-delivery.js +205 -0
  145. package/build/ticket-review-summary.js +14 -0
  146. package/build/tool-call-advisory.js +162 -0
  147. package/build/tool-surface-gating.js +28 -89
  148. package/build/update-check.js +97 -16
  149. package/build/update-status.js +61 -71
  150. package/build/upgrade-advice.js +15 -11
  151. package/build/upgrade-cli.js +360 -20
  152. package/build/v2-readiness-requirements.js +552 -0
  153. package/build/version.generated.js +3 -3
  154. package/build/worker-guard-hook-bin.js +2 -2
  155. package/build/worktree-core.js +22 -7
  156. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  157. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  158. package/docs/EPIC-IMPLEMENTER.md +104 -63
  159. package/docs/epic-implementer/getting-started.md +131 -0
  160. package/docs/epic-implementer/self-hosting.md +133 -0
  161. package/package.json +3 -3
  162. package/pipelines/idea-to-ticket.json +1 -1
  163. package/pipelines/implement-ticket.json +8 -8
  164. package/pipelines/review-ticket.json +2 -2
  165. package/design-assets/favicon/favicon-32x32.png +0 -0
  166. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -1,5 +1,5 @@
1
- /** The pinned operator runbook every remedy below points into. */
2
- export const BRING_UP_RUNBOOK = "docs/epic-implementer/operator-runbook.md";
1
+ /** Where the owner-only bring-up steps these remedies refer to are printed. */
2
+ export const BRING_UP_RUNBOOK = "`drive-epic --help-advanced`";
3
3
  /**
4
4
  * The client-derived refusal vocabulary, IN REFUSAL ORDER.
5
5
  *
@@ -20,20 +20,20 @@ export const BRING_UP_REFUSAL_REASONS = [
20
20
  // ---------------------------------------------------------------------------
21
21
  // Fixed remediations
22
22
  // ---------------------------------------------------------------------------
23
- export const BRING_UP_UNREPORTED_REMEDIATION = "upgrade the Bridge API deploy to a build that reports the bring-up safety facts; " +
24
- "their state is unknown here, not healthy.";
23
+ export const BRING_UP_UNREPORTED_REMEDIATION = "retry later; the server did not report the bring-up safety facts, so their state is unknown here, " +
24
+ "not healthy. If it persists, email info@bridgegpt.ai.";
25
25
  export const DATABASE_MIGRATION_UNREADABLE_REMEDIATION = "retry; the migration state could not be read, which is not the same as confirmed-behind. " +
26
- `If it persists, check the applied revision with \`alembic current\` (${BRING_UP_RUNBOOK} §3d).`;
26
+ `If it persists, check the applied revision with \`alembic current\` (see ${BRING_UP_RUNBOOK}).`;
27
27
  export const DATABASE_MIGRATION_BEHIND_REMEDIATION = "confirm the applied revision with `alembic current` (not `alembic heads`, which reads the " +
28
- "migration scripts), apply this checkout's migrations with `alembic -c alembic.ini upgrade head`, " +
29
- `then retry (${BRING_UP_RUNBOOK} §3d).`;
28
+ "migration scripts), apply this checkout's migrations with `alembic upgrade head`, " +
29
+ `then retry (see ${BRING_UP_RUNBOOK}).`;
30
30
  export const WORKER_HEARTBEAT_UNREADABLE_REMEDIATION = "retry once the Bridge API database is reachable; the reconciler heartbeat could not be read, " +
31
31
  "so worker presence is unknown, not confirmed absent.";
32
32
  export const WORKER_NOT_PRESENT_REMEDIATION = "start a worker (`python worker.py`, or bring up a local plane) — a server started with " +
33
33
  "DISABLE_SCHEDULER=true runs none. Create and approve only against a server that has a " +
34
- `worker (${BRING_UP_RUNBOOK} §3d).`;
34
+ `worker (see ${BRING_UP_RUNBOOK}).`;
35
35
  export const SECOND_RECONCILER_REMEDIATION = "find and stop the extra reconciler: check `ps aux | grep worker.py` for a reparented " +
36
- `survivor of an earlier plane (${BRING_UP_RUNBOOK} §6), and note that a dev server started ` +
36
+ `survivor of an earlier plane (see ${BRING_UP_RUNBOOK}), and note that a dev server started ` +
37
37
  "without DISABLE_SCHEDULER=true runs a reconciler too. A reconciler stopped in the last 120 " +
38
38
  "seconds still counts — heartbeat rows are not deleted on shutdown — so wait and retry. " +
39
39
  "On a hosted deployment, check that exactly one worker process is scaled (for example one " +
@@ -14,10 +14,13 @@ import { doctorConductorLedger } from "./store.js";
14
14
  import { detectClaudeLogin, formatClaudeLoginAdvisory } from "../claude-login.js";
15
15
  import { inspectConductorGitHooks } from "./git-hooks.js";
16
16
  import { MERGE_WORKFLOW_IDENTIFIER } from "./merge-execution-contract.js";
17
+ import { LOCAL_MERGE_CREDENTIAL_ALIAS_ENV_KEY, LOCAL_MERGE_CREDENTIAL_ENV_KEYS, resolveLocalMergeCredentialDetailed, } from "./merge-credential.js";
18
+ import { resolveMergeCredentialAtAction } from "./stored-merge-credential.js";
17
19
  import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
18
20
  import { projectCommandDriftRow } from "./command-drift.js";
19
21
  import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
20
22
  import { resolveProfiles } from "../mcp-profile.js";
23
+ import { isValidWorkerDatabaseName } from "../executor/worker-database-target.js";
21
24
  import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
22
25
  /**
23
26
  * Remediation for a conductor context whose resolved MCP groups omit `epic_implementer`.
@@ -53,7 +56,7 @@ export const GH_CLI_REMEDIATION = "install the GitHub CLI (`gh`) and put it on P
53
56
  * and changes nothing. `merge-pull-request.ts` says exactly that about the same
54
57
  * text; this is the last place it was still live.
55
58
  */
56
- export const GH_AUTH_REMEDIATION = "set BAPI_LOCAL_MERGE_TOKEN (or BAPI_MACHINE_MERGE_TOKEN for machine-user mode) in the " +
59
+ export const GH_AUTH_REMEDIATION = "set GITHUB_MERGE_TOKEN (or BAPI_MACHINE_MERGE_TOKEN for machine-user mode) in the " +
57
60
  "environment this host launches the conductor with; local merge has no ambient-login " +
58
61
  "fallback and fails closed with a named reason when neither is configured.";
59
62
  /** Detail used when the credential check was not reached because `gh` is absent. */
@@ -80,6 +83,342 @@ export const DENY_ENFORCEMENT_SKIP_REMEDIATION = "run `epic-implementer doctor`
80
83
  "preflight; `epic-implementer readiness` never spawns that probe itself.";
81
84
  /** Explicit display value for an unset `BRIDGE_MCP_PROFILE`. */
82
85
  export const MCP_PROFILE_UNSET = "(unset)";
86
+ /** BAPI-1461: the printed setup command, used in stored-token remedies. */
87
+ const PRINTED_SETUP_COMMAND = `npx -y ${MCP_PACKAGE_NAME} install conductor`;
88
+ /** BAPI-1461: the fix for a stored token GitHub rejected. */
89
+ export const STORED_MERGE_TOKEN_REJECTED_REMEDIATION = `replace the stored merge token: run \`${PRINTED_SETUP_COMMAND}\` and accept the offer to ` +
90
+ `replace it, or remove it with \`${PRINTED_SETUP_COMMAND} --remove-merge-token\`.`;
91
+ /** BAPI-1461: the advice when the stored token could not be checked. */
92
+ export const STORED_MERGE_TOKEN_UNVERIFIED_REMEDIATION = "the stored merge token could not be checked right now (GitHub or the credential store did " +
93
+ "not answer). This is not a rejection; run doctor again later.";
94
+ /**
95
+ * BAPI-1461: collect the stored-entry finding. It runs only when no LOCAL
96
+ * credential is set in the environment (an env value always wins, and an
97
+ * invalid one is never rescued), resolves through the shared resolver, and —
98
+ * for a stored token — runs the bounded capability probe. Never throws and
99
+ * never returns the token.
100
+ */
101
+ export async function collectStoredMergeCredentialEvidence(env, context) {
102
+ if (resolveLocalMergeCredentialDetailed(env, "local").status !== "missing")
103
+ return { status: "absent" };
104
+ let resolution;
105
+ try {
106
+ resolution = await resolveMergeCredentialAtAction(env, "local", context);
107
+ }
108
+ catch {
109
+ return { status: "unverified" };
110
+ }
111
+ if (resolution.status === "store-unavailable")
112
+ return { status: "unverified" };
113
+ if (resolution.status === "invalid")
114
+ return { status: "rejected", reason: "invalid" };
115
+ if (resolution.status !== "resolved" || resolution.source !== "store")
116
+ return { status: "absent" };
117
+ if (context.probe === undefined)
118
+ return { status: "configured" };
119
+ const repository = context.providerRepository ?? context.localRemote ?? null;
120
+ if (repository === null)
121
+ return { status: "configured" };
122
+ let verdict;
123
+ try {
124
+ verdict = await context.probe({ repository, credential: resolution.credential });
125
+ }
126
+ catch {
127
+ return { status: "unverified" };
128
+ }
129
+ switch (verdict) {
130
+ case "passed":
131
+ return { status: "configured" };
132
+ case "credential_invalid":
133
+ return { status: "rejected", reason: "invalid" };
134
+ case "no_access":
135
+ return { status: "rejected", reason: "unpermitted" };
136
+ default:
137
+ // An unavailable or inconclusive probe is uncertainty — never a revocation.
138
+ return { status: "unverified" };
139
+ }
140
+ }
141
+ /**
142
+ * BAPI-1452: the note printed when the deprecated local alias supplies the
143
+ * credential. Names the canonical key; never mentions the machine credential,
144
+ * which has no alias and is not deprecated.
145
+ */
146
+ export const LOCAL_MERGE_ALIAS_DEPRECATION_NOTE = `the local merge credential is being read from the deprecated ${LOCAL_MERGE_CREDENTIAL_ALIAS_ENV_KEY}; ` +
147
+ `it still works, but rename it to ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local}.`;
148
+ // ---------------------------------------------------------------------------
149
+ // Worker-database advisory section (BAPI-1479)
150
+ // ---------------------------------------------------------------------------
151
+ /**
152
+ * Runbook section every worker-database row points to. Mirrors
153
+ * `WORKER_DATABASE_RUNBOOK_SECTION` in `executor/worker-database.ts`, which is
154
+ * not imported at runtime (see the import note above); a test pins the two
155
+ * values together.
156
+ */
157
+ export const WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION = "docs/claude/runbooks/local-db-safety.md#worker-databases";
158
+ /** Who acts on every worker-database row. */
159
+ export const WORKER_DATABASE_DOCTOR_OWNER = "host operator";
160
+ /** Hard ceiling on one worker-database inspection, so it can never hang doctor. */
161
+ export const WORKER_DATABASE_DOCTOR_TIMEOUT_MS = 20_000;
162
+ /** Maximum validated names listed per inventory group; the rest are counted. */
163
+ export const WORKER_DATABASE_DOCTOR_NAME_LIMIT = 20;
164
+ const ROW_STATES = new Set(["healthy", "unsafe", "unknown"]);
165
+ const RETAIN_REASONS = new Set([
166
+ "owner_alive",
167
+ "owner_unknown",
168
+ "foreign_host",
169
+ "no_comment",
170
+ "malformed_comment",
171
+ "ownership_changed",
172
+ "drop_failed",
173
+ ]);
174
+ const UNAVAILABLE_CATEGORIES = new Set([
175
+ "client_missing",
176
+ "postgres_unavailable",
177
+ "role_missing",
178
+ "role_misconfigured",
179
+ "bridgeapi_reachable",
180
+ "boundary_unknown",
181
+ "alembic_missing",
182
+ "create_failed",
183
+ "migration_failed",
184
+ "timeout",
185
+ "cancelled",
186
+ ]);
187
+ function safeState(value) {
188
+ return typeof value === "string" && ROW_STATES.has(value) ? value : "unknown";
189
+ }
190
+ function safeCategory(value) {
191
+ return typeof value === "string" && UNAVAILABLE_CATEGORIES.has(value)
192
+ ? value
193
+ : undefined;
194
+ }
195
+ /** Fixed prose for a category that made a check unavailable or unsafe. */
196
+ function describeUnavailableCategory(category) {
197
+ switch (category) {
198
+ case "client_missing":
199
+ return "the psql client was not found";
200
+ case "postgres_unavailable":
201
+ return "local PostgreSQL could not be reached";
202
+ case "timeout":
203
+ return "the check timed out";
204
+ case "cancelled":
205
+ return "the check was cancelled";
206
+ default:
207
+ return "the check could not be completed";
208
+ }
209
+ }
210
+ function makeRow(id, label, state, detail) {
211
+ return {
212
+ id,
213
+ label,
214
+ state,
215
+ owner: WORKER_DATABASE_DOCTOR_OWNER,
216
+ detail,
217
+ ...(state === "healthy" ? {} : { runbook: WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION }),
218
+ };
219
+ }
220
+ function roleRow(role) {
221
+ const r = (role ?? {});
222
+ let state = safeState(r.state);
223
+ const category = safeCategory(r.category);
224
+ let detail;
225
+ if (state === "healthy") {
226
+ detail = "bridge_epic_worker exists with the expected attributes and no elevating memberships";
227
+ }
228
+ else if (state === "unsafe") {
229
+ detail =
230
+ category === "role_missing"
231
+ ? "bridge_epic_worker does not exist"
232
+ : "bridge_epic_worker attributes or memberships differ from the expected least-privilege set";
233
+ }
234
+ else if (category === "role_missing") {
235
+ // A definitive "missing" answer is actionable, not unknown.
236
+ state = "unsafe";
237
+ detail = "bridge_epic_worker does not exist";
238
+ }
239
+ else {
240
+ detail = `unknown — ${describeUnavailableCategory(category)}`;
241
+ }
242
+ return makeRow("worker-db-role", "worker role", state, detail);
243
+ }
244
+ function boundaryRow(boundary) {
245
+ const b = (boundary ?? {});
246
+ const state = safeState(b.state);
247
+ const category = safeCategory(b.category);
248
+ const detail = state === "healthy"
249
+ ? "bridge_epic_worker is refused on bridgeapi"
250
+ : state === "unsafe"
251
+ ? "UNSAFE — bridge_epic_worker can connect to bridgeapi"
252
+ : `unknown — ${category === "boundary_unknown" ? "the bridgeapi refusal could not be confirmed" : describeUnavailableCategory(category)}`;
253
+ return makeRow("worker-db-boundary", "bridgeapi boundary", state, detail);
254
+ }
255
+ function alembicRow(alembic) {
256
+ const state = safeState(alembic?.state);
257
+ const detail = state === "healthy"
258
+ ? "the operator checkout's .venv Alembic is present"
259
+ : state === "unsafe"
260
+ ? "the operator checkout's .venv Alembic is missing"
261
+ : "unknown — the operator checkout's .venv Alembic could not be checked";
262
+ return makeRow("worker-db-alembic", "operator-checkout Alembic", state, detail);
263
+ }
264
+ function safeNameList(value, redacted) {
265
+ if (!Array.isArray(value))
266
+ return [];
267
+ const out = [];
268
+ for (const entry of value) {
269
+ if (isValidWorkerDatabaseName(entry))
270
+ out.push(entry);
271
+ else
272
+ redacted.count += 1;
273
+ }
274
+ return out;
275
+ }
276
+ function sanitizeInventory(inventory) {
277
+ const inv = inventory;
278
+ if (!inv || inv.state !== "known" || typeof inv.value !== "object" || inv.value === null) {
279
+ return { state: "unknown" };
280
+ }
281
+ const value = inv.value;
282
+ if (!Array.isArray(value.active) ||
283
+ !Array.isArray(value.leftovers) ||
284
+ !Array.isArray(value.unverifiable) ||
285
+ typeof value.harnessCount !== "number" ||
286
+ !Number.isInteger(value.harnessCount) ||
287
+ value.harnessCount < 0) {
288
+ return { state: "unknown" };
289
+ }
290
+ const redacted = { count: 0 };
291
+ const active = safeNameList(value.active, redacted);
292
+ const leftovers = safeNameList(value.leftovers, redacted);
293
+ const unverifiable = [];
294
+ for (const entry of value.unverifiable) {
295
+ const e = (entry ?? {});
296
+ if (!isValidWorkerDatabaseName(e.name)) {
297
+ redacted.count += 1;
298
+ continue;
299
+ }
300
+ const reason = typeof e.reason === "string" && RETAIN_REASONS.has(e.reason)
301
+ ? e.reason
302
+ : "unrecognized";
303
+ unverifiable.push({ name: e.name, reason });
304
+ }
305
+ return {
306
+ state: "known",
307
+ active,
308
+ leftovers,
309
+ unverifiable,
310
+ harness_count: value.harnessCount,
311
+ redacted_count: redacted.count,
312
+ };
313
+ }
314
+ function inventoryRows(inventory) {
315
+ if (inventory.state === "unknown") {
316
+ const detail = "unknown — the database inventory could not be read";
317
+ return [
318
+ makeRow("worker-db-leftovers", "worker databases", "unknown", detail),
319
+ makeRow("worker-db-unverifiable", "unverifiable worker databases", "unknown", detail),
320
+ makeRow("worker-db-harness", "conductor_harness databases", "unknown", detail),
321
+ ];
322
+ }
323
+ const leftoverCount = inventory.leftovers.length;
324
+ // A redacted name is unverifiable by definition — it could not be validated.
325
+ const unverifiableCount = inventory.unverifiable.length + inventory.redacted_count;
326
+ return [
327
+ makeRow("worker-db-leftovers", "worker databases", leftoverCount === 0 ? "healthy" : "unsafe", `${inventory.active.length} active (owned by a live executor), ${leftoverCount} left over ` +
328
+ "(dead executor on this host; the next executor start sweeps them)"),
329
+ makeRow("worker-db-unverifiable", "unverifiable worker databases", unverifiableCount === 0 ? "healthy" : "unsafe", `${unverifiableCount} with unreadable, malformed, foreign-host, or unknown ownership ` +
330
+ "(left in place; review manually)"),
331
+ makeRow("worker-db-harness", "conductor_harness databases", inventory.harness_count === 0 ? "healthy" : "unsafe", `${inventory.harness_count} present (owned by the Python harness reaper; the executor never cleans them)`),
332
+ ];
333
+ }
334
+ /** Project a (possibly hostile or malformed) inspection into the sanitized advisory section. */
335
+ export function buildWorkerDatabaseDoctorSection(inspection) {
336
+ const insp = (inspection ?? {});
337
+ const inventory = inspection ? sanitizeInventory(insp.inventory) : { state: "unknown" };
338
+ return {
339
+ advisory: true,
340
+ owner: WORKER_DATABASE_DOCTOR_OWNER,
341
+ runbook: WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION,
342
+ rows: [
343
+ roleRow(insp.role),
344
+ boundaryRow(insp.boundary),
345
+ alembicRow(insp.alembic),
346
+ ...inventoryRows(inventory),
347
+ ],
348
+ inventory,
349
+ };
350
+ }
351
+ /**
352
+ * Run the worker-database inspector with a hard ceiling. A throw, a rejection,
353
+ * or a timeout yields an all-unknown section — never healthy, never a crash.
354
+ *
355
+ * BAPI-1496: exported so a local `drive-epic` launch can collect the same
356
+ * advisory section the doctor shows. `onFailure` receives only the closed
357
+ * `timeout`/`error` classification — never the thrown value — so a caller can
358
+ * say that the inspection failed without echoing exception text.
359
+ */
360
+ export async function collectWorkerDatabaseDoctorSection(inspect, timeoutMs = WORKER_DATABASE_DOCTOR_TIMEOUT_MS, onFailure) {
361
+ let timer;
362
+ try {
363
+ const timeout = new Promise((resolve) => {
364
+ timer = setTimeout(() => resolve(null), timeoutMs);
365
+ timer.unref?.();
366
+ });
367
+ const inspection = await Promise.race([Promise.resolve().then(inspect), timeout]);
368
+ if (inspection === null)
369
+ onFailure?.("timeout");
370
+ return buildWorkerDatabaseDoctorSection(inspection);
371
+ }
372
+ catch {
373
+ onFailure?.("error");
374
+ return buildWorkerDatabaseDoctorSection(null);
375
+ }
376
+ finally {
377
+ if (timer)
378
+ clearTimeout(timer);
379
+ }
380
+ }
381
+ const WORKER_DB_STATE_TAG = {
382
+ healthy: "[OK]",
383
+ unsafe: "[WARNING]",
384
+ unknown: "[UNKNOWN]",
385
+ };
386
+ function formatNameGroup(lines, heading, names) {
387
+ if (names.length === 0)
388
+ return;
389
+ lines.push(` ${heading}:`);
390
+ for (const name of names.slice(0, WORKER_DATABASE_DOCTOR_NAME_LIMIT))
391
+ lines.push(` - ${name}`);
392
+ if (names.length > WORKER_DATABASE_DOCTOR_NAME_LIMIT) {
393
+ lines.push(` … and ${names.length - WORKER_DATABASE_DOCTOR_NAME_LIMIT} more`);
394
+ }
395
+ }
396
+ /** Render the advisory worker-database section. Names are re-validated at render time. */
397
+ export function formatWorkerDatabaseDoctorSection(section) {
398
+ const lines = [];
399
+ lines.push("Worker databases (advisory, read-only)");
400
+ lines.push("──────────────────────────────────────");
401
+ for (const row of section.rows) {
402
+ const state = safeState(row.state);
403
+ lines.push(`${row.label.padEnd(30)} ${WORKER_DB_STATE_TAG[state]} ${state} — ${row.detail}`);
404
+ if (state !== "healthy") {
405
+ lines.push(` → ${WORKER_DATABASE_DOCTOR_OWNER}: see ${WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION}`);
406
+ }
407
+ }
408
+ const inv = section.inventory;
409
+ if (inv.state === "known") {
410
+ formatNameGroup(lines, "active", inv.active.filter(isValidWorkerDatabaseName));
411
+ formatNameGroup(lines, "left over", inv.leftovers.filter(isValidWorkerDatabaseName));
412
+ formatNameGroup(lines, "unverifiable", inv.unverifiable
413
+ .filter((e) => isValidWorkerDatabaseName(e.name))
414
+ .map((e) => `${e.name} (${RETAIN_REASONS.has(e.reason) ? e.reason : "unrecognized"})`));
415
+ if (inv.redacted_count > 0) {
416
+ lines.push(` ${inv.redacted_count} name(s) failed validation and are not shown`);
417
+ }
418
+ }
419
+ lines.push("These rows are advisory: they never change this command's exit status.");
420
+ return lines;
421
+ }
83
422
  /**
84
423
  * The CLI-only verbs that call the Bridge API from a shell-spawned process and
85
424
  * therefore resolve credentials themselves (CLAUDE.md "Shell-Spawned Credential
@@ -103,6 +442,8 @@ export const CREDENTIALED_RECOVERY_COMMANDS = [
103
442
  // BAPI-1300: parked-child continuation and its read-only receipt lookup.
104
443
  "continue-child",
105
444
  "continuation-receipt",
445
+ // BAPI-1469: the read-only bounded wait on a run's state.
446
+ "wait-run",
106
447
  ];
107
448
  /** Remediation for recovery/repair verbs that cannot resolve Bridge credentials. */
108
449
  export const REPAIR_CREDENTIAL_REMEDIATION = "export BAPI_API_KEY in the shell that runs `conductor`, or store a credential for this " +
@@ -439,7 +780,13 @@ export function inspectMcpProfile(env) {
439
780
  * `gh auth status` (no writes, no merge) and reports whether the host could merge
440
781
  * a PR locally if `policy_json.local_merge.enabled` were set. Never throws.
441
782
  */
442
- export function inspectLocalMerge(runCommand, env = process.env) {
783
+ export function inspectLocalMerge(runCommand, env = process.env,
784
+ /**
785
+ * BAPI-1461: the stored-entry finding, collected beforehand by
786
+ * {@link collectStoredMergeCredentialEvidence}. Absent ⇒ env-only, exactly as
787
+ * before. Never consulted for machine-user mode.
788
+ */
789
+ stored) {
443
790
  const run = runCommand ??
444
791
  ((cmd, args) => {
445
792
  try {
@@ -481,16 +828,62 @@ export function inspectLocalMerge(runCommand, env = process.env) {
481
828
  // read-only from the environment — no subprocess, no network, and no credential
482
829
  // VALUE anywhere in the result. Either mode's key satisfies it: which one a run
483
830
  // uses is the server-resolved typed mode, which a local doctor cannot know.
484
- const merge_credential_configured = ["BAPI_LOCAL_MERGE_TOKEN", "BAPI_MACHINE_MERGE_TOKEN"].some((key) => typeof env[key] === "string" && env[key].trim().length > 0);
831
+ //
832
+ // BAPI-1452: presence is now decided by the SAME resolver the merge uses, so
833
+ // the canonical-then-alias precedence and the "an invalid canonical value is
834
+ // not rescued by the alias" rule cannot drift between doctor and the merge.
835
+ const localResolution = resolveLocalMergeCredentialDetailed(env, "local");
836
+ const machineResolution = resolveLocalMergeCredentialDetailed(env, "machine_token");
837
+ // BAPI-1461: a stored token counts only when the environment set no local
838
+ // value at all, and only when GitHub did not reject it.
839
+ const storedConfigured = localResolution.status === "missing" && stored?.status === "configured";
840
+ const storedRejected = localResolution.status === "missing" && stored?.status === "rejected";
841
+ const storedUnverified = localResolution.status === "missing" && stored?.status === "unverified";
842
+ const merge_credential_configured = localResolution.status === "resolved" || storedConfigured || machineResolution.status === "resolved";
843
+ const localInvalid = localResolution.status === "invalid";
844
+ const local_credential_source = localResolution.status === "resolved"
845
+ ? localResolution.via_alias
846
+ ? "alias"
847
+ : "env"
848
+ : storedConfigured
849
+ ? "store"
850
+ : undefined;
851
+ const notes = [];
852
+ if (localResolution.status === "resolved" && localResolution.via_alias) {
853
+ notes.push(LOCAL_MERGE_ALIAS_DEPRECATION_NOTE);
854
+ }
485
855
  const warnings = [];
486
856
  if (!gh_available) {
487
857
  warnings.push("`gh` is not installed or not on PATH. Local merge (policy_json.local_merge.enabled) " +
488
858
  "cannot run; install the GitHub CLI to enable conductor-driven merges.");
489
859
  }
860
+ else if (!merge_credential_configured && storedRejected) {
861
+ warnings.push("the merge token stored for this repository was rejected by GitHub (expired, revoked, or " +
862
+ `without write access), so it is not configured. Fix: ${STORED_MERGE_TOKEN_REJECTED_REMEDIATION}`);
863
+ }
864
+ else if (!merge_credential_configured && storedUnverified) {
865
+ warnings.push(`no merge credential could be confirmed: ${STORED_MERGE_TOKEN_UNVERIFIED_REMEDIATION}`);
866
+ }
490
867
  else if (!merge_credential_configured) {
491
- warnings.push("`gh` is installed but no merge credential is configured. Set BAPI_LOCAL_MERGE_TOKEN " +
492
- "(or BAPI_MACHINE_MERGE_TOKEN for machine-user mode); local merge has no ambient-login " +
493
- "fallback and fails closed with a named reason.");
868
+ warnings.push(localInvalid
869
+ ? "`gh` is installed but the configured GITHUB_MERGE_TOKEN is not a usable token value " +
870
+ "(it contains whitespace or control characters), and BAPI_LOCAL_MERGE_TOKEN is not " +
871
+ "used in its place. Fix GITHUB_MERGE_TOKEN (or set BAPI_MACHINE_MERGE_TOKEN for " +
872
+ "machine-user mode)."
873
+ : "`gh` is installed but no merge credential is configured. Set GITHUB_MERGE_TOKEN " +
874
+ "(or BAPI_MACHINE_MERGE_TOKEN for machine-user mode); local merge has no ambient-login " +
875
+ "fallback and fails closed with a named reason.");
876
+ }
877
+ else if (storedRejected) {
878
+ // BAPI-1461: a machine credential must not conceal a rejected local one.
879
+ warnings.push("the merge token stored for this repository was rejected by GitHub, so a local-mode merge " +
880
+ `would refuse. Fix: ${STORED_MERGE_TOKEN_REJECTED_REMEDIATION}`);
881
+ }
882
+ else if (localInvalid) {
883
+ // A machine credential satisfies the check, but the local one is still
884
+ // broken — worth saying, because local mode would refuse.
885
+ warnings.push("the configured GITHUB_MERGE_TOKEN is not a usable token value, so a local-mode merge " +
886
+ "would refuse; BAPI_LOCAL_MERGE_TOKEN is not used in its place.");
494
887
  }
495
888
  // Capability gap only — local merge is opt-in, so an unconfigured host is not a
496
889
  // hard failure unless the operator enabled it. Reported as degraded for visibility.
@@ -537,18 +930,52 @@ export function inspectLocalMerge(runCommand, env = process.env) {
537
930
  id: "merge-credential",
538
931
  label: "Configured merge credential",
539
932
  status: "pass",
540
- detail: "BAPI_LOCAL_MERGE_TOKEN or BAPI_MACHINE_MERGE_TOKEN is set",
933
+ detail: localResolution.status === "resolved"
934
+ ? localResolution.via_alias
935
+ ? "the local credential is set through the deprecated BAPI_LOCAL_MERGE_TOKEN"
936
+ : "GITHUB_MERGE_TOKEN is set"
937
+ : storedConfigured
938
+ ? "a merge token is stored for this repository"
939
+ : "BAPI_MACHINE_MERGE_TOKEN is set",
541
940
  }
542
- : {
543
- id: "merge-credential",
544
- label: "Configured merge credential",
545
- status: "warn",
546
- detail: "`gh` is installed but neither BAPI_LOCAL_MERGE_TOKEN nor " +
547
- "BAPI_MACHINE_MERGE_TOKEN is set, so a local merge would fail closed",
548
- remediation: GH_AUTH_REMEDIATION,
549
- },
941
+ : storedRejected
942
+ ? {
943
+ id: "merge-credential",
944
+ label: "Configured merge credential",
945
+ status: "warn",
946
+ detail: "the merge token stored for this repository was rejected by GitHub",
947
+ remediation: STORED_MERGE_TOKEN_REJECTED_REMEDIATION,
948
+ }
949
+ : storedUnverified
950
+ ? {
951
+ id: "merge-credential",
952
+ label: "Configured merge credential",
953
+ status: "warn",
954
+ detail: "the stored merge token could not be checked right now",
955
+ remediation: STORED_MERGE_TOKEN_UNVERIFIED_REMEDIATION,
956
+ }
957
+ : {
958
+ id: "merge-credential",
959
+ label: "Configured merge credential",
960
+ status: "warn",
961
+ detail: localInvalid
962
+ ? "`gh` is installed but GITHUB_MERGE_TOKEN is not a usable token value and " +
963
+ "BAPI_MACHINE_MERGE_TOKEN is not set, so a local merge would fail closed"
964
+ : "`gh` is installed but neither GITHUB_MERGE_TOKEN nor " +
965
+ "BAPI_MACHINE_MERGE_TOKEN is set, so a local merge would fail closed",
966
+ remediation: GH_AUTH_REMEDIATION,
967
+ },
550
968
  ];
551
- return { gh_available, merge_credential_configured, degraded, warnings, outcomes };
969
+ return {
970
+ gh_available,
971
+ merge_credential_configured,
972
+ degraded,
973
+ warnings,
974
+ outcomes,
975
+ notes,
976
+ ...(local_credential_source !== undefined ? { local_credential_source } : {}),
977
+ ...(stored !== undefined ? { stored_credential: stored } : {}),
978
+ };
552
979
  }
553
980
  /** Remediation when this host has no `gh` identity to observe with. */
554
981
  export const GH_ACTIONS_MERGE_AUTH_REMEDIATION = "run `gh auth login` so this host has a GitHub identity; without one it cannot " +
@@ -698,17 +1125,25 @@ export async function buildConductorDoctorReport(deps = {}) {
698
1125
  const command_drift = deps.inspectCommandDrift
699
1126
  ? await inspectCommandDriftSafe(deps.inspectCommandDrift)
700
1127
  : undefined;
1128
+ const worker_database = deps.inspectWorkerDatabase
1129
+ ? await collectWorkerDatabaseDoctorSection(deps.inspectWorkerDatabase, deps.workerDatabaseTimeoutMs ?? WORKER_DATABASE_DOCTOR_TIMEOUT_MS)
1130
+ : undefined;
701
1131
  return {
702
1132
  ledger,
703
1133
  git_hooks: inspectHooks(deps.hooksDeps),
704
1134
  mcp_profile,
705
- local_merge: inspectLocalMerge(deps.runCommand),
1135
+ // BAPI-1461: the SAME env the rest of the report reads, plus the stored-entry
1136
+ // finding when a store context was supplied.
1137
+ local_merge: inspectLocalMerge(deps.runCommand, deps.env ?? process.env, deps.storedMergeCredential
1138
+ ? await collectStoredMergeCredentialEvidence(deps.env ?? process.env, deps.storedMergeCredential)
1139
+ : undefined),
706
1140
  github_actions_merge: inspectGitHubActionsMergeSafe(deps),
707
1141
  native_ledger,
708
1142
  deny_enforcement,
709
1143
  claude_login,
710
1144
  repair_credentials: await inspectRepairVerbCredentials(deps.env ?? process.env, deps.resolveRepairAccess),
711
1145
  ...(command_drift ? { command_drift } : {}),
1146
+ ...(worker_database ? { worker_database } : {}),
712
1147
  };
713
1148
  }
714
1149
  /** Run the drift inspector, containing any throw as an unassessable `warn`. */
@@ -865,7 +1300,9 @@ export function formatConductorDoctorReport(report) {
865
1300
  : "[WARNING] no merge credential configured"
866
1301
  : "[WARNING] not installed";
867
1302
  lines.push(`gh available: ${local_merge.gh_available} ${ghTag}`);
868
- lines.push(`merge credential set: ${local_merge.merge_credential_configured}`);
1303
+ lines.push(`merge credential set: ${local_merge.merge_credential_configured}` +
1304
+ // BAPI-1461: say where a stored token came from (never its value).
1305
+ (local_merge.local_credential_source === "store" ? " (stored for this repository)" : ""));
869
1306
  lines.push(`degraded: ${local_merge.degraded}`);
870
1307
  // BAPI-1135 — the two observations render INDEPENDENTLY, each leading with its
871
1308
  // own decisive word, because they have two different fixes. Collapsing them
@@ -897,6 +1334,11 @@ export function formatConductorDoctorReport(report) {
897
1334
  for (const w of local_merge.warnings)
898
1335
  lines.push(` - ${w}`);
899
1336
  }
1337
+ if (local_merge.notes && local_merge.notes.length > 0) {
1338
+ lines.push("local merge notes:");
1339
+ for (const n of local_merge.notes)
1340
+ lines.push(` - ${n}`);
1341
+ }
900
1342
  if (report.repair_credentials) {
901
1343
  const repair = report.repair_credentials;
902
1344
  lines.push("");
@@ -965,6 +1407,12 @@ export function formatConductorDoctorReport(report) {
965
1407
  if (row.remediation)
966
1408
  lines.push(` → ${row.remediation}`);
967
1409
  }
1410
+ // BAPI-1479 — advisory worker-database rows, after every existing section so
1411
+ // the output of a report without them is byte-identical.
1412
+ if (report.worker_database) {
1413
+ lines.push("");
1414
+ lines.push(...formatWorkerDatabaseDoctorSection(report.worker_database));
1415
+ }
968
1416
  return lines.join("\n");
969
1417
  }
970
1418
  // ---------------------------------------------------------------------------