@bridge_gpt/mcp-server 0.2.18 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +1 -1
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +1 -0
- package/build/conductor/bridge-api-client.js +7 -7
- package/build/conductor/cli.js +65 -12
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/epic-reconcile.js +9 -1
- package/build/conductor/epic-runtime.js +403 -43
- package/build/conductor/epic-state.js +7 -0
- package/build/conductor/errors.js +115 -3
- package/build/conductor/event-accessors.js +28 -10
- package/build/conductor/merge-ledger.js +6 -4
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1884 -6917
- package/build/doctor.js +8 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +11222 -8544
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +25 -93
- package/build/start-tickets-prereqs.js +152 -1
- package/build/start-tickets.js +96 -158
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +8 -4
- package/public/css/main.min.css +39 -0
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +2 -1
|
@@ -61,6 +61,76 @@ export class ConductorStoreError extends Error {
|
|
|
61
61
|
this.name = "ConductorStoreError";
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Raised when the frozen v1 `conductor epic-tick` entrypoint is invoked
|
|
66
|
+
* (BAPI-534, Epic Conductor v2 slice 0). The v1 local reconciler/ledger tick is
|
|
67
|
+
* frozen: Epic Conductor v2 replaces it with a server-side reconciler plus the
|
|
68
|
+
* local executor (`mcp-server executor`). The message points operators at the v2
|
|
69
|
+
* path and is path-free / secret-free.
|
|
70
|
+
*/
|
|
71
|
+
export class ConductorEpicTickV1FrozenError extends Error {
|
|
72
|
+
constructor() {
|
|
73
|
+
super("The v1 `conductor epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). Epic " +
|
|
74
|
+
"Conductor v2 replaces the local epic tick with a server-side reconciler " +
|
|
75
|
+
"plus the local executor — run `mcp-server executor` instead.");
|
|
76
|
+
this.name = "ConductorEpicTickV1FrozenError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Raised (BAPI-527) when the worker-side ledger subprocess boundary cannot run.
|
|
81
|
+
*
|
|
82
|
+
* Worker-facing ledger operations (`check_messages`, `wait_for_done_gate` event
|
|
83
|
+
* emission) MUST execute through `conductor-bin.js` under the captured conductor
|
|
84
|
+
* Node (`CONDUCTOR_NODE_PATH`) so the worker Node never loads the `better-sqlite3`
|
|
85
|
+
* native binary. When that runtime is missing or invalid, this error makes the
|
|
86
|
+
* failure LOUD and structured rather than silently falling back to the worker
|
|
87
|
+
* Node. The carried `reason`/`envVar` are the ONLY diagnostic surface — both are
|
|
88
|
+
* fixed, non-secret, path-free tokens. No caught error, path, argv, stdout,
|
|
89
|
+
* stderr, or credential value is ever attached.
|
|
90
|
+
*/
|
|
91
|
+
export class ConductorLedgerSubprocessRuntimeError extends Error {
|
|
92
|
+
reason;
|
|
93
|
+
envVar;
|
|
94
|
+
constructor(reason, envVar) {
|
|
95
|
+
// Single-line, path-free, secret-free (the committed CLI-stderr invariant in
|
|
96
|
+
// security-regressions.test.ts requires it). Names only the reason token.
|
|
97
|
+
super(`Conductor worker ledger subprocess is unavailable (reason: ${reason}).`);
|
|
98
|
+
this.name = "ConductorLedgerSubprocessRuntimeError";
|
|
99
|
+
this.reason = reason;
|
|
100
|
+
this.envVar = envVar;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Structural guard for a native-module-load failure raised by the conductor
|
|
105
|
+
* store (BAPI-526). Detected structurally (by `name` + a valid `details` object)
|
|
106
|
+
* rather than by importing `ConductorNativeModuleLoadError` from `store.ts`, so
|
|
107
|
+
* `errors.ts` avoids a runtime import cycle with the store.
|
|
108
|
+
*/
|
|
109
|
+
export function isConductorNativeModuleLoadError(error) {
|
|
110
|
+
if (!(error instanceof Error))
|
|
111
|
+
return false;
|
|
112
|
+
if (error.name !== "ConductorNativeModuleLoadError")
|
|
113
|
+
return false;
|
|
114
|
+
const details = error.details;
|
|
115
|
+
if (!details || typeof details !== "object" || Array.isArray(details))
|
|
116
|
+
return false;
|
|
117
|
+
const d = details;
|
|
118
|
+
return (typeof d.node_version === "string" &&
|
|
119
|
+
typeof d.node_modules_abi === "string" &&
|
|
120
|
+
typeof d.module === "string");
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Copy ONLY the three allowlisted native-load detail fields. Never spreads
|
|
124
|
+
* arbitrary properties from the error, so a secret/path smuggled onto the error
|
|
125
|
+
* object cannot round-trip into the client envelope.
|
|
126
|
+
*/
|
|
127
|
+
function nativeModuleDetailsEnvelope(details) {
|
|
128
|
+
return {
|
|
129
|
+
node_version: details.node_version,
|
|
130
|
+
node_modules_abi: details.node_modules_abi,
|
|
131
|
+
module: details.module,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
64
134
|
/**
|
|
65
135
|
* Heuristically detect SQLite busy/locked failures so they can be surfaced as a
|
|
66
136
|
* retryable 503 rather than an opaque 500. Matches the `better-sqlite3` error
|
|
@@ -101,11 +171,37 @@ function isSqliteBusyError(error) {
|
|
|
101
171
|
* the raw error text, stack, and any secret material are discarded.
|
|
102
172
|
*/
|
|
103
173
|
export function toConductorErrorEnvelope(error) {
|
|
174
|
+
// v1 epic-tick freeze (BAPI-534): the frozen entrypoint maps to a stable,
|
|
175
|
+
// named 400 pointing operators at Epic Conductor v2 (server-side reconciler +
|
|
176
|
+
// local executor). Checked first — it is a fail-loud guard, not a runtime fault.
|
|
177
|
+
if (error instanceof ConductorEpicTickV1FrozenError) {
|
|
178
|
+
return {
|
|
179
|
+
error: "EPIC_TICK_V1_FROZEN",
|
|
180
|
+
status: 400,
|
|
181
|
+
message: error.message,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// Native-module LOAD failure (BAPI-526): `better-sqlite3` is present but fails
|
|
185
|
+
// to load for this Node runtime (ABI/dlopen mismatch). Checked BEFORE the
|
|
186
|
+
// generic persistence-unavailable branch below (the native-load error subclasses
|
|
187
|
+
// it) so an ABI/dlopen failure surfaces as a distinct, machine-readable kind
|
|
188
|
+
// carrying node_version/ABI — never masked as INTERNAL_ERROR or the
|
|
189
|
+
// undifferentiated PERSISTENCE_UNAVAILABLE. `details` is a fixed three-field
|
|
190
|
+
// allowlist copied explicitly, never spread from the error object.
|
|
191
|
+
if (isConductorNativeModuleLoadError(error)) {
|
|
192
|
+
return {
|
|
193
|
+
error: "LEDGER_NATIVE_MODULE_LOAD_FAILED",
|
|
194
|
+
status: 500,
|
|
195
|
+
message: "Conductor ledger native module failed to load for this Node runtime.",
|
|
196
|
+
details: nativeModuleDetailsEnvelope(error.details),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
104
199
|
// Optional-native-binding degradation (BAPI-451): the conductor `store.ts`
|
|
105
200
|
// throws ConductorPersistenceUnavailableError when `better-sqlite3` could not be
|
|
106
|
-
// loaded. Detected by name (not instanceof) to avoid an
|
|
107
|
-
// store.ts. Surfaced as a retryable-shaped 503 with a fixed,
|
|
108
|
-
// so the caller learns persistence is unavailable instead of
|
|
201
|
+
// loaded (or is not installed). Detected by name (not instanceof) to avoid an
|
|
202
|
+
// import cycle with store.ts. Surfaced as a retryable-shaped 503 with a fixed,
|
|
203
|
+
// secret-free message so the caller learns persistence is unavailable instead of
|
|
204
|
+
// getting an opaque 500.
|
|
109
205
|
if (error instanceof Error && error.name === "ConductorPersistenceUnavailableError") {
|
|
110
206
|
return {
|
|
111
207
|
error: "PERSISTENCE_UNAVAILABLE",
|
|
@@ -113,6 +209,22 @@ export function toConductorErrorEnvelope(error) {
|
|
|
113
209
|
message: "Conductor persistence is unavailable (the optional better-sqlite3 native module is not loaded). Conductor coordination features are disabled; core tools are unaffected.",
|
|
114
210
|
};
|
|
115
211
|
}
|
|
212
|
+
// Worker-ledger subprocess runtime failure (BAPI-527): CONDUCTOR_NODE_PATH /
|
|
213
|
+
// BAPI_CONDUCTOR_CLI_FILE is missing or invalid so the worker cannot reach the
|
|
214
|
+
// local ledger through the captured conductor Node. Surfaced as a retryable-
|
|
215
|
+
// shaped 503 with a fixed, secret-free message plus a small non-secret details
|
|
216
|
+
// bag (env_var + stable reason). NOTHING path/secret-bearing is echoed. This is
|
|
217
|
+
// distinct from LEDGER_NATIVE_MODULE_LOAD_FAILED / PERSISTENCE_UNAVAILABLE,
|
|
218
|
+
// which cover conductor-owned processes that still load better-sqlite3 directly.
|
|
219
|
+
if (error instanceof ConductorLedgerSubprocessRuntimeError) {
|
|
220
|
+
return {
|
|
221
|
+
error: "LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE",
|
|
222
|
+
status: 503,
|
|
223
|
+
message: "Conductor worker ledger subprocess is unavailable; the worker Node cannot reach the local ledger. " +
|
|
224
|
+
"The dispatched conductor Node runtime is missing or invalid — conductor worker coordination is degraded; core tools are unaffected.",
|
|
225
|
+
details: { env_var: error.envVar, reason: error.reason },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
116
228
|
if (error instanceof ConductorValidationError) {
|
|
117
229
|
// Validation messages are conductor-authored and name the offending field,
|
|
118
230
|
// never its value — but redact defensively so an adversarial/secret-bearing
|
|
@@ -161,6 +161,9 @@ const EVENT_PARSERS = {
|
|
|
161
161
|
"spec_review.passed": parseSpecReview,
|
|
162
162
|
"spec_review.changes_requested": parseSpecReview,
|
|
163
163
|
"parse.triggered": parseEmpty,
|
|
164
|
+
// BAPI-527: parse-after-merge terminal markers (details-less typed payload).
|
|
165
|
+
"parse.succeeded": parseEmpty,
|
|
166
|
+
"parse.failed": parseEmpty,
|
|
164
167
|
};
|
|
165
168
|
// ---------------------------------------------------------------------------
|
|
166
169
|
// Public accessors
|
|
@@ -193,20 +196,35 @@ export function getHeadSha(event) {
|
|
|
193
196
|
return normalizeEventHeadSha(details.head_sha);
|
|
194
197
|
}
|
|
195
198
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* `
|
|
201
|
-
*
|
|
199
|
+
* Read and normalize a PR number from the CANONICAL `event.data.details.pr_number`
|
|
200
|
+
* path only (BAPI-525). Returns a positive integer PR number or `null` for
|
|
201
|
+
* missing/non-object details or a malformed value. Fail-closed. Used by the
|
|
202
|
+
* dispatched-backstop's local-first PR binding resolver to bind a ticket's PR from
|
|
203
|
+
* ledger events (e.g. `gate.met`) that carry both `pr_number` and `head_sha`,
|
|
204
|
+
* before falling back to a `gh` lookup.
|
|
205
|
+
*/
|
|
206
|
+
export function getPrNumber(event) {
|
|
207
|
+
const details = getRawEventDetails(event);
|
|
208
|
+
if (!details)
|
|
209
|
+
return null;
|
|
210
|
+
return normalizePrNumber(details.pr_number);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Resolve the immutable {@link MergeActionIdentity} from a PR-bound `gate.met`
|
|
214
|
+
* event. The true merge identity is `(repo, pr_number, head_sha, gate_name)`;
|
|
215
|
+
* `worker_id` is attribution metadata only and is NOT required for identity
|
|
216
|
+
* extraction (BAPI-525 Change A). Returns `null` unless the event is a `gate.met`
|
|
217
|
+
* carrying a complete PR binding (repo, pr_number, head_sha, gate_name) — an
|
|
218
|
+
* incomplete PR binding remains fail-closed regardless of `worker_id`. A
|
|
219
|
+
* worker_id-less `gate.met` produced by the autonomous git-hook chain therefore
|
|
220
|
+
* yields the SAME non-null identity (and byte-identical `action_key`) as its
|
|
221
|
+
* worker-stamped twin. Preserves the historical `required_checks` dual path
|
|
222
|
+
* (top-level then nested `ci_check_status`) and never throws. Branch-name fields,
|
|
223
|
+
* if present, are ignored.
|
|
202
224
|
*/
|
|
203
225
|
export function getMergeIdentity(event) {
|
|
204
226
|
if (event.type !== "gate.met")
|
|
205
227
|
return null;
|
|
206
|
-
// Worker scope is required — a run-level gate.met never binds a specific worker.
|
|
207
|
-
if (typeof event.worker_id !== "string" || event.worker_id.trim().length === 0) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
228
|
const details = getEventDetails(event, "gate.met");
|
|
211
229
|
if (details === null)
|
|
212
230
|
return null;
|
|
@@ -25,10 +25,12 @@ import { emitConductorEvent, openReadonlyConductorDatabaseIfExists, } from "./st
|
|
|
25
25
|
// existing importers (`supervisor-merge.ts`, tests) keep a stable API surface.
|
|
26
26
|
export { buildGateIdentity, makeMergeActionKey };
|
|
27
27
|
/**
|
|
28
|
-
* Resolve the immutable merge identity from a `gate.met` event. Returns
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Resolve the immutable merge identity from a `gate.met` event. Returns a non-null
|
|
29
|
+
* identity for any complete PR-bound `gate.met` (repo, pr_number, head_sha,
|
|
30
|
+
* gate_name), regardless of worker attribution — `worker_id` is attribution
|
|
31
|
+
* metadata only and is not required for merge identity extraction (BAPI-525
|
|
32
|
+
* Change A). Incomplete PR bindings remain fail-closed and yield `null`.
|
|
33
|
+
* Branch-name fields, if present, are ignored.
|
|
32
34
|
*
|
|
33
35
|
* BAPI-493: canonical parsing now lives in the typed accessor layer — this is a
|
|
34
36
|
* thin compatibility wrapper over {@link getMergeIdentity} so `supervisor-merge.ts`
|
|
@@ -23,6 +23,11 @@ async function _fetchGateConfigDefault(access) {
|
|
|
23
23
|
}
|
|
24
24
|
import { resolvePrHeadBinding } from "./pr-discovery.js";
|
|
25
25
|
import { emitConductorEventIfNew } from "./producer-ledger.js";
|
|
26
|
+
// In-process ledger writer — the DEFAULT emit sink for conductor-OWNED paths
|
|
27
|
+
// (the CLI/supervisor/epic runtimes that legitimately own the better-sqlite3
|
|
28
|
+
// load). The worker `wait_for_done_gate` MCP path injects a CLI-subprocess emitter
|
|
29
|
+
// instead (BAPI-527) so the worker Node never loads the native binary.
|
|
30
|
+
import { emitConductorEvent as emitConductorEventInProcess } from "./store.js";
|
|
26
31
|
const PRODUCER_OBSERVED_VIA = "pr-ci-producer";
|
|
27
32
|
/** Bounded wait constants, consistent with the existing wait_for_event pattern. */
|
|
28
33
|
export const WAIT_FOR_GATE_TIMEOUT_MAX_MS = 120_000;
|
|
@@ -124,7 +129,15 @@ function defaultSleep(ms) {
|
|
|
124
129
|
* resolved exactly once by the caller.
|
|
125
130
|
*/
|
|
126
131
|
async function observeWithResolved(binding, access, gateConfig, deps) {
|
|
127
|
-
|
|
132
|
+
// The event-WRITE sink defaults to the in-process store; the worker gate path
|
|
133
|
+
// injects a CLI-subprocess emitter (BAPI-527). When a caller supplies its own
|
|
134
|
+
// `emitIfNew` (e.g. unit tests / conductor-owned callers), that fully overrides
|
|
135
|
+
// this and the injected write sink is not consulted.
|
|
136
|
+
const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEventInProcess;
|
|
137
|
+
const emitIfNew = deps.emitIfNew ??
|
|
138
|
+
((input, dimensions) => emitConductorEventIfNew(input, dimensions, {
|
|
139
|
+
emitEvent: emitConductorEventFn,
|
|
140
|
+
}));
|
|
128
141
|
const pollCi = deps.pollCi ?? pollCiChecksForCommit;
|
|
129
142
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
130
143
|
// Resolve run_id/worker_id for attribution. Env takes precedence; fall back
|
|
@@ -193,7 +206,9 @@ async function observeWithResolved(binding, access, gateConfig, deps) {
|
|
|
193
206
|
let reviewSnapshot = null;
|
|
194
207
|
try {
|
|
195
208
|
const reviewObservation = await observeReviewWithResolved(binding, access, gateConfig, {
|
|
196
|
-
|
|
209
|
+
// Reuse the resolved `emitIfNew` so review-state events emitted alongside CI
|
|
210
|
+
// route through the SAME (possibly CLI-subprocess) write sink (BAPI-527).
|
|
211
|
+
emitIfNew,
|
|
197
212
|
env: deps.env,
|
|
198
213
|
});
|
|
199
214
|
reviewSnapshot = reviewObservation.snapshot;
|
|
@@ -37,7 +37,7 @@ export function makeStableProducerEventId(dedupeKey) {
|
|
|
37
37
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
38
38
|
}
|
|
39
39
|
/** Heuristically detect a SQLite duplicate-id / UNIQUE constraint failure. */
|
|
40
|
-
function isDuplicateConstraintError(error) {
|
|
40
|
+
export function isDuplicateConstraintError(error) {
|
|
41
41
|
if (!error || typeof error !== "object")
|
|
42
42
|
return false;
|
|
43
43
|
const code = error.code;
|
package/build/conductor/store.js
CHANGED
|
@@ -38,6 +38,68 @@ export class ConductorPersistenceUnavailableError extends Error {
|
|
|
38
38
|
this.name = "ConductorPersistenceUnavailableError";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/** The native module whose load status this store owns. */
|
|
42
|
+
const LEDGER_NATIVE_MODULE_NAME = "better-sqlite3";
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when `better-sqlite3` is present but fails to LOAD for this Node runtime
|
|
45
|
+
* (ABI/`NODE_MODULE_VERSION` mismatch or a generic `dlopen` failure) — distinct
|
|
46
|
+
* from "module simply not installed", which stays on the base
|
|
47
|
+
* {@link ConductorPersistenceUnavailableError} (503) path. Subclassing the base
|
|
48
|
+
* keeps existing `instanceof`/name-based degradation working while carrying the
|
|
49
|
+
* structured, secret-free {@link ConductorNativeModuleLoadDetails}. The raw
|
|
50
|
+
* caught error, stack, path, and message are deliberately NOT stored on the
|
|
51
|
+
* public instance.
|
|
52
|
+
*/
|
|
53
|
+
export class ConductorNativeModuleLoadError extends ConductorPersistenceUnavailableError {
|
|
54
|
+
details;
|
|
55
|
+
failureKind;
|
|
56
|
+
constructor(failureKind, details) {
|
|
57
|
+
super("Conductor ledger native module failed to load for this Node runtime.");
|
|
58
|
+
this.name = "ConductorNativeModuleLoadError";
|
|
59
|
+
this.failureKind = failureKind;
|
|
60
|
+
this.details = details;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Build the allowlisted runtime details for the CURRENT process. */
|
|
64
|
+
export function buildNativeModuleLoadDetails() {
|
|
65
|
+
return {
|
|
66
|
+
node_version: process.version,
|
|
67
|
+
node_modules_abi: process.versions.modules ?? "unknown",
|
|
68
|
+
module: LEDGER_NATIVE_MODULE_NAME,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Classify a caught `better-sqlite3` load failure. Inspects ONLY the error `code`
|
|
73
|
+
* and `message` locally; the raw text never leaves this helper.
|
|
74
|
+
*
|
|
75
|
+
* - `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` -> base
|
|
76
|
+
* {@link ConductorPersistenceUnavailableError} ("not installed", 503).
|
|
77
|
+
* - `NODE_MODULE_VERSION` / "compiled against a different Node.js version" /
|
|
78
|
+
* "module version mismatch" -> native-load `abi_mismatch`.
|
|
79
|
+
* - `ERR_DLOPEN_FAILED` / generic `dlopen` -> native-load `dlopen_failed`.
|
|
80
|
+
* - Anything else -> base {@link ConductorPersistenceUnavailableError}.
|
|
81
|
+
*/
|
|
82
|
+
export function classifyDatabaseModuleLoadFailure(error) {
|
|
83
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
84
|
+
const rawMessage = error && typeof error === "object" && typeof error.message === "string"
|
|
85
|
+
? (error.message)
|
|
86
|
+
: "";
|
|
87
|
+
const lowered = rawMessage.toLowerCase();
|
|
88
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
89
|
+
return new ConductorPersistenceUnavailableError();
|
|
90
|
+
}
|
|
91
|
+
const isAbiMismatch = lowered.includes("node_module_version") ||
|
|
92
|
+
lowered.includes("compiled against a different node.js version") ||
|
|
93
|
+
lowered.includes("module version mismatch");
|
|
94
|
+
if (isAbiMismatch) {
|
|
95
|
+
return new ConductorNativeModuleLoadError("abi_mismatch", buildNativeModuleLoadDetails());
|
|
96
|
+
}
|
|
97
|
+
const isDlopen = code === "ERR_DLOPEN_FAILED" || lowered.includes("dlopen");
|
|
98
|
+
if (isDlopen) {
|
|
99
|
+
return new ConductorNativeModuleLoadError("dlopen_failed", buildNativeModuleLoadDetails());
|
|
100
|
+
}
|
|
101
|
+
return new ConductorPersistenceUnavailableError();
|
|
102
|
+
}
|
|
41
103
|
/**
|
|
42
104
|
* Cached, concurrency-safe async loader for the optional `better-sqlite3` module.
|
|
43
105
|
*
|
|
@@ -50,13 +112,21 @@ export class ConductorPersistenceUnavailableError extends Error {
|
|
|
50
112
|
* warning is emitted exactly once and every subsequent call throws
|
|
51
113
|
* {@link ConductorPersistenceUnavailableError} immediately, without re-attempting the
|
|
52
114
|
* failing import on every Conductor operation.
|
|
115
|
+
*
|
|
116
|
+
* BAPI-526: the caught error is CLASSIFIED (not swallowed) into a TYPED failure —
|
|
117
|
+
* ABI/dlopen mismatches become {@link ConductorNativeModuleLoadError} carrying
|
|
118
|
+
* `node_version`/`node_modules_abi`, while "module not installed" stays the base
|
|
119
|
+
* {@link ConductorPersistenceUnavailableError}. That typed failure is cached and
|
|
120
|
+
* re-thrown on every later call so the classification is never lost.
|
|
53
121
|
*/
|
|
54
122
|
let databaseModulePromise = null;
|
|
55
|
-
let
|
|
123
|
+
let dbLoadFailure = null;
|
|
124
|
+
let dbLoadDiagnosticEmitted = false;
|
|
56
125
|
async function getDatabaseModule() {
|
|
57
|
-
// Cached
|
|
58
|
-
|
|
59
|
-
|
|
126
|
+
// Cached typed failure: never re-attempt a known-failing native import, and
|
|
127
|
+
// preserve the ABI/dlopen classification across every subsequent call.
|
|
128
|
+
if (dbLoadFailure) {
|
|
129
|
+
throw dbLoadFailure;
|
|
60
130
|
}
|
|
61
131
|
if (databaseModulePromise)
|
|
62
132
|
return databaseModulePromise;
|
|
@@ -67,13 +137,25 @@ async function getDatabaseModule() {
|
|
|
67
137
|
const ctor = (mod.default ?? mod);
|
|
68
138
|
return ctor;
|
|
69
139
|
}
|
|
70
|
-
catch {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
catch (err) {
|
|
141
|
+
const classified = classifyDatabaseModuleLoadFailure(err);
|
|
142
|
+
dbLoadFailure = classified;
|
|
143
|
+
// Drop the rejected promise so the typed `dbLoadFailure` above is the sole
|
|
144
|
+
// cached state (the rejected promise itself must not be the only record).
|
|
145
|
+
databaseModulePromise = null;
|
|
146
|
+
if (!dbLoadDiagnosticEmitted) {
|
|
147
|
+
dbLoadDiagnosticEmitted = true;
|
|
148
|
+
// Fixed, path-free, secret-free diagnostic: module name, current Node
|
|
149
|
+
// version, current ABI, and coarse failure kind ONLY. Never the raw
|
|
150
|
+
// dlopen path, stack, or caught message.
|
|
151
|
+
const kind = classified instanceof ConductorNativeModuleLoadError
|
|
152
|
+
? classified.failureKind
|
|
153
|
+
: "unavailable";
|
|
154
|
+
console.error(`[conductor] ${LEDGER_NATIVE_MODULE_NAME} native module could not be loaded ` +
|
|
155
|
+
`(node=${process.version} abi=${process.versions.modules ?? "unknown"} kind=${kind}). ` +
|
|
74
156
|
"Conductor coordination features are disabled for this session; core MCP tools are unaffected.");
|
|
75
157
|
}
|
|
76
|
-
throw
|
|
158
|
+
throw classified;
|
|
77
159
|
}
|
|
78
160
|
})();
|
|
79
161
|
return databaseModulePromise;
|
|
@@ -183,11 +265,15 @@ export function resolveConductorStoreConfig(env = process.env) {
|
|
|
183
265
|
* CHECK vocabulary. Bumped to 7 because the durable parse-after-merge fold adds
|
|
184
266
|
* the `parse.triggered` event type to the same `events.type` CHECK vocabulary
|
|
185
267
|
* (replacing the in-memory parse-wait map so stateless epic-tick invocations can
|
|
186
|
-
* fold a merged ticket to `done`).
|
|
187
|
-
*
|
|
188
|
-
* the
|
|
268
|
+
* fold a merged ticket to `done`). Bumped to 8 in BAPI-527 because the durable
|
|
269
|
+
* parse-after-merge TERMINAL fold adds the `parse.succeeded` and `parse.failed`
|
|
270
|
+
* event types to the same `events.type` CHECK vocabulary (so the stateless
|
|
271
|
+
* epic-tick can fold a merged ticket to `done` on terminal parse success and
|
|
272
|
+
* `blocked` on permanent parse failure directly from the ledger). Older ledgers
|
|
273
|
+
* stamped at a lower version are rebuilt by {@link migrateConductorSchemaIfNeeded}
|
|
274
|
+
* so their CHECK clause accepts the current taxonomy.
|
|
189
275
|
*/
|
|
190
|
-
export const CURRENT_CONDUCTOR_SCHEMA_VERSION =
|
|
276
|
+
export const CURRENT_CONDUCTOR_SCHEMA_VERSION = 8;
|
|
191
277
|
/** Render the taxonomy `CHECK (type IN (...))` clause from the single source of truth. */
|
|
192
278
|
function buildTypeCheckClause() {
|
|
193
279
|
const list = SEMANTIC_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
@@ -327,15 +413,15 @@ export function migrateConductorSchemaIfNeeded(db) {
|
|
|
327
413
|
return;
|
|
328
414
|
}
|
|
329
415
|
const migrate = db.transaction(() => {
|
|
330
|
-
// Scratch table named for the current schema version (
|
|
331
|
-
// migration rebuild never reads as a stale pre-
|
|
332
|
-
db.exec(buildEventsTableSql("
|
|
333
|
-
db.exec(`INSERT INTO
|
|
416
|
+
// Scratch table named for the current schema version (v8, BAPI-527) so the
|
|
417
|
+
// migration rebuild never reads as a stale pre-parse-terminal-taxonomy artifact.
|
|
418
|
+
db.exec(buildEventsTableSql("events_migrated_v8"));
|
|
419
|
+
db.exec(`INSERT INTO events_migrated_v8
|
|
334
420
|
(seq, id, source, type, subject, run_id, worker_id, producer, schema_version, time, data_json, confidence, observed_via, created_at)
|
|
335
421
|
SELECT seq, id, source, type, subject, run_id, worker_id, producer, schema_version, time, data_json, confidence, observed_via, created_at
|
|
336
422
|
FROM events`);
|
|
337
423
|
db.exec("DROP TABLE events");
|
|
338
|
-
db.exec("ALTER TABLE
|
|
424
|
+
db.exec("ALTER TABLE events_migrated_v8 RENAME TO events");
|
|
339
425
|
db.exec(buildEventsIndexesSql());
|
|
340
426
|
// Ensure auxiliary tables and their relay indexes exist on older ledgers too.
|
|
341
427
|
db.exec(buildAuxiliaryTablesSql());
|
|
@@ -818,6 +904,56 @@ export async function upsertSupervisorProjection(input, config = resolveConducto
|
|
|
818
904
|
* and network-home degradation. Never creates the DB, inserts a diagnostic
|
|
819
905
|
* event, or mutates WAL/schema state.
|
|
820
906
|
*/
|
|
907
|
+
/**
|
|
908
|
+
* Read-only probe of whether the ledger native binding actually loads for this
|
|
909
|
+
* Node runtime (BAPI-526). Reuses the lazy loader so it reflects the REAL load
|
|
910
|
+
* outcome, and NEVER opens the SQLite database, runs migrations, creates files,
|
|
911
|
+
* or emits events. On success reports `loads: true`; on a native-load failure
|
|
912
|
+
* reports the coarse `failure_kind` and marks the host degraded. All returned
|
|
913
|
+
* fields are secret-free and path-free.
|
|
914
|
+
*/
|
|
915
|
+
export async function inspectConductorLedgerNativeModuleLoad() {
|
|
916
|
+
const details = buildNativeModuleLoadDetails();
|
|
917
|
+
try {
|
|
918
|
+
await getDatabaseModule();
|
|
919
|
+
return {
|
|
920
|
+
module: details.module,
|
|
921
|
+
node_version: details.node_version,
|
|
922
|
+
node_modules_abi: details.node_modules_abi,
|
|
923
|
+
loads: true,
|
|
924
|
+
failure_kind: "none",
|
|
925
|
+
degraded: false,
|
|
926
|
+
warning: null,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
if (err instanceof ConductorNativeModuleLoadError) {
|
|
931
|
+
return {
|
|
932
|
+
...err.details,
|
|
933
|
+
loads: false,
|
|
934
|
+
failure_kind: err.failureKind,
|
|
935
|
+
degraded: true,
|
|
936
|
+
warning: "better-sqlite3 failed to load for this Node runtime; conductor ledger operations are degraded.",
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
if (err instanceof ConductorPersistenceUnavailableError) {
|
|
940
|
+
return {
|
|
941
|
+
...details,
|
|
942
|
+
loads: false,
|
|
943
|
+
failure_kind: "module_not_found",
|
|
944
|
+
degraded: true,
|
|
945
|
+
warning: "better-sqlite3 is not installed; conductor ledger operations are unavailable.",
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
...details,
|
|
950
|
+
loads: false,
|
|
951
|
+
failure_kind: "unknown",
|
|
952
|
+
degraded: true,
|
|
953
|
+
warning: "better-sqlite3 could not be loaded; conductor ledger operations are degraded.",
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
}
|
|
821
957
|
export async function doctorConductorLedger(config = resolveConductorStoreConfig()) {
|
|
822
958
|
const health = getConductorPathHealth();
|
|
823
959
|
const network = detectNetworkMountedHome();
|
|
@@ -825,6 +961,12 @@ export async function doctorConductorLedger(config = resolveConductorStoreConfig
|
|
|
825
961
|
if (network.network_mounted && network.warning) {
|
|
826
962
|
warnings.push(network.warning);
|
|
827
963
|
}
|
|
964
|
+
// Read-only native-binding load probe (BAPI-526). Reflects the real lazy-loader
|
|
965
|
+
// outcome; never opens/creates/migrates the ledger.
|
|
966
|
+
const native_load = await inspectConductorLedgerNativeModuleLoad();
|
|
967
|
+
if (native_load.degraded && native_load.warning) {
|
|
968
|
+
warnings.push(native_load.warning);
|
|
969
|
+
}
|
|
828
970
|
let schemaPresent = false;
|
|
829
971
|
let journalMode = null;
|
|
830
972
|
let eventCount = null;
|
|
@@ -873,6 +1015,7 @@ export async function doctorConductorLedger(config = resolveConductorStoreConfig
|
|
|
873
1015
|
message_relay: {
|
|
874
1016
|
message_cooldown_ms: config.message_cooldown_ms,
|
|
875
1017
|
},
|
|
1018
|
+
native_load,
|
|
876
1019
|
degraded: network.network_mounted,
|
|
877
1020
|
warnings,
|
|
878
1021
|
};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Supervisor merge pipeline wrapper (Conductor C6, BAPI-398).
|
|
3
3
|
*
|
|
4
|
-
* Processes an eligible
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Processes an eligible PR-bound `gate.met` event by calling the API-owned merge
|
|
5
|
+
* decision/execution endpoint and recording the returned `merge.*` ledger events
|
|
6
|
+
* into the LOCAL conductor ledger. The merge decision depends ONLY on the PR/gate
|
|
7
|
+
* identity `(repo, pr_number, head_sha, gate)` — `worker_id` is attribution
|
|
8
|
+
* metadata that is stamped onto the emitted `merge.*` events for audit but never
|
|
9
|
+
* gates the merge (BAPI-525 Change A). When the source `gate.met` lacks a
|
|
10
|
+
* `worker_id` (e.g. the autonomous git-hook chain), attribution is resolved from
|
|
11
|
+
* durable dispatch state via an optional `resolveWorkerIdForGateEvent` seam. This
|
|
12
|
+
* module:
|
|
7
13
|
*
|
|
8
14
|
* - never performs a provider merge locally and never handles VCS write
|
|
9
15
|
* credentials (no `gh`, no GITHUB_TOKEN, no installation token, no shell-out),
|
|
@@ -52,6 +58,24 @@ function mapApiErrorReason(error) {
|
|
|
52
58
|
}
|
|
53
59
|
return "api_network";
|
|
54
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the worker attribution stamped onto emitted `merge.*` ledger events for
|
|
63
|
+
* a `gate.met`. Precedence: (1) a non-empty `event.worker_id`, (2) the durable
|
|
64
|
+
* attribution seam, (3) `null`. Attribution is audit-only and is never required
|
|
65
|
+
* for the merge decision (BAPI-525 Change A).
|
|
66
|
+
*/
|
|
67
|
+
function resolveAttributionWorkerId(event, identity, resolve) {
|
|
68
|
+
if (typeof event.worker_id === "string" && event.worker_id.trim().length > 0) {
|
|
69
|
+
return event.worker_id.trim();
|
|
70
|
+
}
|
|
71
|
+
if (resolve) {
|
|
72
|
+
const resolved = resolve(event, identity);
|
|
73
|
+
if (typeof resolved === "string" && resolved.trim().length > 0) {
|
|
74
|
+
return resolved.trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
55
79
|
/**
|
|
56
80
|
* Process an eligible `gate.met` event end-to-end: terminal-success short-circuit,
|
|
57
81
|
* API merge call, ordered ledger recording, and sanitized error handling. Never
|
|
@@ -68,6 +92,9 @@ export async function processGateMetMerge(access, event, deps = {}) {
|
|
|
68
92
|
return { processed: false, reason: "ineligible" };
|
|
69
93
|
}
|
|
70
94
|
const actionKey = identity.action_key;
|
|
95
|
+
// Attribution only — resolved from the event or durable dispatch state, never a
|
|
96
|
+
// merge gate (BAPI-525 Change A). A null value is tolerated end-to-end.
|
|
97
|
+
const attributionWorkerId = resolveAttributionWorkerId(event, identity, deps.resolveWorkerIdForGateEvent);
|
|
71
98
|
// Terminal-success short-circuit BEFORE any API call (idempotent across restart).
|
|
72
99
|
if (await checkTerminal(actionKey)) {
|
|
73
100
|
return { processed: false, reason: "already_succeeded" };
|
|
@@ -92,7 +119,7 @@ export async function processGateMetMerge(access, event, deps = {}) {
|
|
|
92
119
|
reason,
|
|
93
120
|
details: baseDetails,
|
|
94
121
|
run_id: event.run_id ?? null,
|
|
95
|
-
worker_id:
|
|
122
|
+
worker_id: attributionWorkerId,
|
|
96
123
|
summary: `merge.failed ${reason}`,
|
|
97
124
|
}, { emitEvent: deps.emitEvent });
|
|
98
125
|
return { processed: true, outcome: "api_error", reason };
|
|
@@ -119,7 +146,7 @@ export async function processGateMetMerge(access, event, deps = {}) {
|
|
|
119
146
|
reason: ledgerEvent.reason ?? null,
|
|
120
147
|
details: ledgerEvent.details ?? baseDetails,
|
|
121
148
|
run_id: event.run_id ?? null,
|
|
122
|
-
worker_id:
|
|
149
|
+
worker_id: attributionWorkerId,
|
|
123
150
|
}, { emitEvent: deps.emitEvent });
|
|
124
151
|
emitted.push({ type: ledgerEvent.type, emitted: result.emitted });
|
|
125
152
|
}
|
|
@@ -51,6 +51,14 @@ export const SEMANTIC_EVENT_TYPES = [
|
|
|
51
51
|
// post-merge repository re-index, so the (stateless) reconcile loop can fold a
|
|
52
52
|
// merged ticket to `done` from the ledger instead of an in-memory wait map.
|
|
53
53
|
"parse.triggered",
|
|
54
|
+
// BAPI-527: durable parse-after-merge TERMINAL markers. Emitted by epic-tick
|
|
55
|
+
// once the durable `/jira/parse-status` surface reports a terminal outcome for a
|
|
56
|
+
// triggered post-merge parse: `parse.succeeded` lets the merge.succeeded signal
|
|
57
|
+
// proceed to `done`; `parse.failed` folds the ticket to `blocked` (and escalates).
|
|
58
|
+
// Payloads are compact (summary/status/reason/details with epic_key + ticket_key
|
|
59
|
+
// + bounded parse error metadata) — never subprocess stdout/stderr/stacks/commands.
|
|
60
|
+
"parse.succeeded",
|
|
61
|
+
"parse.failed",
|
|
54
62
|
];
|
|
55
63
|
/**
|
|
56
64
|
* Type guard: returns `true` only when `value` is one of the exact taxonomy
|