@bridge_gpt/mcp-server 0.2.14 → 0.2.18

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 (51) hide show
  1. package/README.md +2 -2
  2. package/build/agents.generated.js +19 -1
  3. package/build/commands.generated.js +6 -5
  4. package/build/conductor/bridge-api-client.js +191 -11
  5. package/build/conductor/claude-hook.js +22 -4
  6. package/build/conductor/cli.js +11 -13
  7. package/build/conductor/done-gate.js +5 -0
  8. package/build/conductor/epic-reconcile.js +62 -13
  9. package/build/conductor/epic-runtime.js +447 -35
  10. package/build/conductor/epic-state.js +517 -63
  11. package/build/conductor/errors.js +41 -0
  12. package/build/conductor/event-accessors.js +234 -0
  13. package/build/conductor/file-scope-guard.js +201 -0
  14. package/build/conductor/github-mergeability.js +85 -0
  15. package/build/conductor/local-merge.js +47 -1
  16. package/build/conductor/merge-identity.js +41 -0
  17. package/build/conductor/merge-ledger.js +13 -68
  18. package/build/conductor/plan.js +12 -2
  19. package/build/conductor/pr-discovery.js +11 -1
  20. package/build/conductor/supervisor-config.js +4 -39
  21. package/build/conductor/supervisor-escalation.js +10 -26
  22. package/build/conductor/supervisor-ledger.js +5 -12
  23. package/build/conductor/supervisor-message-relay.js +2 -5
  24. package/build/conductor/supervisor-notification.js +1 -1
  25. package/build/conductor/supervisor-runtime.js +12 -54
  26. package/build/conductor/supervisor-state.js +4 -18
  27. package/build/conductor/supervisor-types.js +2 -2
  28. package/build/conductor/taxonomy.js +4 -0
  29. package/build/conductor-bin.js +2333 -666
  30. package/build/conductor-claude-hook-bin.js +4 -2
  31. package/build/doctor.js +32 -0
  32. package/build/index.js +9985 -7579
  33. package/build/install-bridge.js +25 -8
  34. package/build/install-doctor.js +387 -0
  35. package/build/pipelines.generated.js +30 -5
  36. package/build/readme.generated.js +1 -1
  37. package/build/regression-check.js +872 -0
  38. package/build/review-tickets.js +175 -21
  39. package/build/sfcc/permissions.js +13 -1
  40. package/build/sfcc/reads-custom-object-def.js +56 -39
  41. package/build/sfcc/register.js +1 -1
  42. package/build/sfcc/tool-wrapper.js +5 -1
  43. package/build/start-tickets-conductor.js +22 -6
  44. package/build/start-tickets-prereqs.js +73 -0
  45. package/build/start-tickets.js +122 -22
  46. package/build/version.generated.js +1 -1
  47. package/package.json +5 -5
  48. package/pipelines/review-ticket.json +24 -2
  49. package/public/css/main.min.css +3272 -1
  50. package/public/css/main.min.css.map +1 -1
  51. package/smoke-test/SMOKE-TEST.md +4 -2
@@ -16,82 +16,27 @@
16
16
  * It performs NO privileged action and never handles VCS write credentials.
17
17
  */
18
18
  import { createHash } from "node:crypto";
19
- import { normalizePrNumber, normalizeRepoName, normalizeSha } from "./git-ci-types.js";
19
+ import { getMergeIdentity } from "./event-accessors.js";
20
+ import { buildGateIdentity, makeMergeActionKey, } from "./merge-identity.js";
20
21
  import { emitConductorEvent, openReadonlyConductorDatabaseIfExists, } from "./store.js";
