@bridge_gpt/mcp-server 0.2.10 → 0.2.13

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 (54) hide show
  1. package/README.md +66 -4
  2. package/build/commands.generated.js +6 -6
  3. package/build/conductor/bridge-api-client.js +2 -1
  4. package/build/conductor/cli.js +16 -16
  5. package/build/conductor/doctor.js +79 -2
  6. package/build/conductor/epic-reconcile.js +213 -16
  7. package/build/conductor/epic-runtime.js +215 -60
  8. package/build/conductor/epic-state.js +105 -16
  9. package/build/conductor/errors.js +12 -0
  10. package/build/conductor/git-ci-types.js +10 -0
  11. package/build/conductor/git-producer.js +4 -4
  12. package/build/conductor/local-merge.js +212 -0
  13. package/build/conductor/merge-ledger.js +7 -7
  14. package/build/conductor/pr-ci-producer.js +18 -8
  15. package/build/conductor/pr-review-producer.js +2 -2
  16. package/build/conductor/producer-ledger.js +5 -5
  17. package/build/conductor/spec-review-producer.js +88 -0
  18. package/build/conductor/store.js +100 -25
  19. package/build/conductor/supervisor-ledger.js +2 -2
  20. package/build/conductor/supervisor-merge.js +5 -5
  21. package/build/conductor/supervisor-message-relay.js +1 -1
  22. package/build/conductor/supervisor-runtime.js +10 -10
  23. package/build/conductor/taxonomy.js +9 -0
  24. package/build/conductor/tools.js +5 -5
  25. package/build/conductor-bin.js +12689 -19
  26. package/build/conductor-claude-hook-bin.js +167 -17
  27. package/build/decision-page-schema.js +26 -0
  28. package/build/doctor.js +203 -0
  29. package/build/index.js +25084 -3632
  30. package/build/init.js +57 -0
  31. package/build/install-bridge.js +80 -0
  32. package/build/mcp-profile.js +33 -30
  33. package/build/pipelines.generated.js +70 -48
  34. package/build/readme.generated.js +1 -1
  35. package/build/sfcc/client.js +151 -0
  36. package/build/sfcc/config.js +39 -0
  37. package/build/sfcc/credentials.js +136 -0
  38. package/build/sfcc/ocapi-shape.js +77 -0
  39. package/build/sfcc/output.js +39 -0
  40. package/build/sfcc/permissions.js +136 -0
  41. package/build/sfcc/reads-custom-object-def.js +119 -0
  42. package/build/sfcc/reads-site-preference.js +158 -0
  43. package/build/sfcc/reads-system-object.js +162 -0
  44. package/build/sfcc/register.js +73 -0
  45. package/build/sfcc/setup-status.js +114 -0
  46. package/build/sfcc/tool-wrapper.js +70 -0
  47. package/build/start-tickets-conductor.js +9 -1
  48. package/build/start-tickets.js +47 -4
  49. package/build/version.generated.js +1 -1
  50. package/package.json +7 -4
  51. package/pipelines/check-ci-ticket.json +2 -2
  52. package/pipelines/implement-ticket.json +2 -2
  53. package/pipelines/learn-repository.json +84 -42
  54. package/smoke-test/SMOKE-TEST.md +11 -17
@@ -18,11 +18,66 @@
18
18
  */
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { existsSync } from "node:fs";
21
- import Database from "better-sqlite3";
22
21
  import { SEMANTIC_EVENT_TYPES, assertSemanticEventType, } from "./taxonomy.js";
23
22
  import { ConductorValidationError } from "./errors.js";
24
23
  import { prepareDataJsonForStorage } from "./data-normalization.js";
25
24
  import { ensureConductorDatabaseFile, getConductorLedgerPath, getConductorPathHealth, detectNetworkMountedHome, } from "./paths.js";
