@bridge_gpt/mcp-server 0.2.16 → 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.
Files changed (103) hide show
  1. package/CONDUCTOR.md +75 -0
  2. package/README.md +2 -2
  3. package/build/agent-capabilities/probe-context.js +13 -3
  4. package/build/agent-capabilities/probes.js +262 -11
  5. package/build/agent-capabilities/reporter.js +1 -0
  6. package/build/agents.generated.js +3 -3
  7. package/build/backend-warnings.js +44 -0
  8. package/build/claude-settings.js +129 -0
  9. package/build/commands.generated.js +7 -6
  10. package/build/conductor/bridge-api-client.js +198 -18
  11. package/build/conductor/claude-hook.js +22 -4
  12. package/build/conductor/cli.js +76 -25
  13. package/build/conductor/deny-enforcement-preflight.js +96 -0
  14. package/build/conductor/doctor.js +183 -2
  15. package/build/conductor/done-gate.js +5 -0
  16. package/build/conductor/epic-reconcile.js +71 -14
  17. package/build/conductor/epic-runtime.js +839 -67
  18. package/build/conductor/epic-state.js +524 -63
  19. package/build/conductor/errors.js +156 -3
  20. package/build/conductor/event-accessors.js +252 -0
  21. package/build/conductor/file-scope-guard.js +201 -0
  22. package/build/conductor/github-mergeability.js +85 -0
  23. package/build/conductor/local-merge.js +47 -1
  24. package/build/conductor/merge-identity.js +41 -0
  25. package/build/conductor/merge-ledger.js +19 -72
  26. package/build/conductor/plan.js +12 -2
  27. package/build/conductor/pr-ci-producer.js +17 -2
  28. package/build/conductor/pr-discovery.js +11 -1
  29. package/build/conductor/producer-ledger.js +1 -1
  30. package/build/conductor/store.js +161 -18
  31. package/build/conductor/supervisor-config.js +4 -39
  32. package/build/conductor/supervisor-escalation.js +10 -26
  33. package/build/conductor/supervisor-ledger.js +5 -12
  34. package/build/conductor/supervisor-merge.js +32 -5
  35. package/build/conductor/supervisor-message-relay.js +2 -5
  36. package/build/conductor/supervisor-notification.js +1 -1
  37. package/build/conductor/supervisor-runtime.js +12 -54
  38. package/build/conductor/supervisor-state.js +4 -18
  39. package/build/conductor/supervisor-types.js +2 -2
  40. package/build/conductor/taxonomy.js +12 -0
  41. package/build/conductor/tools.js +28 -6
  42. package/build/conductor/worker-ledger-cli.js +244 -0
  43. package/build/conductor-bin.js +1800 -5166
  44. package/build/conductor-claude-hook-bin.js +4 -2
  45. package/build/doctor.js +40 -0
  46. package/build/executor/cli.js +229 -0
  47. package/build/executor/credentials.js +65 -0
  48. package/build/executor/deps.js +117 -0
  49. package/build/executor/env.js +79 -0
  50. package/build/executor/heartbeat.js +59 -0
  51. package/build/executor/http-client.js +131 -0
  52. package/build/executor/index.js +10 -0
  53. package/build/executor/job-errors.js +55 -0
  54. package/build/executor/job-log-registry.js +110 -0
  55. package/build/executor/job-runner.js +688 -0
  56. package/build/executor/job-types.js +60 -0
  57. package/build/executor/merge-job.js +155 -0
  58. package/build/executor/observation.js +123 -0
  59. package/build/executor/permissions.js +79 -0
  60. package/build/executor/preflight.js +144 -0
  61. package/build/executor/process.js +81 -0
  62. package/build/executor/prompt-spec.js +235 -0
  63. package/build/executor/results.js +134 -0
  64. package/build/executor/resume-pre-spawn.js +179 -0
  65. package/build/executor/runner.js +98 -0
  66. package/build/executor/terminal-mutation.js +34 -0
  67. package/build/executor/test-clock.js +109 -0
  68. package/build/executor/types.js +18 -0
  69. package/build/executor/verdict-artifact.js +53 -0
  70. package/build/executor/viewer-tabs.js +78 -0
  71. package/build/executor/watch-cli.js +113 -0
  72. package/build/executor/worker-command.js +106 -0
  73. package/build/executor/worker-finalization.js +97 -0
  74. package/build/executor/worker-log.js +92 -0
  75. package/build/executor/worktree-gc.js +134 -0
  76. package/build/executor/worktree-inspection.js +86 -0
  77. package/build/executor/worktree.js +103 -0
  78. package/build/index.js +13950 -9669
  79. package/build/install-bridge.js +25 -8
  80. package/build/install-doctor.js +387 -0
  81. package/build/mcp-invoke.js +19 -3
  82. package/build/mcp-provisioning.js +31 -25
  83. package/build/mcp-registration-doctor.js +27 -7
  84. package/build/mcp-server-invocation.js +152 -0
  85. package/build/pipelines.generated.js +31 -6
  86. package/build/readme.generated.js +1 -1
  87. package/build/regression-check.js +53 -1
  88. package/build/review-tickets.js +175 -21
  89. package/build/sfcc/reads-site-preference.js +52 -19
  90. package/build/start-tickets-conductor.js +47 -99
  91. package/build/start-tickets-prereqs.js +185 -4
  92. package/build/start-tickets.js +218 -180
  93. package/build/version.generated.js +1 -1
  94. package/build/visual-diff-worker.js +313 -0
  95. package/build/visual-diff.js +632 -0
  96. package/build/worktree-core.js +202 -0
  97. package/package.json +10 -6
  98. package/pipelines/review-ticket.json +24 -2
  99. package/public/css/main.min.css +3311 -1
  100. package/public/css/main.min.css.map +1 -1
  101. package/public/js/main.min.js +7924 -1
  102. package/public/js/main.min.js.map +1 -1
  103. package/smoke-test/SMOKE-TEST.md +5 -2