21
- /**
22
- * Compose the stable gate-identity segment, mirroring the Python
23
- * `normalize_gate_identity`: `{name}@{config_hash}` when a hash is present
24
- * (lower-cased), otherwise just the gate name.
25
- */
26
- export function buildGateIdentity(gateName, configHash) {
27
- const name = gateName.trim();
28
- const hash = typeof configHash === "string" ? configHash.trim() : "";
29
- return hash ? `${name}@${hash.toLowerCase()}` : name;
30
- }
31
- /**
32
- * Build the deterministic action key. Normalizes repo / PR / head SHA exactly as
33
- * the Python side does (lower-cased SHA, trimmed repo, positive integer PR) so the
34
- * conductor-computed key and the API-recomputed key are byte-identical. Throws on
35
- * any invalid component. The branch name is never part of the key.
36
- */
37
- export function makeMergeActionKey(repo, prNumber, headSha, gateIdentity) {
38
- const r = normalizeRepoName(repo);
39
- const pr = normalizePrNumber(prNumber);
40
- const sha = normalizeSha(headSha);
41
- const gate = (gateIdentity ?? "").trim();
42
- if (r === null || pr === null || sha === null || gate.length === 0) {
43
- throw new Error("invalid merge action key component");
44
- }
45
- return `merge:${r}:${pr}:${sha}:${gate}`;
46
- }
22
+ // BAPI-493: the merge-identity primitives now live in the dependency-light
23
+ // `merge-identity.ts` module (so the typed event-accessor layer can build a
24
+ // MergeActionIdentity without an import cycle). They are re-exported here so
25
+ // existing importers (`supervisor-merge.ts`, tests) keep a stable API surface.
26
+ export { buildGateIdentity, makeMergeActionKey };
47
27
  /**
48
28
  * Resolve the immutable merge identity from a `gate.met` event. Returns `null`
49
29
  * unless the event is a worker-scoped `gate.met` carrying a complete PR binding
50
30
  * (repo, pr_number, head_sha, gate_name). Run-level or incomplete events yield
51
31
  * `null`. Branch-name fields, if present, are ignored.
32
+ *
33
+ * BAPI-493: canonical parsing now lives in the typed accessor layer — this is a
34
+ * thin compatibility wrapper over {@link getMergeIdentity} so `supervisor-merge.ts`
35
+ * and other callers keep an unchanged call site while the hand-rolled
36
+ * `data.details` reader is retired.
52
37
  */
53
38
  export function extractMergeActionIdentityFromGateEvent(event) {
54
- if (event.type !== "gate.met")
55
- return null;
56
- // Worker scope is required — a run-level gate.met never binds a specific worker.
57
- if (typeof event.worker_id !== "string" || event.worker_id.trim().length === 0) {
58
- return null;
59
- }
60
- const data = event.data ?? {};
61
- const details = data.details;
62
- if (!details || typeof details !== "object")
63
- return null;
64
- const d = details;
65
- const repo = normalizeRepoName(d.repo);
66
- const prNumber = normalizePrNumber(d.pr_number);
67
- const headSha = normalizeSha(d.head_sha);
68
- const gateName = typeof d.gate_name === "string" ? d.gate_name.trim() : "";
69
- if (repo === null || prNumber === null || headSha === null || gateName.length === 0) {
70
- return null;
71
- }
72
- const configHash = typeof d.config_hash === "string" && d.config_hash.trim().length > 0
73
- ? d.config_hash.trim()
74
- : null;
75
- const requiredChecks = Array.isArray(d.required_checks)
76
- ? d.required_checks.filter((c) => typeof c === "string" && c.trim().length > 0)
77
- : [];
78
- const gateIdentity = buildGateIdentity(gateName, configHash);
79
- const actionKey = makeMergeActionKey(repo, prNumber, headSha, gateIdentity);
80
- return {
81
- repo,
82
- pr_number: prNumber,
83
- head_sha: headSha,
84
- gate_name: gateName,
85
- config_hash: configHash,
86
- required_checks: requiredChecks,
87
- gate_identity: gateIdentity,
88
- action_key: actionKey,
89
- gate_event: {
90
- id: typeof event.id === "string" ? event.id : undefined,
91
- seq: typeof event.seq === "number" ? event.seq : undefined,
92
- time: typeof event.time === "string" ? event.time : undefined,
93
- },
94
- };
39
+ return getMergeIdentity(event);
95
40
  }
