@bridge_gpt/mcp-server 0.2.9 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -7
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +263 -35
- package/build/conductor/cli.js +38 -17
- package/build/conductor/doctor.js +35 -2
- package/build/conductor/done-gate.js +301 -58
- package/build/conductor/epic-reconcile.js +318 -4
- package/build/conductor/epic-runtime.js +382 -18
- package/build/conductor/epic-state.js +188 -15
- package/build/conductor/errors.js +12 -0
- package/build/conductor/git-ci-types.js +16 -0
- package/build/conductor/git-producer.js +4 -4
- package/build/conductor/merge-ledger.js +7 -7
- package/build/conductor/pr-ci-producer.js +118 -19
- package/build/conductor/pr-review-producer.js +116 -0
- package/build/conductor/producer-ledger.js +5 -5
- package/build/conductor/spec-review-producer.js +88 -0
- package/build/conductor/store.js +105 -26
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-merge.js +5 -5
- package/build/conductor/supervisor-message-relay.js +32 -1
- package/build/conductor/supervisor-runtime.js +10 -10
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +7 -7
- package/build/conductor-bin.js +12350 -19
- package/build/conductor-claude-hook-bin.js +167 -17
- package/build/decision-page-schema.js +26 -0
- package/build/doctor.js +200 -0
- package/build/index.js +23696 -4351
- package/build/init.js +481 -0
- package/build/install-bridge.js +772 -0
- package/build/mcp-profile.js +43 -0
- package/build/pipelines.generated.js +70 -48
- package/build/readme.generated.js +1 -1
- package/build/start-tickets-conductor.js +1 -0
- package/build/start-tickets.js +186 -10
- package/build/upgrade-cli.js +154 -0
- package/build/version.generated.js +1 -1
- package/package.json +7 -4
- package/pipelines/check-ci-ticket.json +2 -2
- package/pipelines/implement-ticket.json +2 -2
- package/pipelines/learn-repository.json +84 -42
- package/smoke-test/SMOKE-TEST.md +11 -17
package/build/conductor/store.js
CHANGED
|
@@ -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;
|
|
@@ -32,7 +87,7 @@ const RETENTION_MAX_ROWS_DEFAULT = 50_000;
|
|
|
32
87
|
const RETENTION_MAX_ROWS_MIN = 100;
|
|
33
88
|
const RETENTION_MAX_ROWS_MAX = 10_000_000;
|
|
34
89
|
const POLL_LIMIT_DEFAULT = 100;
|
|
35
|
-
const POLL_LIMIT_MAX = 1000;
|
|
90
|
+
export const POLL_LIMIT_MAX = 1000;
|
|
36
91
|
// Message relay per-type cooldown bounds (BAPI-397). A value <= 0 disables the
|
|
37
92
|
// cooldown; positive values are clamped to [1s, 24h].
|
|
38
93
|
const MESSAGE_COOLDOWN_DEFAULT_MS = 300_000;
|
|
@@ -122,11 +177,14 @@ 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.
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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. Older ledgers stamped at a lower version are rebuilt by
|
|
184
|
+
* {@link migrateConductorSchemaIfNeeded} so their CHECK clause accepts the
|
|
185
|
+
* current taxonomy.
|
|
128
186
|
*/
|
|
129
|
-
export const CURRENT_CONDUCTOR_SCHEMA_VERSION =
|
|
187
|
+
export const CURRENT_CONDUCTOR_SCHEMA_VERSION = 6;
|
|
130
188
|
/** Render the taxonomy `CHECK (type IN (...))` clause from the single source of truth. */
|
|
131
189
|
function buildTypeCheckClause() {
|
|
132
190
|
const list = SEMANTIC_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
@@ -292,7 +350,11 @@ export function migrateConductorSchemaIfNeeded(db) {
|
|
|
292
350
|
* permissions, open SQLite, apply WAL/concurrency pragmas, and initialize the
|
|
293
351
|
* schema before returning the live connection.
|
|
294
352
|
*/
|
|
295
|
-
export function openWritableConductorDatabase(config = resolveConductorStoreConfig()) {
|
|
353
|
+
export async function openWritableConductorDatabase(config = resolveConductorStoreConfig()) {
|
|
354
|
+
// Resolve the native binding lazily (BAPI-451). Throws
|
|
355
|
+
// ConductorPersistenceUnavailableError when the optional module is absent so
|
|
356
|
+
// write paths fail-soft into a structured "persistence unavailable" envelope.
|
|
357
|
+
const Database = await getDatabaseModule();
|
|
296
358
|
ensureConductorDatabaseFile();
|
|
297
359
|
const db = new Database(getConductorLedgerPath());
|
|
298
360
|
// busy_timeout MUST be set first: `journal_mode = WAL` and the schema writes
|
|
@@ -333,7 +395,17 @@ export function openWritableConductorDatabase(config = resolveConductorStoreConf
|
|
|
333
395
|
* Open the ledger read-only when it already exists; otherwise return `null`.
|
|
334
396
|
* Never creates files and never runs schema migrations.
|
|
335
397
|
*/
|
|
336
|
-
export function openReadonlyConductorDatabaseIfExists(config = resolveConductorStoreConfig()) {
|
|
398
|
+
export async function openReadonlyConductorDatabaseIfExists(config = resolveConductorStoreConfig()) {
|
|
399
|
+
// Read paths degrade silently: when the optional native binding is absent the
|
|
400
|
+
// loader throws, and callers (poll/snapshot/doctor) treat a null connection as
|
|
401
|
+
// "no ledger yet" — returning empty results instead of crashing.
|
|
402
|
+
let Database;
|
|
403
|
+
try {
|
|
404
|
+
Database = await getDatabaseModule();
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
337
409
|
const dbPath = getConductorLedgerPath();
|
|
338
410
|
let db;
|
|
339
411
|
try {
|
|
@@ -426,7 +498,7 @@ function projectRow(row, dataMode) {
|
|
|
426
498
|
* prepares/redacts the data JSON BEFORE opening the write transaction. Defaults
|
|
427
499
|
* `id`, `time`, and `schema_version`. Closes the connection in a `finally`.
|
|
428
500
|
*/
|
|
429
|
-
export function emitConductorEvent(input, config = resolveConductorStoreConfig()) {
|
|
501
|
+
export async function emitConductorEvent(input, config = resolveConductorStoreConfig()) {
|
|
430
502
|
// --- Validation & preparation OUTSIDE any SQLite transaction. ---
|
|
431
503
|
const type = assertSemanticEventType(input.type);
|
|
432
504
|
if (typeof input.source !== "string" || input.source.trim().length === 0) {
|
|
@@ -450,7 +522,7 @@ export function emitConductorEvent(input, config = resolveConductorStoreConfig()
|
|
|
450
522
|
const schemaVersion = typeof input.schema_version === "number" && Number.isFinite(input.schema_version)
|
|
451
523
|
? input.schema_version
|
|
452
524
|
: 1;
|
|
453
|
-
const db = openWritableConductorDatabase(config);
|
|
525
|
+
const db = await openWritableConductorDatabase(config);
|
|
454
526
|
try {
|
|
455
527
|
const insert = db.prepare(`INSERT INTO events
|
|
456
528
|
(id, source, type, subject, run_id, worker_id, producer, schema_version, time, data_json, confidence, observed_via)
|
|
@@ -552,6 +624,13 @@ function buildFilterClause(filter) {
|
|
|
552
624
|
conditions.push("run_id = @f_run_id");
|
|
553
625
|
params.f_run_id = filter.run_id;
|
|
554
626
|
}
|
|
627
|
+
if (filter.run_ids && filter.run_ids.length > 0) {
|
|
628
|
+
const placeholders = filter.run_ids.map((_r, i) => `@f_run_ids_${i}`);
|
|
629
|
+
filter.run_ids.forEach((r, i) => {
|
|
630
|
+
params[`f_run_ids_${i}`] = r;
|
|
631
|
+
});
|
|
632
|
+
conditions.push(`run_id IN (${placeholders.join(", ")})`);
|
|
633
|
+
}
|
|
555
634
|
if (filter.worker_id) {
|
|
556
635
|
conditions.push("worker_id = @f_worker_id");
|
|
557
636
|
params.f_worker_id = filter.worker_id;
|
|
@@ -574,13 +653,13 @@ function buildFilterClause(filter) {
|
|
|
574
653
|
* applying allowlisted filters and a bounded limit. Returns compact summaries
|
|
575
654
|
* unless `data_mode="full"`. `next_seq` is the cursor to pass on the next call.
|
|
576
655
|
*/
|
|
577
|
-
export function pollConductorEvents(options = {}, config = resolveConductorStoreConfig()) {
|
|
656
|
+
export async function pollConductorEvents(options = {}, config = resolveConductorStoreConfig()) {
|
|
578
657
|
const sinceSeq = typeof options.since_seq === "number" && options.since_seq >= 0
|
|
579
658
|
? Math.floor(options.since_seq)
|
|
580
659
|
: 1;
|
|
581
660
|
const dataMode = options.data_mode === "full" ? "full" : "summary";
|
|
582
661
|
const limit = Math.min(POLL_LIMIT_MAX, Math.max(1, Math.floor(options.limit ?? POLL_LIMIT_DEFAULT)));
|
|
583
|
-
const db = openReadonlyConductorDatabaseIfExists(config);
|
|
662
|
+
const db = await openReadonlyConductorDatabaseIfExists(config);
|
|
584
663
|
if (!db) {
|
|
585
664
|
return { events: [], next_seq: sinceSeq, count: 0 };
|
|
586
665
|
}
|
|
@@ -610,11 +689,11 @@ export async function waitForConductorEvent(options = {}, config = resolveConduc
|
|
|
610
689
|
const timeoutMs = Math.min(WAIT_TIMEOUT_MAX_MS, Math.max(0, Math.floor(options.timeout_ms ?? WAIT_TIMEOUT_MAX_MS)));
|
|
611
690
|
const deadline = Date.now() + timeoutMs;
|
|
612
691
|
// Poll immediately at least once.
|
|
613
|
-
let result = pollConductorEvents(options, config);
|
|
692
|
+
let result = await pollConductorEvents(options, config);
|
|
614
693
|
while (result.count === 0 && Date.now() < deadline) {
|
|
615
694
|
const remaining = deadline - Date.now();
|
|
616
695
|
await sleep(Math.min(WAIT_POLL_INTERVAL_MS, Math.max(1, remaining)));
|
|
617
|
-
result = pollConductorEvents(options, config);
|
|
696
|
+
result = await pollConductorEvents(options, config);
|
|
618
697
|
}
|
|
619
698
|
return { ...result, timed_out: result.count === 0 };
|
|
620
699
|
}
|
|
@@ -656,8 +735,8 @@ export function rowToSupervisorProjection(row) {
|
|
|
656
735
|
* The projection is maintained by the conductor supervisor runtime
|
|
657
736
|
* (`conductor supervise`); this read never derives state from raw events.
|
|
658
737
|
*/
|
|
659
|
-
export function getSupervisorSnapshot(runId, config = resolveConductorStoreConfig()) {
|
|
660
|
-
const db = openReadonlyConductorDatabaseIfExists(config);
|
|
738
|
+
export async function getSupervisorSnapshot(runId, config = resolveConductorStoreConfig()) {
|
|
739
|
+
const db = await openReadonlyConductorDatabaseIfExists(config);
|
|
661
740
|
if (!db) {
|
|
662
741
|
return { run_id: runId, status: "unknown", projection: null };
|
|
663
742
|
}
|
|
@@ -682,7 +761,7 @@ export function getSupervisorSnapshot(runId, config = resolveConductorStoreConfi
|
|
|
682
761
|
* connection is always closed in a `finally`. The statement is fully
|
|
683
762
|
* parameterized — run-supplied values never enter the SQL string.
|
|
684
763
|
*/
|
|
685
|
-
export function upsertSupervisorProjection(input, config = resolveConductorStoreConfig()) {
|
|
764
|
+
export async function upsertSupervisorProjection(input, config = resolveConductorStoreConfig()) {
|
|
686
765
|
if (typeof input.run_id !== "string" || input.run_id.trim().length === 0) {
|
|
687
766
|
throw new ConductorValidationError("Supervisor projection 'run_id' is required.");
|
|
688
767
|
}
|
|
@@ -690,7 +769,7 @@ export function upsertSupervisorProjection(input, config = resolveConductorStore
|
|
|
690
769
|
const gatesJson = JSON.stringify(input.gates ?? {});
|
|
691
770
|
const assessmentJson = input.assessment == null ? null : JSON.stringify(input.assessment);
|
|
692
771
|
const summaryJson = input.summary == null ? null : JSON.stringify(input.summary);
|
|
693
|
-
const db = openWritableConductorDatabase(config);
|
|
772
|
+
const db = await openWritableConductorDatabase(config);
|
|
694
773
|
try {
|
|
695
774
|
const upsert = db.prepare(`INSERT INTO supervisor_projection
|
|
696
775
|
(run_id, status, last_seq, last_event_time, active_workers_json, gates_json, assessment_json, summary_json, updated_at)
|
|
@@ -736,7 +815,7 @@ export function upsertSupervisorProjection(input, config = resolveConductorStore
|
|
|
736
815
|
* and network-home degradation. Never creates the DB, inserts a diagnostic
|
|
737
816
|
* event, or mutates WAL/schema state.
|
|
738
817
|
*/
|
|
739
|
-
export function doctorConductorLedger(config = resolveConductorStoreConfig()) {
|
|
818
|
+
export async function doctorConductorLedger(config = resolveConductorStoreConfig()) {
|
|
740
819
|
const health = getConductorPathHealth();
|
|
741
820
|
const network = detectNetworkMountedHome();
|
|
742
821
|
const warnings = [...health.warnings];
|
|
@@ -748,7 +827,7 @@ export function doctorConductorLedger(config = resolveConductorStoreConfig()) {
|
|
|
748
827
|
let eventCount = null;
|
|
749
828
|
let maxSeq = null;
|
|
750
829
|
let userVersion = null;
|
|
751
|
-
const db = openReadonlyConductorDatabaseIfExists(config);
|
|
830
|
+
const db = await openReadonlyConductorDatabaseIfExists(config);
|
|
752
831
|
if (db) {
|
|
753
832
|
try {
|
|
754
833
|
const tableRow = db
|
|
@@ -803,7 +882,7 @@ export function doctorConductorLedger(config = resolveConductorStoreConfig()) {
|
|
|
803
882
|
* `supervisor_projection`. If the DB does not exist, returns a successful empty
|
|
804
883
|
* purge. Runs `wal_checkpoint(TRUNCATE)` after deletion when possible.
|
|
805
884
|
*/
|
|
806
|
-
export function purgeConductorLedger(config = resolveConductorStoreConfig()) {
|
|
885
|
+
export async function purgeConductorLedger(config = resolveConductorStoreConfig()) {
|
|
807
886
|
if (!existsSync(getConductorLedgerPath())) {
|
|
808
887
|
return {
|
|
809
888
|
ok: true,
|
|
@@ -811,7 +890,7 @@ export function purgeConductorLedger(config = resolveConductorStoreConfig()) {
|
|
|
811
890
|
deleted: { events: 0, messages: 0, supervisor_projection: 0 },
|
|
812
891
|
};
|
|
813
892
|
}
|
|
814
|
-
const db = openWritableConductorDatabase(config);
|
|
893
|
+
const db = await openWritableConductorDatabase(config);
|
|
815
894
|
try {
|
|
816
895
|
const purge = db.transaction(() => {
|
|
817
896
|
const events = db.prepare("DELETE FROM events").run().changes;
|
|
@@ -956,7 +1035,7 @@ function insertRelayAuditEvent(db, audit) {
|
|
|
956
1035
|
* are NEVER logged; unexpected storage errors are re-thrown after the connection
|
|
957
1036
|
* is closed in `finally`.
|
|
958
1037
|
*/
|
|
959
|
-
export function sendWorkerMessage(input, config = resolveConductorStoreConfig()) {
|
|
1038
|
+
export async function sendWorkerMessage(input, config = resolveConductorStoreConfig()) {
|
|
960
1039
|
validateWorkerMessageIdentity(input);
|
|
961
1040
|
const runId = input.run_id.trim();
|
|
962
1041
|
const workerId = input.worker_id.trim();
|
|
@@ -990,7 +1069,7 @@ export function sendWorkerMessage(input, config = resolveConductorStoreConfig())
|
|
|
990
1069
|
const producer = typeof input.producer === "string" && input.producer.trim().length > 0
|
|
991
1070
|
? input.producer.trim()
|
|
992
1071
|
: "worker-message-relay";
|
|
993
|
-
const db = openWritableConductorDatabase(config);
|
|
1072
|
+
const db = await openWritableConductorDatabase(config);
|
|
994
1073
|
try {
|
|
995
1074
|
// (1) Exact idempotency-key hit: never insert or audit a second time.
|
|
996
1075
|
const existing = db
|
|
@@ -1077,7 +1156,7 @@ export function sendWorkerMessage(input, config = resolveConductorStoreConfig())
|
|
|
1077
1156
|
* always reads each pending message exactly once. Returned payloads are NEVER
|
|
1078
1157
|
* logged; the connection is closed in `finally`.
|
|
1079
1158
|
*/
|
|
1080
|
-
export function checkWorkerMessages(input, config = resolveConductorStoreConfig()) {
|
|
1159
|
+
export async function checkWorkerMessages(input, config = resolveConductorStoreConfig()) {
|
|
1081
1160
|
if (typeof input.run_id !== "string" || input.run_id.trim().length === 0) {
|
|
1082
1161
|
throw new ConductorValidationError("Message poll 'run_id' is required and must be a non-empty string.");
|
|
1083
1162
|
}
|
|
@@ -1089,7 +1168,7 @@ export function checkWorkerMessages(input, config = resolveConductorStoreConfig(
|
|
|
1089
1168
|
const limit = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0
|
|
1090
1169
|
? Math.min(CHECK_MESSAGES_LIMIT_MAX, Math.floor(input.limit))
|
|
1091
1170
|
: CHECK_MESSAGES_LIMIT_DEFAULT;
|
|
1092
|
-
const db = openWritableConductorDatabase(config);
|
|
1171
|
+
const db = await openWritableConductorDatabase(config);
|
|
1093
1172
|
try {
|
|
1094
1173
|
const selectPending = db.prepare(`SELECT * FROM messages
|
|
1095
1174
|
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,
|
|
@@ -47,6 +47,37 @@ export function buildSupervisorEscalationWorkerMessage(candidate, assessment, st
|
|
|
47
47
|
producer: "worker-message-relay",
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the remediation NUDGE relay message for a blocked epic ticket whose
|
|
52
|
+
* worker is still alive. Mirrors {@link buildSupervisorEscalationWorkerMessage}'s
|
|
53
|
+
* allowlisted top-level keys (`summary`, `status`, `details`) and `cause_seq`
|
|
54
|
+
* idempotency contract. `status` is fixed to `"remediation_requested"`; all
|
|
55
|
+
* remediation detail (reason, attempt, the bounded review digest, and its
|
|
56
|
+
* truncation flag) is nested under `details`. The `review_digest` text is passed
|
|
57
|
+
* through unchanged — redaction/size-capping is the backend's responsibility.
|
|
58
|
+
*/
|
|
59
|
+
export function buildSupervisorRemediationWorkerMessage(input) {
|
|
60
|
+
const messageType = input.reason === "ci.failed" ? "supervisor.ci_failed" : "supervisor.changes_requested";
|
|
61
|
+
const summaryLabel = input.reason === "ci.failed" ? "ci failed" : "review changes requested";
|
|
62
|
+
return {
|
|
63
|
+
run_id: input.runId,
|
|
64
|
+
worker_id: input.workerId,
|
|
65
|
+
type: messageType,
|
|
66
|
+
cause_seq: input.causeSeq,
|
|
67
|
+
payload: {
|
|
68
|
+
summary: `${summaryLabel}: ${input.ticketKey}`,
|
|
69
|
+
status: "remediation_requested",
|
|
70
|
+
details: {
|
|
71
|
+
reason: input.reason,
|
|
72
|
+
attempt: input.attempt,
|
|
73
|
+
review_digest: input.reviewDigest,
|
|
74
|
+
truncated: input.truncated,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
source: "conductor-supervisor",
|
|
78
|
+
producer: "worker-message-relay",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
50
81
|
/**
|
|
51
82
|
* Build the escalation message and enqueue it through {@link sendWorkerMessage}.
|
|
52
83
|
* The store result already encodes the expected idempotency/cooldown outcomes
|
|
@@ -54,7 +85,7 @@ export function buildSupervisorEscalationWorkerMessage(candidate, assessment, st
|
|
|
54
85
|
* rather than treating as failures. Unexpected storage errors are NOT swallowed
|
|
55
86
|
* — they propagate to the caller's best-effort wrapper.
|
|
56
87
|
*/
|
|
57
|
-
export function sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, deps = {}) {
|
|
88
|
+
export async function sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, deps = {}) {
|
|
58
89
|
const sendMessage = deps.sendMessage ?? sendWorkerMessage;
|
|
59
90
|
const input = buildSupervisorEscalationWorkerMessage(candidate, assessment, state);
|
|
60
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 */
|
|
@@ -35,6 +35,14 @@ export const SEMANTIC_EVENT_TYPES = [
|
|
|
35
35
|
"merge.succeeded",
|
|
36
36
|
"merge.failed",
|
|
37
37
|
"merge.pending_approval",
|
|
38
|
+
// BAPI-440 PR review-state telemetry event types.
|
|
39
|
+
"review.passed",
|
|
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",
|
|
38
46
|
];
|
|
39
47
|
/**
|
|
40
48
|
* Type guard: returns `true` only when `value` is one of the exact taxonomy
|
package/build/conductor/tools.js
CHANGED
|
@@ -13,7 +13,7 @@ import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
|
13
13
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
14
14
|
import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
15
15
|
import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
|
|
16
|
-
import { waitForDoneGate } from "./pr-ci-producer.js";
|
|
16
|
+
import { waitForDoneGate, resolveDispatchRunIdForBinding } from "./pr-ci-producer.js";
|
|
17
17
|
import { resolveConductorBridgeApiAccess, fetchEpicRunState, ConductorBridgeApiError } from "./bridge-api-client.js";
|
|
18
18
|
/** Build a Zod enum from the semantic taxonomy so arbitrary types are rejected up front. */
|
|
19
19
|
export function buildEventTypeZodEnum() {
|
|
@@ -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
|
}
|
|
@@ -249,7 +249,7 @@ function registerWaitForDoneGateTool(registerTool) {
|
|
|
249
249
|
timeoutMs: args.timeout_ms,
|
|
250
250
|
pollIntervalMs: args.poll_interval_ms,
|
|
251
251
|
worktreePath: args.worktree_path,
|
|
252
|
-
});
|
|
252
|
+
}, { resolveRunId: resolveDispatchRunIdForBinding });
|
|
253
253
|
return jsonResult({
|
|
254
254
|
gate_met: result.gate_met,
|
|
255
255
|
timed_out: result.timed_out,
|
|
@@ -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,
|