@@ -2,15 +2,10 @@
2
2
  * Supervisor configuration resolution (BAPI-396, conductor C4).
3
3
  *
4
4
  * Centralizes the supervisor's wake cadence, global timeout, per-state stall
5
- * thresholds, liveness thresholds, escalation cooldown, and LLM budget caps.
6
- * All values are resolved from `process.env` with conservative, bounded defaults
7
- * following the style of {@link resolveConductorStoreConfig} in `store.ts`:
8
- * malformed values fall back to the default and excessive values are clamped.
9
- *
10
- * Token discipline: the WAKE cadence (frequent, deterministic event polling) is
11
- * intentionally separate from the LLM cadence (judgment-triggered only). The
12
- * deterministic loop runs every `wake_interval_ms`; the LLM is consulted at most
13
- * `llm_max_calls` times per run and only for ambiguous escalation candidates.
5
+ * thresholds, liveness thresholds, and escalation cooldown. All values are
6
+ * resolved from `process.env` with conservative, bounded defaults following the
7
+ * style of {@link resolveConductorStoreConfig} in `store.ts`: malformed values
8
+ * fall back to the default and excessive values are clamped.
14
9
  */
15
10
  const MINUTE_MS = 60_000;
16
11
  const HOUR_MS = 60 * MINUTE_MS;
@@ -36,13 +31,6 @@ const LIVENESS_STALLED_AFTER_MAX_MS = 4 * HOUR_MS;
36
31
  const DEAD_AFTER_DEFAULT_MS = 2 * HOUR_MS;
37
32
  const DEAD_AFTER_MIN_MS = 10 * MINUTE_MS;
38
33
  const DEAD_AFTER_MAX_MS = 24 * HOUR_MS;