96
41
  /**
97
42
  * Derive a deterministic, UUID-shaped event id from the event type plus action
@@ -31,12 +31,16 @@ import { stableJsonHash } from "./git-ci-types.js";
31
31
  *
32
32
  * - Trims outer whitespace from every `ticket_key` (nodes and edges).
33
33
  * - Sorts each node's `depends_on` array alphabetically.
34
+ * - Sorts each node's `touched_files` array alphabetically, when present.
34
35
  * - Sorts `nodes` lexicographically by normalized `ticket_key`.
35
36
  * - Sorts `edges` lexicographically by `from` then `to`.
37
+ * - Sorts each edge's `overlap_files` array alphabetically, when present.
36
38
  *
37
39
  * The canonical form is fed to {@link hashPlan} / `stableJsonHash`, which
38
40
  * additionally sorts object keys recursively, so two independently constructed
39
- * equal plans always produce a byte-identical SHA-256 digest.
41
+ * equal plans always produce a byte-identical SHA-256 digest. Only fields
42
+ * already present on the input are normalized — canonicalization never
43
+ * derives new data from filesystem, git, network, or LLM state.
40
44
  */
41
45
  export function canonicalizePlanDAG(plan) {
42
46
  const nodes = plan.nodes
@@ -44,10 +48,16 @@ export function canonicalizePlanDAG(plan) {
44
48
  ...node,
45
49
  ticket_key: node.ticket_key.trim(),
46
50
  depends_on: [...node.depends_on].map((k) => k.trim()).sort(),
51
+ ...(node.touched_files ? { touched_files: [...node.touched_files].sort() } : {}),
47
52
  }))
48
53
  .sort((a, b) => a.ticket_key.localeCompare(b.ticket_key));
49
54
  const edges = [...plan.edges]
50
- .map((e) => ({ from: e.from.trim(), to: e.to.trim() }))
55
+ .map((e) => ({
56
+ from: e.from.trim(),
57
+ to: e.to.trim(),
58
+ ...(e.kind ? { kind: e.kind } : {}),
59
+ ...(e.overlap_files ? { overlap_files: [...e.overlap_files].sort() } : {}),
60
+ }))
51
61
  .sort((a, b) => {
52
62
  const cmp = a.from.localeCompare(b.from);
53
63
  return cmp !== 0 ? cmp : a.to.localeCompare(b.to);
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { execFileSync } from "node:child_process";
13
13
  import { normalizePrNumber, normalizeRepoName, normalizeSha } from "./git-ci-types.js";
14
+ import { parseGhPrMergeabilityFields } from "./github-mergeability.js";
14
15
  import { getGitWorktreeContext } from "./git-inspection.js";
15
16
  /** Bounded timeout for the one-shot `gh` lookup. */
16
17
  export const GH_COMMAND_TIMEOUT_MS = 5_000;
@@ -30,7 +31,13 @@ export function runGhCommand(args, options = {}) {
30
31
  return { ok: false, stdout: "" };
31
32
  }
32
33
  }
33
- const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,headRefOid,headRefName,url,state"];
34
+ const GH_PR_VIEW_ARGS = [
35
+ "pr",
36
+ "view",
37
+ "--json",
38
+ // BAPI-494: mergeability fields added to the SAME one-shot call — no new gh process.
39
+ "number,headRefOid,headRefName,url,state,mergeable,mergeStateStatus",
40
+ ];
34
41
  /**
35
42
  * Perform a one-shot `gh pr view` lookup for the current branch's PR. Returns a
36
43
  * normalized {@link DiscoveredPr} or `null` when gh is unavailable, there is no
@@ -55,10 +62,13 @@ export function discoverPrWithGhCli(options = {}, deps = {}) {
55
62
  const state = typeof record.state === "string" ? record.state : "";
56
63
  if (number === null || state.length === 0)
57
64
  return null;
65
+ const mergeability = parseGhPrMergeabilityFields(record);
58
66
  const discovered = {
59
67
  number,
60
68
  head_sha: normalizeSha(record.headRefOid),
61
69
  state,
70
+ mergeable: mergeability.mergeable,
71
+ mergeStateStatus: mergeability.mergeStateStatus,
62
72
  };
63
73
  if (typeof record.headRefName === "string" && record.headRefName.trim().length > 0) {
64
74
  discovered.head_ref = record.headRefName.trim();
@@ -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
  },
@@ -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