@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.
- package/README.md +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export const BRING_UP_RUNBOOK = "
|
|
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 = "
|
|
24
|
-
"
|
|
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}
|
|
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
|
|
29
|
-
`then retry (${BRING_UP_RUNBOOK}
|
|
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}
|
|
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}
|
|
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
|
|
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
|
-
|
|
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(
|
|
492
|
-
"
|
|
493
|
-
|
|
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:
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
"
|
|
548
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|