39
- // --- LLM budget. ----------------------------------------------------------
40
- const LLM_MAX_CALLS_DEFAULT = 10;
41
- const LLM_MAX_CALLS_MIN = 0;
42
- const LLM_MAX_CALLS_MAX = 1000;
43
- const LLM_TIMEOUT_DEFAULT_MS = 30_000;
44
- const LLM_TIMEOUT_MIN_MS = 1_000;
45
- const LLM_TIMEOUT_MAX_MS = 120_000;
46
34
  /**
47
35
  * Parse a bounded integer from an env-style string. Mirrors `parseBoundedInt`
48
36
  * in `store.ts`: blank/undefined/non-finite/float-with-fraction/non-numeric
@@ -68,19 +56,6 @@ export function parseBoundedSupervisorInt(raw, fallback, min, max) {
68
56
  const POLL_LIMIT_DEFAULT = 200;
69
57
  const POLL_LIMIT_MIN = 1;
70
58
  const POLL_LIMIT_MAX = 1000;
71
- /** Truthy parse for boolean-ish env strings (`0`/`false`/`off`/`no` => false). */
72
- function parseBoolEnv(raw, fallback) {
73
- if (raw === undefined)
74
- return fallback;
75
- const v = raw.trim().toLowerCase();
76
- if (v.length === 0)
77
- return fallback;
78
- if (v === "0" || v === "false" || v === "off" || v === "no")
79
- return false;
80
- if (v === "1" || v === "true" || v === "on" || v === "yes")
81
- return true;
82
- return fallback;
83
- }
84
59
  /**
85
60
  * Resolve the supervisor configuration. Precedence per field: a direct override
86
61
  * (when provided and valid) wins over the environment, which wins over the
@@ -96,13 +71,6 @@ export function resolveSupervisorConfig(overrides = {}, env = process.env) {
96
71
  const quiet_after_ms = parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_QUIET_AFTER_MS, QUIET_AFTER_DEFAULT_MS, QUIET_AFTER_MIN_MS, QUIET_AFTER_MAX_MS);
97
72
  const liveness_stalled_after_ms = parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LIVENESS_STALLED_AFTER_MS, LIVENESS_STALLED_AFTER_DEFAULT_MS, LIVENESS_STALLED_AFTER_MIN_MS, LIVENESS_STALLED_AFTER_MAX_MS);
98
73
  const dead_after_ms = parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_DEAD_AFTER_MS, DEAD_AFTER_DEFAULT_MS, DEAD_AFTER_MIN_MS, DEAD_AFTER_MAX_MS);
99
- // LLM is enabled by default but degrades cleanly. A direct `llm_enabled:false`
100
- // override (e.g. CLI `--no-llm`) forces deterministic-only mode.
101
- const llm_enabled = overrides.llm_enabled !== undefined
102
- ? overrides.llm_enabled
103
- : parseBoolEnv(env.BAPI_CONDUCTOR_LLM_ENABLED, true);
104
- const llm_max_calls = clampOverride(overrides.llm_max_calls, parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LLM_MAX_CALLS, LLM_MAX_CALLS_DEFAULT, LLM_MAX_CALLS_MIN, LLM_MAX_CALLS_MAX), LLM_MAX_CALLS_MIN, LLM_MAX_CALLS_MAX);
105
- const llm_timeout_ms = clampOverride(overrides.llm_timeout_ms, parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_LLM_TIMEOUT_MS, LLM_TIMEOUT_DEFAULT_MS, LLM_TIMEOUT_MIN_MS, LLM_TIMEOUT_MAX_MS), LLM_TIMEOUT_MIN_MS, LLM_TIMEOUT_MAX_MS);
106
74
  const poll_limit = parseBoundedSupervisorInt(env.BAPI_CONDUCTOR_SUPERVISOR_POLL_LIMIT, POLL_LIMIT_DEFAULT, POLL_LIMIT_MIN, POLL_LIMIT_MAX);
107
75
  return {
108
76
  wake_interval_ms,
@@ -114,9 +82,6 @@ export function resolveSupervisorConfig(overrides = {}, env = process.env) {
114
82
  dead_after_ms,
115
83
  },
116
84
  escalation_cooldown_ms,
117
- llm_enabled,
118
- llm_max_calls,
119
- llm_timeout_ms,
120
85
  poll_limit,
121
86
  };
122
87
  }
@@ -34,8 +34,7 @@ function elapsedSinceSignal(worker, now) {
34
34
  * Inspect run state and return deterministic escalation candidates. Reasons:
35
35
  * `worker_not_started`, `worker_stalled`, `worker_blocked`, `worker_dead`,
36
36
  * `candidate_done_stuck`, `verification_stalled`, and `global_timeout`. Terminal
37
- * workers never produce candidates. `ambiguous` marks candidates that benefit
38
- * from an LLM judgment (long-running stalls) vs. unambiguous ones.
37
+ * workers never produce candidates.
39
38
  */
40
39
  export function findSupervisorEscalationCandidates(state, config, now) {
41
40
  const candidates = [];
@@ -59,7 +58,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
59
58
  state: worker.state,
60
59
  liveness: worker.liveness,
61
60
  elapsed_ms: elapsed,
62
- ambiguous: false,
63
61
  context: baseContext,
64
62
  });
65
63
  continue;
@@ -82,7 +80,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
82
80
  state: worker.state,
83
81
  liveness: worker.liveness,
84
82
  elapsed_ms: elapsed,
85
- ambiguous: false,
86
83
  context: baseContext,
87
84
  });
88
85
  }
@@ -95,7 +92,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
95
92
  state: worker.state,
96
93
  liveness: worker.liveness,
97
94
  elapsed_ms: elapsed,
98
- ambiguous: false,
99
95
  context: { ...baseContext, blocked_reason: worker.blocked_reason },
100
96
  });
101
97
  break;
@@ -107,7 +103,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
107
103
  state: worker.state,
108
104
  liveness: worker.liveness,
109
105
  elapsed_ms: elapsed,
110
- ambiguous: true,
111
106
  context: baseContext,
112
107
  });
113
108
  break;
@@ -124,7 +119,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
124
119
  state: worker.state,
125
120
  liveness: worker.liveness,
126
121
  elapsed_ms: elapsed,
127
- ambiguous: true,
128
122
  context: baseContext,
129
123
  });
130
124
  }
@@ -141,7 +135,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
141
135
  state: worker.state,
142
136
  liveness: worker.liveness,
143
137
  elapsed_ms: elapsed,
144
- ambiguous: true,
145
138
  context: baseContext,
146
139
  });
147
140
  }