25
+ // ---------------------------------------------------------------------------
26
+ // Optional native binding loader (BAPI-451)
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Thrown when the optional native `better-sqlite3` binding cannot be loaded
30
+ * (the optional dependency was skipped at install, or no prebuilt binary exists
31
+ * for this platform×Node and the source build failed). In this degraded state
32
+ * Conductor persistence is unavailable; callers degrade gracefully rather than
33
+ * crashing the MCP server, whose core (non-Conductor) tools are unaffected.
34
+ */
35
+ export class ConductorPersistenceUnavailableError extends Error {
36
+ constructor(message = "Conductor persistence is unavailable: the optional 'better-sqlite3' native module could not be loaded.") {
37
+ super(message);
38
+ this.name = "ConductorPersistenceUnavailableError";
39
+ }
40
+ }
41
+ /**
42
+ * Cached, concurrency-safe async loader for the optional `better-sqlite3` module.
43
+ *
44
+ * The static `import Database from "better-sqlite3"` was removed (BAPI-451) so the
45
+ * native binding is never pulled into the eager boot graph. The first call performs
46
+ * a dynamic `import("better-sqlite3")`; the in-flight promise is stored module-scoped
47
+ * and synchronously, so concurrent first callers from the read and write open paths
48
+ * share a single load and the ESM-resolution + native-binding cost is paid exactly
49
+ * once. On failure the degraded state is cached in `dbLoadDegraded`: the stderr
50
+ * warning is emitted exactly once and every subsequent call throws
51
+ * {@link ConductorPersistenceUnavailableError} immediately, without re-attempting the
52
+ * failing import on every Conductor operation.
53
+ */
54
+ let databaseModulePromise = null;
55
+ let dbLoadDegraded = false;
56
+ async function getDatabaseModule() {
57
+ // Cached degraded state: never re-attempt a known-failing native import.
58
+ if (dbLoadDegraded) {
59
+ throw new ConductorPersistenceUnavailableError();
60
+ }
61
+ if (databaseModulePromise)
62
+ return databaseModulePromise;
63
+ databaseModulePromise = (async () => {
64
+ try {
65
+ const mod = await import("better-sqlite3");
66
+ // CommonJS interop: the better-sqlite3 default export is the constructor.
67
+ const ctor = (mod.default ?? mod);
68
+ return ctor;
69
+ }
70
+ catch {
71
+ if (!dbLoadDegraded) {
72
+ dbLoadDegraded = true;
73
+ console.error("[conductor] Conductor persistence is unavailable: the optional 'better-sqlite3' native module could not be loaded. " +
74
+ "Conductor coordination features are disabled for this session; core MCP tools are unaffected.");
75
+ }
76
+ throw new ConductorPersistenceUnavailableError();
77
+ }
78
+ })();
79
+ return databaseModulePromise;
80
+ }
26
81
  const BUSY_TIMEOUT_DEFAULT = 10_000;
27
82
  const BUSY_TIMEOUT_MIN = 250;
28
83
  const BUSY_TIMEOUT_MAX = 120_000;
@@ -122,11 +177,17 @@ export function resolveConductorStoreConfig(env = process.env) {
122
177
  * `merge.attempted`, `merge.succeeded`, and `merge.failed` lifecycle event types
123
178
  * to the same `events.type` CHECK vocabulary. Bumped to 5 in BAPI-413 because
124
179
  * the per-merge human approval feature adds the `merge.pending_approval` lifecycle
125
- * event type to the same CHECK vocabulary. Older ledgers stamped at a lower
126
- * version are rebuilt by {@link migrateConductorSchemaIfNeeded} so their CHECK
127
- * clause accepts the current taxonomy.
180
+ * event type to the same CHECK vocabulary. Bumped to 6 in BAPI-445 because the
181
+ * pre-implementation spec re-review verdict feature adds the `spec_review.passed`
182
+ * and `spec_review.changes_requested` event types to the same `events.type`
183
+ * CHECK vocabulary. Bumped to 7 because the durable parse-after-merge fold adds
184
+ * the `parse.triggered` event type to the same `events.type` CHECK vocabulary
185
+ * (replacing the in-memory parse-wait map so stateless epic-tick invocations can
186
+ * fold a merged ticket to `done`). Older ledgers stamped at a lower version are
187
+ * rebuilt by {@link migrateConductorSchemaIfNeeded} so their CHECK clause accepts
188
+ * the current taxonomy.
128
189
  */
129
- export const CURRENT_CONDUCTOR_SCHEMA_VERSION = 5;
190
+ export const CURRENT_CONDUCTOR_SCHEMA_VERSION = 7;
130
191
  /** Render the taxonomy `CHECK (type IN (...))` clause from the single source of truth. */
131
192
  function buildTypeCheckClause() {
132
193
  const list = SEMANTIC_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
@@ -292,7 +353,11 @@ export function migrateConductorSchemaIfNeeded(db) {
292
353
  * permissions, open SQLite, apply WAL/concurrency pragmas, and initialize the
293
354
  * schema before returning the live connection.
294
355
  */
295
- export function openWritableConductorDatabase(config = resolveConductorStoreConfig()) {
356
+ export async function openWritableConductorDatabase(config = resolveConductorStoreConfig()) {
357
+ // Resolve the native binding lazily (BAPI-451). Throws
358
+ // ConductorPersistenceUnavailableError when the optional module is absent so
359
+ // write paths fail-soft into a structured "persistence unavailable" envelope.
360
+ const Database = await getDatabaseModule();
296
361
  ensureConductorDatabaseFile();
297
362
  const db = new Database(getConductorLedgerPath());
298
363
  // busy_timeout MUST be set first: `journal_mode = WAL` and the schema writes
@@ -333,7 +398,17 @@ export function openWritableConductorDatabase(config = resolveConductorStoreConf
333
398
  * Open the ledger read-only when it already exists; otherwise return `null`.
334
399
  * Never creates files and never runs schema migrations.
335
400
  */
336
- export function openReadonlyConductorDatabaseIfExists(config = resolveConductorStoreConfig()) {
401
+ export async function openReadonlyConductorDatabaseIfExists(config = resolveConductorStoreConfig()) {
402
+ // Read paths degrade silently: when the optional native binding is absent the
403
+ // loader throws, and callers (poll/snapshot/doctor) treat a null connection as
404
+ // "no ledger yet" — returning empty results instead of crashing.
405
+ let Database;
406
+ try {
407
+ Database = await getDatabaseModule();
408
+ }
409
+ catch {
410
+ return null;
411
+ }
337
412
  const dbPath = getConductorLedgerPath();
338
413
  let db;
339
414
  try {
@@ -426,7 +501,7 @@ function projectRow(row, dataMode) {
426
501
  * prepares/redacts the data JSON BEFORE opening the write transaction. Defaults
427
502
  * `id`, `time`, and `schema_version`. Closes the connection in a `finally`.
428
503
  */
429
- export function emitConductorEvent(input, config = resolveConductorStoreConfig()) {
504
+ export async function emitConductorEvent(input, config = resolveConductorStoreConfig()) {
430
505
  // --- Validation & preparation OUTSIDE any SQLite transaction. ---
431
506
  const type = assertSemanticEventType(input.type);
432
507
  if (typeof input.source !== "string" || input.source.trim().length === 0) {
@@ -450,7 +525,7 @@ export function emitConductorEvent(input, config = resolveConductorStoreConfig()
450
525
  const schemaVersion = typeof input.schema_version === "number" && Number.isFinite(input.schema_version)
451
526
  ? input.schema_version
452
527
  : 1;
453
- const db = openWritableConductorDatabase(config);
528
+ const db = await openWritableConductorDatabase(config);
454
529
  try {
455
530
  const insert = db.prepare(`INSERT INTO events
456
531
  (id, source, type, subject, run_id, worker_id, producer, schema_version, time, data_json, confidence, observed_via)
@@ -581,13 +656,13 @@ function buildFilterClause(filter) {
581
656
  * applying allowlisted filters and a bounded limit. Returns compact summaries
582
657
  * unless `data_mode="full"`. `next_seq` is the cursor to pass on the next call.
583
658
  */
584
- export function pollConductorEvents(options = {}, config = resolveConductorStoreConfig()) {
659
+ export async function pollConductorEvents(options = {}, config = resolveConductorStoreConfig()) {
585
660
  const sinceSeq = typeof options.since_seq === "number" && options.since_seq >= 0
586
661
  ? Math.floor(options.since_seq)
587
662
  : 1;
588
663
  const dataMode = options.data_mode === "full" ? "full" : "summary";
589
664
  const limit = Math.min(POLL_LIMIT_MAX, Math.max(1, Math.floor(options.limit ?? POLL_LIMIT_DEFAULT)));
590
- const db = openReadonlyConductorDatabaseIfExists(config);
665
+ const db = await openReadonlyConductorDatabaseIfExists(config);
591
666
  if (!db) {
592
667
  return { events: [], next_seq: sinceSeq, count: 0 };
593
668
  }
@@ -617,11 +692,11 @@ export async function waitForConductorEvent(options = {}, config = resolveConduc
617
692
  const timeoutMs = Math.min(WAIT_TIMEOUT_MAX_MS, Math.max(0, Math.floor(options.timeout_ms ?? WAIT_TIMEOUT_MAX_MS)));
618
693
  const deadline = Date.now() + timeoutMs;
619
694
  // Poll immediately at least once.
620
- let result = pollConductorEvents(options, config);
695
+ let result = await pollConductorEvents(options, config);
621
696
  while (result.count === 0 && Date.now() < deadline) {
622
697
  const remaining = deadline - Date.now();
623
698
  await sleep(Math.min(WAIT_POLL_INTERVAL_MS, Math.max(1, remaining)));
624
- result = pollConductorEvents(options, config);
699
+ result = await pollConductorEvents(options, config);
625
700
  }
626
701
  return { ...result, timed_out: result.count === 0 };
627
702
  }
@@ -663,8 +738,8 @@ export function rowToSupervisorProjection(row) {
663
738
  * The projection is maintained by the conductor supervisor runtime
664
739
  * (`conductor supervise`); this read never derives state from raw events.
665
740
  */
666
- export function getSupervisorSnapshot(runId, config = resolveConductorStoreConfig()) {
667
- const db = openReadonlyConductorDatabaseIfExists(config);
741
+ export async function getSupervisorSnapshot(runId, config = resolveConductorStoreConfig()) {
742
+ const db = await openReadonlyConductorDatabaseIfExists(config);
668
743
  if (!db) {
669
744
  return { run_id: runId, status: "unknown", projection: null };
670
745
  }
@@ -689,7 +764,7 @@ export function getSupervisorSnapshot(runId, config = resolveConductorStoreConfi
689
764
  * connection is always closed in a `finally`. The statement is fully
690
765
  * parameterized — run-supplied values never enter the SQL string.
691
766
  */
692
- export function upsertSupervisorProjection(input, config = resolveConductorStoreConfig()) {
767
+ export async function upsertSupervisorProjection(input, config = resolveConductorStoreConfig()) {
693
768
  if (typeof input.run_id !== "string" || input.run_id.trim().length === 0) {
694
769
  throw new ConductorValidationError("Supervisor projection 'run_id' is required.");
695
770
  }
@@ -697,7 +772,7 @@ export function upsertSupervisorProjection(input, config = resolveConductorStore
697
772
  const gatesJson = JSON.stringify(input.gates ?? {});
698
773
  const assessmentJson = input.assessment == null ? null : JSON.stringify(input.assessment);
699
774
  const summaryJson = input.summary == null ? null : JSON.stringify(input.summary);
700
- const db = openWritableConductorDatabase(config);
775
+ const db = await openWritableConductorDatabase(config);
701
776
  try {
702
777
  const upsert = db.prepare(`INSERT INTO supervisor_projection
703
778
  (run_id, status, last_seq, last_event_time, active_workers_json, gates_json, assessment_json, summary_json, updated_at)
@@ -743,7 +818,7 @@ export function upsertSupervisorProjection(input, config = resolveConductorStore
743
818
  * and network-home degradation. Never creates the DB, inserts a diagnostic
744
819
  * event, or mutates WAL/schema state.
745
820
  */
746
- export function doctorConductorLedger(config = resolveConductorStoreConfig()) {
821
+ export async function doctorConductorLedger(config = resolveConductorStoreConfig()) {
747
822
  const health = getConductorPathHealth();
748
823
  const network = detectNetworkMountedHome();
749
824
  const warnings = [...health.warnings];
@@ -755,7 +830,7 @@ export function doctorConductorLedger(config = resolveConductorStoreConfig()) {
755
830
  let eventCount = null;
756
831
  let maxSeq = null;
757
832
  let userVersion = null;
758
- const db = openReadonlyConductorDatabaseIfExists(config);
833
+ const db = await openReadonlyConductorDatabaseIfExists(config);
759
834
  if (db) {
760
835
  try {
761
836
  const tableRow = db
@@ -810,7 +885,7 @@ export function doctorConductorLedger(config = resolveConductorStoreConfig()) {
810
885
  * `supervisor_projection`. If the DB does not exist, returns a successful empty
811
886
  * purge. Runs `wal_checkpoint(TRUNCATE)` after deletion when possible.
812
887
  */
813
- export function purgeConductorLedger(config = resolveConductorStoreConfig()) {
888
+ export async function purgeConductorLedger(config = resolveConductorStoreConfig()) {
814
889
  if (!existsSync(getConductorLedgerPath())) {
815
890
  return {
816
891
  ok: true,
@@ -818,7 +893,7 @@ export function purgeConductorLedger(config = resolveConductorStoreConfig()) {
818
893
  deleted: { events: 0, messages: 0, supervisor_projection: 0 },
819
894
  };
820
895
  }
821
- const db = openWritableConductorDatabase(config);
896
+ const db = await openWritableConductorDatabase(config);
822
897
  try {
823
898
  const purge = db.transaction(() => {
824
899
  const events = db.prepare("DELETE FROM events").run().changes;
@@ -963,7 +1038,7 @@ function insertRelayAuditEvent(db, audit) {
963
1038
  * are NEVER logged; unexpected storage errors are re-thrown after the connection
964
1039
  * is closed in `finally`.
965
1040
  */
966
- export function sendWorkerMessage(input, config = resolveConductorStoreConfig()) {
1041
+ export async function sendWorkerMessage(input, config = resolveConductorStoreConfig()) {
967
1042
  validateWorkerMessageIdentity(input);
968
1043
  const runId = input.run_id.trim();
969
1044
  const workerId = input.worker_id.trim();
@@ -997,7 +1072,7 @@ export function sendWorkerMessage(input, config = resolveConductorStoreConfig())
997
1072
  const producer = typeof input.producer === "string" && input.producer.trim().length > 0
998
1073
  ? input.producer.trim()
999
1074
  : "worker-message-relay";
1000
- const db = openWritableConductorDatabase(config);
1075
+ const db = await openWritableConductorDatabase(config);
1001
1076
  try {
1002
1077
  // (1) Exact idempotency-key hit: never insert or audit a second time.
1003
1078
  const existing = db
@@ -1084,7 +1159,7 @@ export function sendWorkerMessage(input, config = resolveConductorStoreConfig())
1084
1159
  * always reads each pending message exactly once. Returned payloads are NEVER
1085
1160
  * logged; the connection is closed in `finally`.
1086
1161
  */
1087
- export function checkWorkerMessages(input, config = resolveConductorStoreConfig()) {
1162
+ export async function checkWorkerMessages(input, config = resolveConductorStoreConfig()) {
1088
1163
  if (typeof input.run_id !== "string" || input.run_id.trim().length === 0) {
1089
1164
  throw new ConductorValidationError("Message poll 'run_id' is required and must be a non-empty string.");
1090
1165
  }
@@ -1096,7 +1171,7 @@ export function checkWorkerMessages(input, config = resolveConductorStoreConfig(
1096
1171
  const limit = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0
1097
1172
  ? Math.min(CHECK_MESSAGES_LIMIT_MAX, Math.floor(input.limit))
1098
1173
  : CHECK_MESSAGES_LIMIT_DEFAULT;
1099
- const db = openWritableConductorDatabase(config);
1174
+ const db = await openWritableConductorDatabase(config);
1100
1175
  try {
1101
1176
  const selectPending = db.prepare(`SELECT * FROM messages
1102
1177
  WHERE run_id = @run_id AND worker_id = @worker_id AND state = 'pending'
@@ -71,7 +71,7 @@ function isDuplicateConstraintError(error) {
71
71
  * reason: "duplicate" }` rather than surfaced as a failure. This function writes
72
72
  * ONLY an audit event — it performs no privileged action.
73
73
  */
74
- export function emitSupervisorAssessmentIfNew(input, deps = {}) {
74
+ export async function emitSupervisorAssessmentIfNew(input, deps = {}) {
75
75
  const emitEvent = deps.emitEvent ?? emitConductorEvent;
76
76
  const idempotencyKey = makeSupervisorIdempotencyKey(input.idempotency);
77
77
  const eventId = makeSupervisorAssessmentEventId(idempotencyKey);
@@ -107,7 +107,7 @@ export function emitSupervisorAssessmentIfNew(input, deps = {}) {
107
107
  },
108
108
  };
109
109
  try {
110
- const result = emitEvent(event);
110
+ const result = await emitEvent(event);
111
111
  return { emitted: true, event_id: eventId, event: result.event };
112
112
  }
113
113
  catch (error) {
@@ -69,7 +69,7 @@ export async function processGateMetMerge(access, event, deps = {}) {
69
69
  }
70
70
  const actionKey = identity.action_key;
71
71
  // Terminal-success short-circuit BEFORE any API call (idempotent across restart).
72
- if (checkTerminal(actionKey)) {
72
+ if (await checkTerminal(actionKey)) {
73
73
  return { processed: false, reason: "already_succeeded" };
74
74
  }
75
75
  const baseDetails = {
@@ -85,7 +85,7 @@ export async function processGateMetMerge(access, event, deps = {}) {
85
85
  }
86
86
  catch (error) {
87
87
  const reason = mapApiErrorReason(error);
88
- emitMergeLedgerEventIfNew({
88
+ await emitMergeLedgerEventIfNew({
89
89
  type: "merge.failed",
90
90
  action_key: actionKey,
91
91
  status: "failed",
@@ -101,18 +101,18 @@ export async function processGateMetMerge(access, event, deps = {}) {
101
101
  for (const ledgerEvent of response.ledger_events ?? []) {
102
102
  // Dry-run dedup: skip if one already exists locally, but NEVER treat it as
103
103
  // terminal — a later enabled merge for the same PR/head/gate can still run.
104
- if (ledgerEvent.type === "merge.dry_run" && checkDryRun(actionKey)) {
104
+ if (ledgerEvent.type === "merge.dry_run" && (await checkDryRun(actionKey))) {
105
105
  emitted.push({ type: ledgerEvent.type, emitted: false });
106
106
  continue;
107
107
  }
108
108
  // Pending-approval dedup: skip re-emission if a pending_approval event was
109
109
  // already recorded for this action key. NEVER terminal — the worker must
110
110
  // remain active until the backend reports the merge is approved and complete.
111
- if (ledgerEvent.type === "merge.pending_approval" && checkPendingApproval(actionKey)) {
111
+ if (ledgerEvent.type === "merge.pending_approval" && (await checkPendingApproval(actionKey))) {
112
112
  emitted.push({ type: ledgerEvent.type, emitted: false });
113
113
  continue;
114
114
  }
115
- const result = emitMergeLedgerEventIfNew({
115
+ const result = await emitMergeLedgerEventIfNew({
116
116
  type: ledgerEvent.type,
117
117
  action_key: actionKey,
118
118
  status: ledgerEvent.status,
@@ -85,7 +85,7 @@ export function buildSupervisorRemediationWorkerMessage(input) {
85
85
  * rather than treating as failures. Unexpected storage errors are NOT swallowed
86
86
  * — they propagate to the caller's best-effort wrapper.
87
87
  */
88
- export function sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, deps = {}) {
88
+ export async function sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, deps = {}) {
89
89
  const sendMessage = deps.sendMessage ?? sendWorkerMessage;
90
90
  const input = buildSupervisorEscalationWorkerMessage(candidate, assessment, state);
91
91
  return sendMessage(input);
@@ -110,7 +110,7 @@ async function processEscalations(state, config, client, deps) {
110
110
  }
111
111
  let outcome = "skipped";
112
112
  try {
113
- const result = deps.emitAssessment({
113
+ const result = await deps.emitAssessment({
114
114
  run_id: state.run_id,
115
115
  worker_id: candidate.worker_id,
116
116
  assessment,
@@ -134,7 +134,7 @@ async function processEscalations(state, config, client, deps) {
134
134
  // text are never logged on failure.
135
135
  if ((outcome === "emitted" || outcome === "duplicate") && candidate.worker_id) {
136
136
  try {
137
- sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, {
137
+ await sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, {
138
138
  sendMessage: deps.sendWorkerMessage,
139
139
  });
140
140
  }
@@ -216,9 +216,9 @@ export async function runSupervisor(options, deps = {}) {
216
216
  }
217
217
  }
218
218
  };
219
- const persist = (state) => {
219
+ const persist = async (state) => {
220
220
  try {
221
- upsertProjection(toSupervisorProjectionInput(state));
221
+ await upsertProjection(toSupervisorProjectionInput(state));
222
222
  }
223
223
  catch {
224
224
  // Persistence is best-effort within an iteration; the next iteration retries.
@@ -226,7 +226,7 @@ export async function runSupervisor(options, deps = {}) {
226
226
  };
227
227
  let state;
228
228
  try {
229
- const snapshot = getSnapshot(runId);
229
+ const snapshot = await getSnapshot(runId);
230
230
  state = hydrateSupervisorRunStateFromSnapshot(snapshot, runId, config, now());
231
231
  }
232
232
  catch {
@@ -258,7 +258,7 @@ export async function runSupervisor(options, deps = {}) {
258
258
  // still in progress.
259
259
  if (isSupervisorRunTerminal(state)) {
260
260
  state.status = terminalStatus(state);
261
- persist(state);
261
+ await persist(state);
262
262
  log(`[supervisor] run=${runId} complete status=${state.status} workers=${Object.keys(state.workers).length}`);
263
263
  return {
264
264
  run_id: runId,
@@ -271,7 +271,7 @@ export async function runSupervisor(options, deps = {}) {
271
271
  }
272
272
  if (hasSupervisorGlobalTimeoutElapsed(state, now())) {
273
273
  state.status = "timed_out";
274
- persist(state);
274
+ await persist(state);
275
275
  errorLog(`[supervisor] run=${runId} global timeout elapsed`);
276
276
  return {
277
277
  run_id: runId,
@@ -314,12 +314,12 @@ export async function runSupervisor(options, deps = {}) {
314
314
  // Persist after the batch AND after housekeeping/escalation, because state
315
315
  // can change even when no events arrived. The terminal/timeout exit is
316
316
  // re-evaluated at the top of the next iteration (no extra blocking wait).
317
- persist(state);
317
+ await persist(state);
318
318
  }
319
319
  // Iteration cap reached without a terminal/global-timeout exit (frozen clock):
320
320
  // treat as a timeout rather than spinning.
321
321
  state.status = "timed_out";
322
- persist(state);
322
+ await persist(state);
323
323
  return {
324
324
  run_id: runId,
325
325
  status: "timed_out",
@@ -333,7 +333,7 @@ export async function runSupervisor(options, deps = {}) {
333
333
  // Unexpected boundary exception — sanitize, attempt to persist failed status.
334
334
  state.status = "failed";
335
335
  try {
336
- upsertProjection(toSupervisorProjectionInput(state));
336
+ await upsertProjection(toSupervisorProjectionInput(state));
337
337
  }
338
338
  catch {
339
339
  /* best-effort */
@@ -38,6 +38,15 @@ export const SEMANTIC_EVENT_TYPES = [
38
38
  // BAPI-440 PR review-state telemetry event types.
39
39
  "review.passed",
40
40
  "review.changes_requested",
41
+ // BAPI-445 pre-implementation SPEC re-review verdict event types. Kept
42
+ // distinct from the review.* PR-review verdicts above so the conductor never
43
+ // mis-folds a spec-review outcome as the implementation PR's review state.
44
+ "spec_review.passed",
45
+ "spec_review.changes_requested",
46
+ // Durable parse-after-merge marker. Emitted by epic-tick when it triggers a
47
+ // post-merge repository re-index, so the (stateless) reconcile loop can fold a
48
+ // merged ticket to `done` from the ledger instead of an in-memory wait map.
49
+ "parse.triggered",
41
50
  ];
42
51
  /**
43
52
  * Type guard: returns `true` only when `value` is one of the exact taxonomy
@@ -79,7 +79,7 @@ function registerEmitEventTool(registerTool) {
79
79
  observed_via: z.string().optional().describe("Optional channel the event was observed through."),
80
80
  },
81
81
  }, withConductorToolErrorHandling(async (args) => {
82
- const result = emitConductorEvent({
82
+ const result = await emitConductorEvent({
83
83
  source: args.source,
84
84
  type: args.type,
85
85
  subject: args.subject,
@@ -113,7 +113,7 @@ function registerPollEventsTool(registerTool) {
113
113
  limit: z.number().int().positive().optional().describe("Max events to return (default 100, max 1000)."),
114
114
  },
115
115
  }, withConductorToolErrorHandling(async (args) => {
116
- const result = pollConductorEvents({
116
+ const result = await pollConductorEvents({
117
117
  since_seq: args.since_seq ?? 1,
118
118
  filter: args.filter,
119
119
  data_mode: args.data_mode ?? "summary",
@@ -167,7 +167,7 @@ function registerGetSupervisorSnapshotTool(registerTool) {
167
167
  run_id: z.string().describe("The run/session identifier to read the supervisor projection for."),
168
168
  },
169
169
  }, withConductorToolErrorHandling(async (args) => {
170
- const result = getSupervisorSnapshot(args.run_id);
170
+ const result = await getSupervisorSnapshot(args.run_id);
171
171
  return jsonResult(result);
172
172
  }));
173
173
  }
@@ -299,7 +299,7 @@ function registerSendMessageTool(registerTool) {
299
299
  .describe("Optional per-call cooldown override in ms (falls back to the configured cooldown)."),
300
300
  },
301
301
  }, withConductorToolErrorHandling(async (args) => {
302
- const result = sendWorkerMessage({
302
+ const result = await sendWorkerMessage({
303
303
  run_id: args.run_id,
304
304
  worker_id: args.worker_id,
305
305
  type: args.type,
@@ -338,7 +338,7 @@ function registerCheckMessagesTool(registerTool) {
338
338
  if (runId.trim().length === 0 || workerId.trim().length === 0) {
339
339
  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
340
  }
341
- const result = checkWorkerMessages({
341
+ const result = await checkWorkerMessages({
342
342
  run_id: runId,
343
343
  worker_id: workerId,
344
344
  limit: args.limit,