@@ -160,7 +153,6 @@ export function findSupervisorEscalationCandidates(state, config, now) {
160
153
  state: null,
161
154
  liveness: null,
162
155
  elapsed_ms: Math.max(0, now - deadlineMs),
163
- ambiguous: false,
164
156
  context: {
165
157
  run_id: state.run_id,
166
158
  deadline_at: state.global_deadline_at,
@@ -176,23 +168,20 @@ function cooldownWindowFor(now, cooldownMs) {
176
168
  return String(Math.floor(now / width));
177
169
  }
178
170
  /**
179
- * Decide whether a candidate should be emitted now. A candidate is SUPPRESSED
171
+ * Decide whether a candidate should be emitted now. A candidate is a DUPLICATE
180
172
  * when an escalation for the same worker + reason was already emitted (or hit a
181
173
  * duplicate) within the SAME cooldown window. Returns the cooldown window
182
174
  * identifier the caller must pass to `makeSupervisorIdempotencyKey` so the
183
- * idempotency key and the suppression check agree.
175
+ * idempotency key and the duplicate check agree.
184
176
  */
185
177
  export function shouldEmitEscalation(state, candidate, config, now) {
186
178
  const cooldownWindow = cooldownWindowFor(now, config.escalation_cooldown_ms);
187
179
  const alreadyDecided = state.escalations.some((record) => record.reason === candidate.reason &&
188
180
  (record.worker_id ?? null) === (candidate.worker_id ?? null) &&
189
181
  record.cooldown_window === cooldownWindow &&
190
- // A prior emit/duplicate OR an explicit LLM "do not escalate" (suppressed)
191
- // decision in this window is binding — do not re-decide (and, for ambiguous
192
- // candidates, do not re-query the LLM) until the window rolls over.
193
- (record.outcome === "emitted" ||
194
- record.outcome === "duplicate" ||
195
- record.outcome === "suppressed"));
182
+ // A prior emitted/duplicate decision in this window is binding — do not
183
+ // re-decide until the window rolls over.
184
+ (record.outcome === "emitted" || record.outcome === "duplicate"));
196
185
  return { emit: !alreadyDecided, cooldown_window: cooldownWindow };
197
186
  }
198
187
  /**
@@ -227,18 +216,13 @@ function formatElapsed(ms) {
227
216
  }
228
217
  /**
229
218
  * Render a concise, SECRET-FREE terminal escalation line. Includes run id,
230
- * worker id, reason, state, liveness, elapsed time, and optional LLM-drafted
231
- * text. Never includes raw payloads or full JSON dumps.
219
+ * worker id, reason, state, liveness, and elapsed time. Never includes raw
220
+ * payloads or full JSON dumps.
232
221
  */
233
- export function formatEscalationForTerminal(runId, candidate, draftText) {
222
+ export function formatEscalationForTerminal(runId, candidate) {
234
223
  const worker = candidate.worker_id ? ` worker=${candidate.worker_id}` : "";
235
224
  const stateBit = candidate.state ? ` state=${candidate.state}` : "";
236
225
  const liveBit = candidate.liveness ? ` liveness=${candidate.liveness}` : "";
237
226
  const elapsed = ` elapsed=${formatElapsed(candidate.elapsed_ms)}`;
238
- let line = `[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`;
239
- if (draftText && draftText.trim().length > 0) {
240
- // Single-line the drafted text so the terminal stays scannable.
241
- line += ` :: ${draftText.replace(/\s+/g, " ").trim()}`;
242
- }
243
- return line;
227
+ return `[supervisor] run=${runId}${worker} reason=${candidate.reason}${stateBit}${liveBit}${elapsed}`;
244
228
  }
@@ -6,9 +6,8 @@
6
6
  * concurrent retries, each event carries a deterministic id derived from a
7
7
  * STABLE idempotency key. The key is built only from normalized dimensions
8
8
  * (run id, optional worker id, reason, kind, cooldown window) — it deliberately
9
- * EXCLUDES raw event payloads and any LLM free text, so two assessments with
10
- * different draft text but the same decision collide on the `events.id` UNIQUE
11
- * constraint instead of double-emitting.
9
+ * EXCLUDES raw event payloads, so two assessments for the same decision
10
+ * collide on the `events.id` UNIQUE constraint instead of double-emitting.
12
11
  *
13
12
  * This module performs NO privileged action: it only writes an audit event to
14
13
  * the local ledger.
@@ -22,7 +21,7 @@ function normalizeDimension(value) {
22
21
  /**
23
22
  * Build a stable idempotency key from normalized dimensions. Whitespace and
24
23
  * case are normalized; empty/undefined `worker_id` collapses to a fixed
25
- * run-level token. NO raw payload or LLM free text ever enters the key.
24
+ * run-level token. NO raw payload ever enters the key.
26
25
  */
27
26
  export function makeSupervisorIdempotencyKey(meta) {
28
27
  const parts = [
@@ -75,8 +74,7 @@ export async function emitSupervisorAssessmentIfNew(input, deps = {}) {
75
74
  const emitEvent = deps.emitEvent ?? emitConductorEvent;
76
75
  const idempotencyKey = makeSupervisorIdempotencyKey(input.idempotency);
77
76
  const eventId = makeSupervisorAssessmentEventId(idempotencyKey);
78
- // Build secret-free assessment details. The assessment carries only the
79
- // classification/decision and optional short drafted text — never raw payload.
77
+ // Build secret-free deterministic assessment details never raw payload.
80
78
  const details = {
81
79
  ...(input.details ?? {}),
82
80
  idempotency_key: idempotencyKey,
@@ -85,12 +83,7 @@ export async function emitSupervisorAssessmentIfNew(input, deps = {}) {
85
83
  cooldown_window: input.idempotency.cooldown_window,
86
84
  classification: input.assessment.classification,
87
85
  confidence: input.assessment.confidence,
88
- should_escalate: input.assessment.should_escalate,
89
- assessment_source: input.assessment.source,
90
86
  };
91
- if (input.assessment.draft_escalation_text) {
92
- details.draft_escalation_text = input.assessment.draft_escalation_text;
93
- }
94
87
  const event = {
95
88
  id: eventId,
96
89
  source: "conductor-supervisor",
@@ -101,7 +94,7 @@ export async function emitSupervisorAssessmentIfNew(input, deps = {}) {
101
94
  observed_via: "supervisor",
102
95
  data: {
103
96
  summary: `supervisor assessment: ${input.idempotency.reason}`,
104
- status: input.assessment.should_escalate ? "escalated" : "noted",
97
+ status: "escalated",
105
98
  reason: input.idempotency.reason,
106
99
  details,
107
100
  },
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Supervisor merge pipeline wrapper (Conductor C6, BAPI-398).
3
3
  *
4
- * Processes an eligible worker-scoped `gate.met` event by calling the API-owned
5
- * merge decision/execution endpoint and recording the returned `merge.*` ledger
6
- * events into the LOCAL conductor ledger. This module:
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: event.worker_id ?? null,
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: event.worker_id ?? null,
149
+ worker_id: attributionWorkerId,
123
150
  }, { emitEvent: deps.emitEvent });
124
151
  emitted.push({ type: ledgerEvent.type, emitted: result.emitted });
125
152
  }
@@ -28,11 +28,8 @@ export function buildSupervisorEscalationWorkerMessage(candidate, assessment, st
28
28
  state: candidate.state,
29
29
  liveness: candidate.liveness,
30
30
  elapsed_ms: candidate.elapsed_ms,
31
- assessment_source: assessment.source,
31
+ assessment_source: "deterministic",
32
32
  };
33
- if (assessment.draft_escalation_text) {
34
- details.draft_escalation_text = assessment.draft_escalation_text;
35
- }
36
33
  return {
37
34
  run_id: state.run_id,
38
35
  worker_id: candidate.worker_id,
@@ -40,7 +37,7 @@ export function buildSupervisorEscalationWorkerMessage(candidate, assessment, st
40
37
  cause_seq: state.last_seq,
41
38
  payload: {
42
39
  summary: `supervisor escalation: ${candidate.reason}`,
43
- status: assessment.should_escalate ? "escalated" : "noted",
40
+ status: "escalated",
44
41
  details,
45
42
  },
46
43
  source: "conductor-supervisor",
@@ -23,7 +23,7 @@ export async function dispatchSupervisorNotification(epicRunId, candidate, asses
23
23
  worker_id: candidate.worker_id ?? null,
24
24
  elapsed_ms: candidate.elapsed_ms,
25
25
  ticket_key: (candidate.context?.ticket_key ?? null),
26
- draft_text: assessment.draft_escalation_text ?? null,
26
+ classification: assessment.classification,
27
27
  },
28
28
  };
29
29
  const headers = {
@@ -12,13 +12,10 @@
12
12
  * 2. supervisor_projection — the resumable operational mirror (this loop owns
13
13
  * writes; crash/restart re-hydrates from it).
14
14
  * 3. supervisor.assessment events — AUDIT ONLY; never override raw truth.
15
- * 4. LLM judgment context — DISPOSABLE; advisory text only, never persisted as
16
- * truth and never able to execute an action.
17
15
  *
18
- * The deterministic loop owns correctness. The LLM is consulted only to classify
19
- * AMBIGUOUS stalls and draft escalation text, within a per-run budget, and
20
- * degrades to deterministic-only behavior whenever it is disabled, exhausted, or
21
- * fails.
16
+ * The deterministic loop owns correctness and always escalates a detected
17
+ * candidate (BAPI-496): there is no LLM judgment boundary and no suppression
18
+ * path.
22
19
  */
23
20
  import { waitForConductorEvent, getSupervisorSnapshot, upsertSupervisorProjection, sendWorkerMessage, } from "./store.js";
24
21
  import { emitSupervisorAssessmentIfNew } from "./supervisor-ledger.js";
@@ -26,36 +23,18 @@ import { sendSupervisorEscalationWorkerMessageIfNew } from "./supervisor-message
26
23
  import { resolveSupervisorConfig } from "./supervisor-config.js";
27
24
  import { hydrateSupervisorRunStateFromSnapshot, applyConductorEventToSupervisorState, applySupervisorHousekeeping, isSupervisorRunTerminal, hasSupervisorGlobalTimeoutElapsed, toSupervisorProjectionInput, } from "./supervisor-state.js";
28
25
  import { findSupervisorEscalationCandidates, shouldEmitEscalation, recordEscalationResult, formatEscalationForTerminal, } from "./supervisor-escalation.js";
29
- import { assessSupervisorCandidate } from "./supervisor-judgment.js";
30
26
  import { makeSupervisorIdempotencyKey } from "./supervisor-ledger.js";
31
- import { createDefaultSupervisorJudgmentClient } from "./supervisor-judgment-python.js";
32
27
  import { resolveConductorBridgeApiAccess, } from "./bridge-api-client.js";
33
28
  import { extractMergeActionIdentityFromGateEvent } from "./merge-ledger.js";
34
29
  import { processGateMetMerge } from "./supervisor-merge.js";
35
30
  import { dispatchSupervisorNotification } from "./supervisor-notification.js";
36
31
  import { ConductorValidationError } from "./errors.js";
37
- /** Compact, secret-free worker snapshot for the judgment request. */
38
- function compactWorkerForJudgment(worker) {
39
- if (!worker)
40
- return null;
41
- return {
42
- worker_id: worker.worker_id,
43
- ticket_key: worker.ticket_key,
44
- state: worker.state,
45
- liveness: worker.liveness,
46
- last_event_time: worker.last_event_time,
47
- last_progress_time: worker.last_progress_time,
48
- };
49
- }
50
- /** Deterministic (degraded) assessment for an unambiguous candidate. */
32
+ /** Deterministic assessment for an escalation candidate. */
51
33
  function deterministicAssessment(candidate) {
52
34
  return {
53
35
  classification: "stuck",
54
36
  confidence: 1,
55
- should_escalate: true,
56
37
  reason: candidate.reason,
57
- draft_escalation_text: null,
58
- source: "degraded",
59
38
  };
60
39
  }
61
40
  /** Resolve the terminal run status from worker outcomes. */
@@ -64,20 +43,20 @@ function terminalStatus(state) {
64
43
  return anyFailed ? "failed" : "complete";
65
44
  }
66
45
  /**
67
- * Process the escalation pipeline for the current state: find candidates, assess
68
- * ambiguous ones (within budget), emit idempotent assessment events, print
46
+ * Process the escalation pipeline for the current state: find candidates,
47
+ * deterministically assess them, emit idempotent assessment events, print
69
48
  * terminal lines for newly-emitted escalations, and record outcomes. Returns
70
49
  * nothing; it mutates `state`.
71
50
  */
72
- async function processEscalations(state, config, client, deps) {
51
+ async function processEscalations(state, config, deps) {
73
52
  const now = deps.now();
74
53
  const candidates = findSupervisorEscalationCandidates(state, config, now);
75
54
  for (const candidate of candidates) {
76
55
  const decision = shouldEmitEscalation(state, candidate, config, now);
77
56
  if (!decision.emit)
78
57
  continue;
79
- // Compute the idempotency key up front so EVERY decision in this window
80
- // including an LLM "do not escalate" — can be recorded under it.
58
+ // Compute the idempotency key up front so the decision in this window can
59
+ // be recorded under it.
81
60
  const idempotency = {
82
61
  run_id: state.run_id,
83
62
  worker_id: candidate.worker_id,
@@ -86,28 +65,8 @@ async function processEscalations(state, config, client, deps) {
86
65
  cooldown_window: decision.cooldown_window,
87
66
  };
88
67
  const idempotencyKey = makeSupervisorIdempotencyKey(idempotency);
89
- let assessment;
90
- if (candidate.ambiguous) {
91
- assessment = await assessSupervisorCandidate({
92
- run_id: state.run_id,
93
- candidate,
94
- worker: candidate.worker_id ? compactWorkerForJudgment(state.workers[candidate.worker_id]) : null,
95
- }, config, state.llm_budget, client);
96
- }
97
- else {
98
- assessment = deterministicAssessment(candidate);
99
- }
68
+ const assessment = deterministicAssessment(candidate);
100
69
  state.latest_assessment = assessment;
101
- if (!assessment.should_escalate) {
102
- // The judgment declined to escalate. Record a `suppressed` decision for
103
- // this cooldown window so the candidate is NOT re-judged every wake
104
- // interval — without this, a persistently-ambiguous worker would re-query
105
- // the LLM each tick, exhaust the per-run budget, and then escalate anyway
106
- // once the degraded fallback kicks in. The decision stands until the
107
- // window rolls over.
108
- recordEscalationResult(state, candidate, decision.cooldown_window, idempotencyKey, "suppressed", now);
109
- continue;
110
- }
111
70
  let outcome = "skipped";
112
71
  try {
113
72
  const result = await deps.emitAssessment({
@@ -145,7 +104,7 @@ async function processEscalations(state, config, client, deps) {
145
104
  // Only print on a genuinely NEW escalation — never re-spam a duplicate after
146
105
  // a crash/restart within the same cooldown window.
147
106
  if (outcome === "emitted") {
148
- deps.log(formatEscalationForTerminal(state.run_id, candidate, assessment.draft_escalation_text));
107
+ deps.log(formatEscalationForTerminal(state.run_id, candidate));
149
108
  // S7: dispatch an out-of-band human notification for newly emitted escalations.
150
109
  // Best-effort: a failure must never stop supervising.
151
110
  if (deps.dispatchNotification) {
@@ -179,7 +138,6 @@ export async function runSupervisor(options, deps = {}) {
179
138
  const upsertProjection = deps.upsertProjection ?? upsertSupervisorProjection;
180
139
  const emitAssessment = deps.emitAssessment ?? emitSupervisorAssessmentIfNew;
181
140
  const sendMessage = deps.sendWorkerMessage ?? sendWorkerMessage;
182
- const judgmentClient = deps.judgmentClient ?? createDefaultSupervisorJudgmentClient(config);
183
141
  const resolveBridgeAccess = deps.resolveBridgeAccess ?? (() => resolveConductorBridgeApiAccess());
184
142
  const processMerge = deps.processMerge ?? processGateMetMerge;
185
143
  const dispatchNotification = deps.dispatchNotification ?? dispatchSupervisorNotification;
@@ -304,7 +262,7 @@ export async function runSupervisor(options, deps = {}) {
304
262
  // Housekeeping (liveness + state-specific stalls) runs every iteration so a
305
263
  // timeout with no events still advances liveness/stall state.
306
264
  applySupervisorHousekeeping(state, config, now());
307
- await processEscalations(state, config, judgmentClient, {
265
+ await processEscalations(state, config, {
308
266
  emitAssessment,
309
267
  sendWorkerMessage: sendMessage,
310
268
  log,
@@ -48,11 +48,6 @@ export function createEmptySupervisorRunState(runId, config, now) {
48
48
  gates: {},
49
49
  latest_assessment: null,
50
50
  escalations: [],
51
- llm_budget: {
52
- enabled: config.llm_enabled,
53
- max_calls: config.llm_max_calls,
54
- used_calls: 0,
55
- },
56
51
  started_at: startedIso,
57
52
  updated_at: startedIso,
58
53
  global_deadline_at: msToIso(now + config.global_timeout_ms),
@@ -93,15 +88,8 @@ export function hydrateSupervisorRunStateFromSnapshot(snapshot, runId, config, n
93
88
  escalations: Array.isArray(summary.escalations)
94
89
  ? summary.escalations
95
90
  : [],
96
- llm_budget: summary.llm_budget && typeof summary.llm_budget === "object"
97
- ? {
98
- enabled: config.llm_enabled,
99
- max_calls: config.llm_max_calls,
100
- used_calls: typeof summary.llm_budget.used_calls === "number"
101
- ? summary.llm_budget.used_calls
102
- : 0,
103
- }
104
- : empty.llm_budget,
91
+ // A legacy pre-BAPI-496 LLM-budget summary key is intentionally ignored
92
+ // rather than treated as an error — old projections still carry it.
105
93
  started_at: typeof summary.started_at === "string" ? summary.started_at : empty.started_at,
106
94
  global_deadline_at: typeof summary.global_deadline_at === "string" ? summary.global_deadline_at : empty.global_deadline_at,
107
95
  roster_discovered: summary.roster_discovered === true,
@@ -536,11 +524,10 @@ function compactWorker(worker) {
536
524
  * holds compact worker summaries (never raw event data); `gates` holds gate
537
525
  * metadata; `assessment` holds the latest assessment or null; and `summary`
538
526
  * holds the FULL resumable supervisor state (worker watchdog states, liveness,
539
- * last seq, escalation history, LLM budget usage, run start/deadline metadata).
527
+ * last seq, escalation history, run start/deadline metadata).
540
528
  *
541
529
  * The serialized summary is secret-free by construction: it is built only from
542
- * the compact state fields above — never from raw payloads, secrets, or LLM
543
- * prompts.
530
+ * the compact state fields above — never from raw payloads or secrets.
544
531
  */
545
532
  export function toSupervisorProjectionInput(state) {
546
533
  const summary = {
@@ -553,7 +540,6 @@ export function toSupervisorProjectionInput(state) {
553
540
  gates: state.gates,
554
541
  latest_assessment: state.latest_assessment,
555
542
  escalations: state.escalations,
556
- llm_budget: state.llm_budget,
557
543
  started_at: state.started_at,
558
544
  updated_at: state.updated_at,
559
545
  global_deadline_at: state.global_deadline_at,
@@ -3,8 +3,8 @@
3
3
  * (BAPI-396, conductor C4).
4
4
  *
5
5
  * These are pure TypeScript type declarations with NO runtime behavior so every
6
- * supervisor module (config, reducer, escalation, judgment, runtime) can depend
7
- * on them freely without import cycles or side effects.
6
+ * supervisor module (config, reducer, escalation, runtime) can depend on them
7
+ * freely without import cycles or side effects.
8
8
  *
9
9
  * Vocabulary split — the supervisor tracks two ORTHOGONAL axes per worker:
10
10
  * - {@link WatchdogState}: the worker's progress through the run lifecycle
@@ -34,6 +34,10 @@ export const SEMANTIC_EVENT_TYPES = [
34
34
  "merge.attempted",
35
35
  "merge.succeeded",
36
36
  "merge.failed",
37
+ // BAPI-494: a PR that cannot be merged (CONFLICTING/DIRTY). A terminal,
38
+ // head-scoped blocking signal that folds to `blocked` and routes through the
39
+ // BAPI-441 remediation pass as a resume-mode redispatch.
40
+ "merge.conflict",
37
41
  "merge.pending_approval",
38
42
  // BAPI-440 PR review-state telemetry event types.
39
43
  "review.passed",
@@ -47,6 +51,14 @@ export const SEMANTIC_EVENT_TYPES = [
47
51
  // post-merge repository re-index, so the (stateless) reconcile loop can fold a
48
52
  // merged ticket to `done` from the ledger instead of an in-memory wait map.
49
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",
50
62
  ];
51
63
  /**
52
64
  * Type guard: returns `true` only when `value` is one of the exact taxonomy
@@ -11,9 +11,14 @@
11
11
  import { z } from "zod";
12
12
  import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
13
13
  import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
14
- import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
14
+ import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, } from "./store.js";
15
15
  import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
16
16
  import { waitForDoneGate, resolveDispatchRunIdForBinding } from "./pr-ci-producer.js";
17
+ // BAPI-527: worker-facing ledger operations run through the conductor CLI
18
+ // subprocess (under the captured conductor Node), NOT the in-process SQLite store,
19
+ // so the worker Node never loads the `better-sqlite3` native binary. `store.ts`'s
20
+ // `checkWorkerMessages` is intentionally NOT imported here anymore.
21
+ import { checkWorkerMessagesViaCli, emitConductorEventIfNewViaCli } from "./worker-ledger-cli.js";
17
22
  import { resolveConductorBridgeApiAccess, fetchEpicRunState, ConductorBridgeApiError } from "./bridge-api-client.js";
18
23
  /** Build a Zod enum from the semantic taxonomy so arbitrary types are rejected up front. */
19
24
  export function buildEventTypeZodEnum() {
@@ -37,7 +42,10 @@ function jsonResult(value) {
37
42
  /**
38
43
  * Wrap a tool handler so any thrown value becomes a sanitized structured JSON
39
44
  * error envelope (validation -> 400, store-busy -> 503, otherwise 500). Raw
40
- * input data, stack traces, and secret material are never echoed back.
45
+ * input data, stack traces, and secret material are never echoed back. Any
46
+ * allowlisted structured `details` on the envelope (e.g. the BAPI-526
47
+ * LEDGER_NATIVE_MODULE_LOAD_FAILED node_version/ABI/module fields) is preserved
48
+ * through {@link jsonResult}'s pretty serialization unchanged.
41
49
  */
42
50
  export function withConductorToolErrorHandling(handler) {
43
51
  return async (args) => {
@@ -249,7 +257,17 @@ function registerWaitForDoneGateTool(registerTool) {
249
257
  timeoutMs: args.timeout_ms,
250
258
  pollIntervalMs: args.poll_interval_ms,
251
259
  worktreePath: args.worktree_path,
252
- }, { resolveRunId: resolveDispatchRunIdForBinding });
260
+ }, {
261
+ resolveRunId: resolveDispatchRunIdForBinding,
262
+ // BAPI-527: route the gate's ENTIRE dedup+emit through the conductor CLI
263
+ // subprocess. Injecting `emitIfNew` (not just the write sink) replaces the
264
+ // in-process `emitConductorEventIfNew`, whose `eventAlreadyExists` poll
265
+ // pre-check would load better-sqlite3 in the worker Node. The dedupe key +
266
+ // deterministic id are derived purely and the id is forwarded to the CLI,
267
+ // so dedup happens server-side on the events.id UNIQUE constraint — the
268
+ // worker path performs NO in-process ledger read or write.
269
+ emitIfNew: (input, dimensions) => emitConductorEventIfNewViaCli(input, dimensions),
270
+ });
253
271
  return jsonResult({
254
272
  gate_met: result.gate_met,
255
273
  timed_out: result.timed_out,
@@ -338,9 +356,13 @@ function registerCheckMessagesTool(registerTool) {
338
356
  if (runId.trim().length === 0 || workerId.trim().length === 0) {
339
357
  throw new ConductorValidationError("Conductor worker identity is unavailable: provide run_id + worker_id, or set BAPI_CONDUCTOR_RUN_ID and BAPI_CONDUCTOR_WORKER_ID.");
340
358
  }
341
- const result = await checkWorkerMessages({
342
- run_id: runId,
343
- worker_id: workerId,
359
+ // BAPI-527: read+ack through the conductor CLI subprocess (captured
360
+ // conductor Node), NOT the in-process store, so the worker Node never loads
361
+ // better-sqlite3. A missing/invalid CONDUCTOR_NODE_PATH surfaces as the
362
+ // typed LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE envelope via the wrapper below.
363
+ const result = await checkWorkerMessagesViaCli({
364
+ runId,
365
+ workerId,
344
366
  limit: args.limit,
345
367
  });
346
368
  return jsonResult(result);