ai-whisper 0.2.1 → 0.4.0
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/dist/bin/broker-daemon.js +346 -103
- package/dist/bin/companion-agent.js +344 -101
- package/dist/bin/relay-monitor.js +352 -104
- package/dist/bin/whisper.js +7942 -6626
- package/dist/skills/ai-whisper-bugfix/SKILL.md +18 -0
- package/dist/skills/ai-whisper-ralph/SKILL.md +18 -0
- package/dist/skills/ai-whisper-sdd/SKILL.md +18 -0
- package/package.json +2 -2
- package/skills/ai-whisper-bugfix/SKILL.md +18 -0
- package/skills/ai-whisper-ralph/SKILL.md +18 -0
- package/skills/ai-whisper-sdd/SKILL.md +18 -0
|
@@ -582,6 +582,100 @@ function getCollab(db, collabId) {
|
|
|
582
582
|
});
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
586
|
+
function rowToRecord(row) {
|
|
587
|
+
return {
|
|
588
|
+
workflowId: row.workflow_id,
|
|
589
|
+
collabId: row.collab_id,
|
|
590
|
+
workflowType: row.workflow_type,
|
|
591
|
+
name: row.name,
|
|
592
|
+
specPath: row.spec_path,
|
|
593
|
+
roleBindings: JSON.parse(row.role_bindings),
|
|
594
|
+
status: row.status,
|
|
595
|
+
currentPhaseIndex: row.current_phase_index,
|
|
596
|
+
haltReason: row.halt_reason,
|
|
597
|
+
workflowContext: JSON.parse(row.workflow_context),
|
|
598
|
+
createdAt: row.created_at,
|
|
599
|
+
updatedAt: row.updated_at
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function insertWorkflow(db, input) {
|
|
603
|
+
db.prepare(`INSERT INTO workflows
|
|
604
|
+
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
605
|
+
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
606
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(input.workflowId, input.collabId, input.workflowType, input.name, input.specPath, JSON.stringify(input.roleBindings), input.status, input.currentPhaseIndex, JSON.stringify(input.workflowContext), input.now, input.now);
|
|
607
|
+
}
|
|
608
|
+
function getWorkflowById(db, workflowId) {
|
|
609
|
+
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
610
|
+
return row ? rowToRecord(row) : null;
|
|
611
|
+
}
|
|
612
|
+
function listWorkflows(db, filter = {}) {
|
|
613
|
+
const clauses = [];
|
|
614
|
+
const args = [];
|
|
615
|
+
if (filter.collabId) {
|
|
616
|
+
clauses.push("collab_id = ?");
|
|
617
|
+
args.push(filter.collabId);
|
|
618
|
+
}
|
|
619
|
+
if (filter.status) {
|
|
620
|
+
clauses.push("status = ?");
|
|
621
|
+
args.push(filter.status);
|
|
622
|
+
}
|
|
623
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
624
|
+
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
625
|
+
return rows.map(rowToRecord);
|
|
626
|
+
}
|
|
627
|
+
function setWorkflowStatus(db, input) {
|
|
628
|
+
db.prepare(`UPDATE workflows
|
|
629
|
+
SET status = ?, halt_reason = ?, updated_at = ?
|
|
630
|
+
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
631
|
+
}
|
|
632
|
+
function updateWorkflowContext(db, input) {
|
|
633
|
+
const existing = getWorkflowById(db, input.workflowId);
|
|
634
|
+
if (!existing) {
|
|
635
|
+
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
636
|
+
}
|
|
637
|
+
const merged = { ...existing.workflowContext, ...input.patch };
|
|
638
|
+
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
639
|
+
}
|
|
640
|
+
function incrementCurrentPhaseIndex(db, input) {
|
|
641
|
+
db.prepare(`UPDATE workflows
|
|
642
|
+
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
643
|
+
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
644
|
+
}
|
|
645
|
+
function countActiveWorkflowsForCollab(db, collabId) {
|
|
646
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
|
|
647
|
+
return row.n;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ../broker/dist/runtime/workspace-snapshot.js
|
|
651
|
+
import { execFileSync } from "node:child_process";
|
|
652
|
+
function captureWorkspaceSnapshotSync(workspaceRoot) {
|
|
653
|
+
try {
|
|
654
|
+
const ref = execFileSync("git", ["-C", workspaceRoot, "stash", "create"], {
|
|
655
|
+
encoding: "utf8",
|
|
656
|
+
timeout: 1e4
|
|
657
|
+
}).trim();
|
|
658
|
+
if (ref)
|
|
659
|
+
return ref;
|
|
660
|
+
const head = execFileSync("git", ["-C", workspaceRoot, "rev-parse", "HEAD"], {
|
|
661
|
+
encoding: "utf8",
|
|
662
|
+
timeout: 1e4
|
|
663
|
+
}).trim();
|
|
664
|
+
return head || null;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function diffChangedFilesSince(workspaceRoot, sinceRef) {
|
|
670
|
+
try {
|
|
671
|
+
const stdout = execFileSync("git", ["-C", workspaceRoot, "diff", "--name-only", sinceRef], { encoding: "utf8", timeout: 1e4 });
|
|
672
|
+
const files = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".ai-whisper/"));
|
|
673
|
+
return [...new Set(files)].sort();
|
|
674
|
+
} catch {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
585
679
|
// ../broker/dist/storage/repositories/reply-repository.js
|
|
586
680
|
function insertReply(db, reply) {
|
|
587
681
|
db.prepare(`INSERT INTO reply (
|
|
@@ -1047,7 +1141,7 @@ function upsertRelayTurnState(db, input) {
|
|
|
1047
1141
|
}
|
|
1048
1142
|
|
|
1049
1143
|
// ../broker/dist/storage/repositories/relay-capture-diagnostics-repository.js
|
|
1050
|
-
function
|
|
1144
|
+
function rowToRecord2(row) {
|
|
1051
1145
|
return {
|
|
1052
1146
|
captureId: row.capture_id,
|
|
1053
1147
|
handoffId: row.handoff_id,
|
|
@@ -1084,32 +1178,32 @@ function listCaptureDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
|
1084
1178
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1085
1179
|
WHERE collab_id = ?${filterClause}
|
|
1086
1180
|
ORDER BY created_at DESC`).all(collabId, ...filterArgs);
|
|
1087
|
-
return rows2.map(
|
|
1181
|
+
return rows2.map(rowToRecord2);
|
|
1088
1182
|
}
|
|
1089
1183
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1090
1184
|
WHERE collab_id = ?${filterClause}
|
|
1091
1185
|
ORDER BY created_at DESC
|
|
1092
1186
|
LIMIT ?`).all(collabId, ...filterArgs, limit);
|
|
1093
|
-
return rows.map(
|
|
1187
|
+
return rows.map(rowToRecord2);
|
|
1094
1188
|
}
|
|
1095
1189
|
function listCaptureDiagnosticsByCollabAndChain(db, collabId, chainId, limit) {
|
|
1096
1190
|
if (limit === null) {
|
|
1097
1191
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1098
1192
|
WHERE collab_id = ? AND chain_id = ?
|
|
1099
1193
|
ORDER BY created_at DESC`).all(collabId, chainId);
|
|
1100
|
-
return rows2.map(
|
|
1194
|
+
return rows2.map(rowToRecord2);
|
|
1101
1195
|
}
|
|
1102
1196
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1103
1197
|
WHERE collab_id = ? AND chain_id = ?
|
|
1104
1198
|
ORDER BY created_at DESC
|
|
1105
1199
|
LIMIT ?`).all(collabId, chainId, limit);
|
|
1106
|
-
return rows.map(
|
|
1200
|
+
return rows.map(rowToRecord2);
|
|
1107
1201
|
}
|
|
1108
1202
|
function listCaptureDiagnosticsByHandoff(db, handoffId) {
|
|
1109
1203
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1110
1204
|
WHERE handoff_id = ?
|
|
1111
1205
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1112
|
-
return rows.map(
|
|
1206
|
+
return rows.map(rowToRecord2);
|
|
1113
1207
|
}
|
|
1114
1208
|
function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
1115
1209
|
const result = db.prepare("DELETE FROM relay_capture_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1117,7 +1211,7 @@ function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1117
1211
|
}
|
|
1118
1212
|
|
|
1119
1213
|
// ../broker/dist/storage/repositories/relay-evaluator-diagnostics-repository.js
|
|
1120
|
-
function
|
|
1214
|
+
function rowToRecord3(row) {
|
|
1121
1215
|
return {
|
|
1122
1216
|
evaluatorId: row.evaluator_id,
|
|
1123
1217
|
handoffId: row.handoff_id,
|
|
@@ -1162,32 +1256,32 @@ function listEvaluatorDiagnosticsByCollab(db, collabId, limit, opts) {
|
|
|
1162
1256
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1163
1257
|
WHERE collab_id = ?${filterClause}
|
|
1164
1258
|
ORDER BY created_at DESC`).all(collabId, ...filterArgs);
|
|
1165
|
-
return rows2.map(
|
|
1259
|
+
return rows2.map(rowToRecord3);
|
|
1166
1260
|
}
|
|
1167
1261
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1168
1262
|
WHERE collab_id = ?${filterClause}
|
|
1169
1263
|
ORDER BY created_at DESC
|
|
1170
1264
|
LIMIT ?`).all(collabId, ...filterArgs, limit);
|
|
1171
|
-
return rows.map(
|
|
1265
|
+
return rows.map(rowToRecord3);
|
|
1172
1266
|
}
|
|
1173
1267
|
function listEvaluatorDiagnosticsByCollabAndChain(db, collabId, chainId, limit) {
|
|
1174
1268
|
if (limit === null) {
|
|
1175
1269
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1176
1270
|
WHERE collab_id = ? AND chain_id = ?
|
|
1177
1271
|
ORDER BY created_at DESC`).all(collabId, chainId);
|
|
1178
|
-
return rows2.map(
|
|
1272
|
+
return rows2.map(rowToRecord3);
|
|
1179
1273
|
}
|
|
1180
1274
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1181
1275
|
WHERE collab_id = ? AND chain_id = ?
|
|
1182
1276
|
ORDER BY created_at DESC
|
|
1183
1277
|
LIMIT ?`).all(collabId, chainId, limit);
|
|
1184
|
-
return rows.map(
|
|
1278
|
+
return rows.map(rowToRecord3);
|
|
1185
1279
|
}
|
|
1186
1280
|
function listEvaluatorDiagnosticsByHandoff(db, handoffId) {
|
|
1187
1281
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1188
1282
|
WHERE handoff_id = ?
|
|
1189
1283
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1190
|
-
return rows.map(
|
|
1284
|
+
return rows.map(rowToRecord3);
|
|
1191
1285
|
}
|
|
1192
1286
|
function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
1193
1287
|
const result = db.prepare("DELETE FROM relay_evaluator_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1195,7 +1289,7 @@ function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1195
1289
|
}
|
|
1196
1290
|
|
|
1197
1291
|
// ../broker/dist/storage/repositories/relay-handoff-repository.js
|
|
1198
|
-
function
|
|
1292
|
+
function rowToRecord4(row) {
|
|
1199
1293
|
return {
|
|
1200
1294
|
handoffId: row.handoff_id,
|
|
1201
1295
|
collabId: row.collab_id,
|
|
@@ -1232,7 +1326,7 @@ function queryRelayHandoff(db, handoffId) {
|
|
|
1232
1326
|
FROM relay_handoff h
|
|
1233
1327
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1234
1328
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1235
|
-
return row ?
|
|
1329
|
+
return row ? rowToRecord4(row) : null;
|
|
1236
1330
|
}
|
|
1237
1331
|
function createRelayHandoffTxn(db, input) {
|
|
1238
1332
|
return db.transaction(() => {
|
|
@@ -1384,7 +1478,7 @@ function queryLatestHandedBackHandoff(db, collabId) {
|
|
|
1384
1478
|
WHERE h.collab_id = ? AND h.status = 'handed_back'
|
|
1385
1479
|
ORDER BY h.resolved_at DESC
|
|
1386
1480
|
LIMIT 1`).get(collabId);
|
|
1387
|
-
return row ?
|
|
1481
|
+
return row ? rowToRecord4(row) : null;
|
|
1388
1482
|
}
|
|
1389
1483
|
function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
1390
1484
|
db.transaction(() => {
|
|
@@ -1408,6 +1502,30 @@ function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
|
1408
1502
|
}
|
|
1409
1503
|
})();
|
|
1410
1504
|
}
|
|
1505
|
+
function hasInFlightAcceptedHandoffForWorkflow(db, workflowId) {
|
|
1506
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM relay_handoff WHERE workflow_id = ? AND status = 'accepted'").get(workflowId);
|
|
1507
|
+
return row.n > 0;
|
|
1508
|
+
}
|
|
1509
|
+
function resetStrandedOrchestrationForWorkflow(db, workflowId) {
|
|
1510
|
+
const result = db.prepare(`UPDATE relay_handoff
|
|
1511
|
+
SET orchestrator_status = 'idle', orchestrator_claimed_at = NULL
|
|
1512
|
+
WHERE workflow_id = ?
|
|
1513
|
+
AND status = 'handed_back'
|
|
1514
|
+
AND orchestrator_status = 'pending'
|
|
1515
|
+
AND evaluator_verdict IS NULL`).run(workflowId);
|
|
1516
|
+
return result.changes;
|
|
1517
|
+
}
|
|
1518
|
+
function prependToPendingHandoffRequestText(db, input) {
|
|
1519
|
+
const row = db.prepare(`SELECT handoff_id, request_text FROM relay_handoff
|
|
1520
|
+
WHERE workflow_id = ? AND status = 'pending'
|
|
1521
|
+
ORDER BY created_at DESC LIMIT 1`).get(input.workflowId);
|
|
1522
|
+
if (!row)
|
|
1523
|
+
return false;
|
|
1524
|
+
db.prepare("UPDATE relay_handoff SET request_text = ?, last_activity_at = ? WHERE handoff_id = ?").run(`${input.prefix}
|
|
1525
|
+
|
|
1526
|
+
${row.request_text}`, input.now, row.handoff_id);
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1411
1529
|
function claimRelayHandoffForOrchestrationTxn(db, input) {
|
|
1412
1530
|
const result = db.prepare(`UPDATE relay_handoff
|
|
1413
1531
|
SET orchestrator_status = 'pending',
|
|
@@ -1426,10 +1544,12 @@ function listRelayHandoffsPendingOrchestration(db, collabId) {
|
|
|
1426
1544
|
c.orchestrator_max_rounds AS max_rounds
|
|
1427
1545
|
FROM relay_handoff h
|
|
1428
1546
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1547
|
+
LEFT JOIN workflows w ON w.workflow_id = h.workflow_id
|
|
1429
1548
|
WHERE h.collab_id = ?
|
|
1430
1549
|
AND h.status = 'handed_back'
|
|
1431
|
-
AND h.orchestrator_status = 'idle'
|
|
1432
|
-
|
|
1550
|
+
AND h.orchestrator_status = 'idle'
|
|
1551
|
+
AND (w.status IS NULL OR w.status <> 'paused')`).all(collabId);
|
|
1552
|
+
return rows.map(rowToRecord4);
|
|
1433
1553
|
}
|
|
1434
1554
|
function createLoopRelayHandoffTxn(db, input) {
|
|
1435
1555
|
return db.transaction(() => {
|
|
@@ -1625,7 +1745,7 @@ function getHandoffWithWorkflowMetaById(db, handoffId) {
|
|
|
1625
1745
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1626
1746
|
if (!row)
|
|
1627
1747
|
return null;
|
|
1628
|
-
const base =
|
|
1748
|
+
const base = rowToRecord4(row);
|
|
1629
1749
|
return {
|
|
1630
1750
|
...base,
|
|
1631
1751
|
handoffStep: row.handoff_step,
|
|
@@ -1727,7 +1847,8 @@ function cleanupOrchestrationOnShutdownTxn(db, input) {
|
|
|
1727
1847
|
import { basename } from "node:path";
|
|
1728
1848
|
function listActiveCollabSummaries(db, input) {
|
|
1729
1849
|
const nowMs = Date.parse(input.now ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
1730
|
-
const
|
|
1850
|
+
const base = Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
1851
|
+
const cutoff = new Date(Math.max(0, base - input.sinceMs)).toISOString();
|
|
1731
1852
|
const eligible = db.prepare(`SELECT c.collab_id AS collabId,
|
|
1732
1853
|
COALESCE(MAX(h.last_activity_at), '') AS lastAct
|
|
1733
1854
|
FROM collab c
|
|
@@ -1766,7 +1887,8 @@ function buildCollabSummary(db, collabId) {
|
|
|
1766
1887
|
const collab = db.prepare(`SELECT display_name AS displayName, workspace_root AS workspaceRoot
|
|
1767
1888
|
FROM collab WHERE collab_id = ?`).get(e.collabId);
|
|
1768
1889
|
const wf = db.prepare(`SELECT workflow_id AS workflowId, workflow_type AS workflowType,
|
|
1769
|
-
name, status, current_phase_index AS currentPhaseIndex
|
|
1890
|
+
name, status, current_phase_index AS currentPhaseIndex,
|
|
1891
|
+
created_at AS createdAt
|
|
1770
1892
|
FROM workflows WHERE collab_id = ?
|
|
1771
1893
|
ORDER BY (status = 'running') DESC, created_at DESC
|
|
1772
1894
|
LIMIT 1`).get(e.collabId);
|
|
@@ -1834,6 +1956,7 @@ function buildCollabSummary(db, collabId) {
|
|
|
1834
1956
|
handoffState: turn?.handoffState ?? "idle"
|
|
1835
1957
|
},
|
|
1836
1958
|
sessions,
|
|
1959
|
+
workflowCreatedAt: wf?.createdAt ?? null,
|
|
1837
1960
|
lastActivityAt: runLastAct
|
|
1838
1961
|
};
|
|
1839
1962
|
}
|
|
@@ -1875,71 +1998,6 @@ function listRunCostRows(db, input) {
|
|
|
1875
1998
|
// ../broker/dist/control/workflow-control.js
|
|
1876
1999
|
import { randomUUID } from "node:crypto";
|
|
1877
2000
|
|
|
1878
|
-
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
1879
|
-
function rowToRecord4(row) {
|
|
1880
|
-
return {
|
|
1881
|
-
workflowId: row.workflow_id,
|
|
1882
|
-
collabId: row.collab_id,
|
|
1883
|
-
workflowType: row.workflow_type,
|
|
1884
|
-
name: row.name,
|
|
1885
|
-
specPath: row.spec_path,
|
|
1886
|
-
roleBindings: JSON.parse(row.role_bindings),
|
|
1887
|
-
status: row.status,
|
|
1888
|
-
currentPhaseIndex: row.current_phase_index,
|
|
1889
|
-
haltReason: row.halt_reason,
|
|
1890
|
-
workflowContext: JSON.parse(row.workflow_context),
|
|
1891
|
-
createdAt: row.created_at,
|
|
1892
|
-
updatedAt: row.updated_at
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
function insertWorkflow(db, input) {
|
|
1896
|
-
db.prepare(`INSERT INTO workflows
|
|
1897
|
-
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
1898
|
-
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
1899
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`).run(input.workflowId, input.collabId, input.workflowType, input.name, input.specPath, JSON.stringify(input.roleBindings), input.status, input.currentPhaseIndex, JSON.stringify(input.workflowContext), input.now, input.now);
|
|
1900
|
-
}
|
|
1901
|
-
function getWorkflowById(db, workflowId) {
|
|
1902
|
-
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
1903
|
-
return row ? rowToRecord4(row) : null;
|
|
1904
|
-
}
|
|
1905
|
-
function listWorkflows(db, filter = {}) {
|
|
1906
|
-
const clauses = [];
|
|
1907
|
-
const args = [];
|
|
1908
|
-
if (filter.collabId) {
|
|
1909
|
-
clauses.push("collab_id = ?");
|
|
1910
|
-
args.push(filter.collabId);
|
|
1911
|
-
}
|
|
1912
|
-
if (filter.status) {
|
|
1913
|
-
clauses.push("status = ?");
|
|
1914
|
-
args.push(filter.status);
|
|
1915
|
-
}
|
|
1916
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1917
|
-
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
1918
|
-
return rows.map(rowToRecord4);
|
|
1919
|
-
}
|
|
1920
|
-
function setWorkflowStatus(db, input) {
|
|
1921
|
-
db.prepare(`UPDATE workflows
|
|
1922
|
-
SET status = ?, halt_reason = ?, updated_at = ?
|
|
1923
|
-
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
1924
|
-
}
|
|
1925
|
-
function updateWorkflowContext(db, input) {
|
|
1926
|
-
const existing = getWorkflowById(db, input.workflowId);
|
|
1927
|
-
if (!existing) {
|
|
1928
|
-
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
1929
|
-
}
|
|
1930
|
-
const merged = { ...existing.workflowContext, ...input.patch };
|
|
1931
|
-
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
1932
|
-
}
|
|
1933
|
-
function incrementCurrentPhaseIndex(db, input) {
|
|
1934
|
-
db.prepare(`UPDATE workflows
|
|
1935
|
-
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
1936
|
-
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
1937
|
-
}
|
|
1938
|
-
function countRunningWorkflowsForCollab(db, collabId) {
|
|
1939
|
-
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status = 'running'").get(collabId);
|
|
1940
|
-
return row.n;
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
2001
|
// ../broker/dist/storage/repositories/workflow-phase-repository.js
|
|
1944
2002
|
function rowToRecord5(row) {
|
|
1945
2003
|
return {
|
|
@@ -2060,6 +2118,14 @@ Findings: (omit this block entirely if none)
|
|
|
2060
2118
|
Non-blocking risks:
|
|
2061
2119
|
- <quality risk that does NOT block this gate, or "None.">
|
|
2062
2120
|
--- end protocol ---`;
|
|
2121
|
+
var WORKFLOW_OPERATOR_CONTROL = `--- ai-whisper operator control ---
|
|
2122
|
+
If the operator interrupts you and asks to pause the workflow (e.g. "pause the workflow, I need to fix X"):
|
|
2123
|
+
1. Find the active workflow id: run \`whisper workflow list\`.
|
|
2124
|
+
2. Run \`whisper workflow pause <id>\`.
|
|
2125
|
+
3. Acknowledge and STOP working \u2014 do not start the next change.
|
|
2126
|
+
The operator will edit artifacts and later run \`whisper workflow resume <id> [--message "..."]\`; you will then receive a notice of what changed and must re-read those files before continuing.
|
|
2127
|
+
Provider note: the Codex CLI EXITS its session on Ctrl+C at an idle prompt (a mid-task Ctrl+C only interrupts the running task). The operator typically interrupts you while you are BUSY before issuing this instruction; do not assume Ctrl+C is a safe no-op.
|
|
2128
|
+
--- end operator control ---`;
|
|
2063
2129
|
var WORKFLOW_DIAGNOSIS_PROTOCOL = `--- ai-whisper diagnosis review protocol ---
|
|
2064
2130
|
You are the gatekeeper for an ai-whisper complex-bug-fixing DIAGNOSIS gate. No human is in the loop. Do NOT trust the implementer's diagnosis \u2014 verify it yourself. The gate stays shut until YOU independently agree on the root cause and that the fix is net-safe.
|
|
2065
2131
|
|
|
@@ -2298,10 +2364,21 @@ var COMPLEX_BUG_FIXING = {
|
|
|
2298
2364
|
}
|
|
2299
2365
|
]
|
|
2300
2366
|
};
|
|
2367
|
+
function withOperatorControl(def) {
|
|
2368
|
+
return {
|
|
2369
|
+
...def,
|
|
2370
|
+
phases: def.phases.map((p) => ({
|
|
2371
|
+
...p,
|
|
2372
|
+
kickoffTemplate: `${p.kickoffTemplate}
|
|
2373
|
+
|
|
2374
|
+
${WORKFLOW_OPERATOR_CONTROL}`
|
|
2375
|
+
}))
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2301
2378
|
var REGISTRY = {
|
|
2302
|
-
[SPEC_DRIVEN_DEVELOPMENT.type]: SPEC_DRIVEN_DEVELOPMENT,
|
|
2303
|
-
[RALPH_LOOP.type]: RALPH_LOOP,
|
|
2304
|
-
[COMPLEX_BUG_FIXING.type]: COMPLEX_BUG_FIXING
|
|
2379
|
+
[SPEC_DRIVEN_DEVELOPMENT.type]: withOperatorControl(SPEC_DRIVEN_DEVELOPMENT),
|
|
2380
|
+
[RALPH_LOOP.type]: withOperatorControl(RALPH_LOOP),
|
|
2381
|
+
[COMPLEX_BUG_FIXING.type]: withOperatorControl(COMPLEX_BUG_FIXING)
|
|
2305
2382
|
};
|
|
2306
2383
|
function ralphRunDir(workspaceRoot, workflowId) {
|
|
2307
2384
|
return join(workspaceRoot, ".ai-whisper", "ralph", workflowId);
|
|
@@ -2357,6 +2434,23 @@ function safeDerivePlanPath(specPath, createdAt) {
|
|
|
2357
2434
|
return `${dir}${stem}.plan.md`;
|
|
2358
2435
|
}
|
|
2359
2436
|
}
|
|
2437
|
+
function composeResumeNotice(input) {
|
|
2438
|
+
const hasFiles = input.changedFiles.length > 0;
|
|
2439
|
+
const hasMessage = !!input.message && input.message.trim().length > 0;
|
|
2440
|
+
if (!hasFiles && !hasMessage)
|
|
2441
|
+
return null;
|
|
2442
|
+
const lines = [];
|
|
2443
|
+
if (hasFiles) {
|
|
2444
|
+
lines.push("While paused, the operator modified these files:");
|
|
2445
|
+
for (const f of input.changedFiles)
|
|
2446
|
+
lines.push(` - ${f}`);
|
|
2447
|
+
lines.push("Re-read them before continuing.");
|
|
2448
|
+
}
|
|
2449
|
+
if (hasMessage)
|
|
2450
|
+
lines.push(`Operator note: ${input.message.trim()}`);
|
|
2451
|
+
lines.push("Re-evaluate whether your current direction still holds; correct course before proceeding.");
|
|
2452
|
+
return lines.join("\n");
|
|
2453
|
+
}
|
|
2360
2454
|
function createWorkflowControl(deps) {
|
|
2361
2455
|
const { db, events } = deps;
|
|
2362
2456
|
function createWorkflow(input) {
|
|
@@ -2379,8 +2473,9 @@ function createWorkflowControl(deps) {
|
|
|
2379
2473
|
}
|
|
2380
2474
|
const workflowId = `wf_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2381
2475
|
const tx = db.transaction(() => {
|
|
2382
|
-
if (
|
|
2383
|
-
|
|
2476
|
+
if (countActiveWorkflowsForCollab(db, input.collabId) > 0) {
|
|
2477
|
+
const active = listWorkflows(db, { collabId: input.collabId }).find((w) => w.status === "running" || w.status === "paused");
|
|
2478
|
+
throw new Error(`another workflow is already active on this collab (${input.collabId})` + (active ? `: ${active.workflowId} (${active.status})` : ""));
|
|
2384
2479
|
}
|
|
2385
2480
|
insertWorkflow(db, {
|
|
2386
2481
|
workflowId,
|
|
@@ -2548,6 +2643,24 @@ function createWorkflowControl(deps) {
|
|
|
2548
2643
|
postmortemPath: bf.postmortemPath
|
|
2549
2644
|
});
|
|
2550
2645
|
}
|
|
2646
|
+
function consumeResumeNotice(input) {
|
|
2647
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
2648
|
+
const notice = wf?.workflowContext?.resumeNotice ?? null;
|
|
2649
|
+
if (notice) {
|
|
2650
|
+
updateWorkflowContext(db, {
|
|
2651
|
+
workflowId: input.workflowId,
|
|
2652
|
+
patch: { resumeNotice: null },
|
|
2653
|
+
now: input.now
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
return notice;
|
|
2657
|
+
}
|
|
2658
|
+
function withResumeNotice(workflowId, requestText, now) {
|
|
2659
|
+
const notice = consumeResumeNotice({ workflowId, now });
|
|
2660
|
+
return notice ? `${notice}
|
|
2661
|
+
|
|
2662
|
+
${requestText}` : requestText;
|
|
2663
|
+
}
|
|
2551
2664
|
function createContinuationHandoff(input) {
|
|
2552
2665
|
const handoffId = `ho_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2553
2666
|
insertWorkflowOwnedRelayHandoff(db, {
|
|
@@ -2555,7 +2668,9 @@ function createWorkflowControl(deps) {
|
|
|
2555
2668
|
collabId: input.workflow.collabId,
|
|
2556
2669
|
senderAgent: input.sender,
|
|
2557
2670
|
targetAgent: input.target,
|
|
2558
|
-
|
|
2671
|
+
// Prepend the one-time resume notice (if any) to the FIRST outgoing request
|
|
2672
|
+
// after a resume, then it is cleared — so the agent re-reads changed files.
|
|
2673
|
+
requestText: withResumeNotice(input.workflow.workflowId, input.requestText, input.now),
|
|
2559
2674
|
chainId: input.chain.chainId,
|
|
2560
2675
|
roundNumber: input.incrementRound ? input.chain.currentRound + 1 : input.chain.currentRound,
|
|
2561
2676
|
maxRounds: input.chain.maxRounds,
|
|
@@ -2615,7 +2730,9 @@ function createWorkflowControl(deps) {
|
|
|
2615
2730
|
collabId: input.workflow.collabId,
|
|
2616
2731
|
senderAgent: sender,
|
|
2617
2732
|
targetAgent: target,
|
|
2618
|
-
|
|
2733
|
+
// Prepend a pending resume notice (consumed once) so a phase-advance kickoff
|
|
2734
|
+
// after a resume also carries the operator's change notice.
|
|
2735
|
+
requestText: withResumeNotice(input.workflow.workflowId, kickoffText, input.now),
|
|
2619
2736
|
chainId,
|
|
2620
2737
|
roundNumber: 1,
|
|
2621
2738
|
maxRounds: phase.maxRounds,
|
|
@@ -3076,20 +3193,100 @@ ${findingsText}`;
|
|
|
3076
3193
|
reason: input.reason
|
|
3077
3194
|
});
|
|
3078
3195
|
}
|
|
3196
|
+
function maybeCaptureQuiesceSnapshot(input) {
|
|
3197
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
3198
|
+
if (!wf || wf.status !== "paused")
|
|
3199
|
+
return;
|
|
3200
|
+
if (wf.workflowContext.pauseSnapshotRef !== void 0) {
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
if (hasInFlightAcceptedHandoffForWorkflow(db, input.workflowId))
|
|
3204
|
+
return;
|
|
3205
|
+
const ref = deps.captureSnapshotRef ? deps.captureSnapshotRef(input.workflowId) : null;
|
|
3206
|
+
updateWorkflowContext(db, {
|
|
3207
|
+
workflowId: input.workflowId,
|
|
3208
|
+
patch: { pauseSnapshotRef: ref },
|
|
3209
|
+
now: input.now
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
function pauseWorkflow(input) {
|
|
3213
|
+
const workflow = getWorkflowById(db, input.workflowId);
|
|
3214
|
+
if (!workflow) {
|
|
3215
|
+
throw new Error(`pauseWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3216
|
+
}
|
|
3217
|
+
if (workflow.status !== "running") {
|
|
3218
|
+
throw new Error(`pauseWorkflow: workflow ${input.workflowId} is ${workflow.status}, only running workflows can be paused`);
|
|
3219
|
+
}
|
|
3220
|
+
const tx = db.transaction(() => {
|
|
3221
|
+
const current = getWorkflowById(db, input.workflowId);
|
|
3222
|
+
if (!current || current.status !== "running")
|
|
3223
|
+
return;
|
|
3224
|
+
setWorkflowStatus(db, {
|
|
3225
|
+
workflowId: input.workflowId,
|
|
3226
|
+
status: "paused",
|
|
3227
|
+
haltReason: null,
|
|
3228
|
+
now: input.now
|
|
3229
|
+
});
|
|
3230
|
+
updateWorkflowContext(db, {
|
|
3231
|
+
workflowId: input.workflowId,
|
|
3232
|
+
patch: { pausedAt: input.now },
|
|
3233
|
+
now: input.now
|
|
3234
|
+
});
|
|
3235
|
+
});
|
|
3236
|
+
tx.immediate();
|
|
3237
|
+
events.emit("workflow.paused", { workflowId: input.workflowId });
|
|
3238
|
+
maybeCaptureQuiesceSnapshot({ workflowId: input.workflowId, now: input.now });
|
|
3239
|
+
}
|
|
3240
|
+
function resumeHaltedWorkflow(workflow, now) {
|
|
3241
|
+
const tx = db.transaction(() => {
|
|
3242
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== workflow.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3243
|
+
if (others.length > 0) {
|
|
3244
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3245
|
+
}
|
|
3246
|
+
setWorkflowStatus(db, {
|
|
3247
|
+
workflowId: workflow.workflowId,
|
|
3248
|
+
status: "running",
|
|
3249
|
+
haltReason: null,
|
|
3250
|
+
now
|
|
3251
|
+
});
|
|
3252
|
+
});
|
|
3253
|
+
tx.immediate();
|
|
3254
|
+
events.emit("workflow.resumed", {
|
|
3255
|
+
workflowId: workflow.workflowId,
|
|
3256
|
+
phaseIndex: workflow.currentPhaseIndex
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3079
3259
|
function resumeWorkflow(input) {
|
|
3080
3260
|
const workflow = getWorkflowById(db, input.workflowId);
|
|
3081
3261
|
if (!workflow) {
|
|
3082
3262
|
throw new Error(`resumeWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3083
3263
|
}
|
|
3084
|
-
if (workflow.status === "
|
|
3085
|
-
|
|
3264
|
+
if (workflow.status === "halted") {
|
|
3265
|
+
resumeHaltedWorkflow(workflow, input.now);
|
|
3266
|
+
return;
|
|
3086
3267
|
}
|
|
3087
|
-
if (workflow.status !== "
|
|
3088
|
-
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is
|
|
3268
|
+
if (workflow.status !== "paused") {
|
|
3269
|
+
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is ${workflow.status}, only paused or halted workflows can be resumed`);
|
|
3089
3270
|
}
|
|
3271
|
+
const ref = workflow.workflowContext.pauseSnapshotRef ?? null;
|
|
3272
|
+
const changedFiles = ref && deps.diffChangedFilesSinceSnapshot ? deps.diffChangedFilesSinceSnapshot(input.workflowId, ref) : [];
|
|
3273
|
+
const notice = composeResumeNotice({
|
|
3274
|
+
changedFiles,
|
|
3275
|
+
message: input.message ?? null
|
|
3276
|
+
});
|
|
3090
3277
|
const tx = db.transaction(() => {
|
|
3091
|
-
|
|
3092
|
-
|
|
3278
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== input.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3279
|
+
if (others.length > 0) {
|
|
3280
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3281
|
+
}
|
|
3282
|
+
resetStrandedOrchestrationForWorkflow(db, input.workflowId);
|
|
3283
|
+
let bakedIntoPending = false;
|
|
3284
|
+
if (notice) {
|
|
3285
|
+
bakedIntoPending = prependToPendingHandoffRequestText(db, {
|
|
3286
|
+
workflowId: input.workflowId,
|
|
3287
|
+
prefix: notice,
|
|
3288
|
+
now: input.now
|
|
3289
|
+
});
|
|
3093
3290
|
}
|
|
3094
3291
|
setWorkflowStatus(db, {
|
|
3095
3292
|
workflowId: input.workflowId,
|
|
@@ -3097,6 +3294,15 @@ ${findingsText}`;
|
|
|
3097
3294
|
haltReason: null,
|
|
3098
3295
|
now: input.now
|
|
3099
3296
|
});
|
|
3297
|
+
updateWorkflowContext(db, {
|
|
3298
|
+
workflowId: input.workflowId,
|
|
3299
|
+
patch: {
|
|
3300
|
+
resumeNotice: bakedIntoPending ? null : notice,
|
|
3301
|
+
pausedAt: null,
|
|
3302
|
+
pauseSnapshotRef: null
|
|
3303
|
+
},
|
|
3304
|
+
now: input.now
|
|
3305
|
+
});
|
|
3100
3306
|
});
|
|
3101
3307
|
tx.immediate();
|
|
3102
3308
|
events.emit("workflow.resumed", {
|
|
@@ -3197,6 +3403,9 @@ ${findingsText}`;
|
|
|
3197
3403
|
beginPhaseRun,
|
|
3198
3404
|
applyOrchestratorVerdict,
|
|
3199
3405
|
haltWorkflow,
|
|
3406
|
+
pauseWorkflow,
|
|
3407
|
+
maybeCaptureQuiesceSnapshot,
|
|
3408
|
+
consumeResumeNotice,
|
|
3200
3409
|
resumeWorkflow,
|
|
3201
3410
|
cancelWorkflow,
|
|
3202
3411
|
getHandoffWithWorkflowMeta,
|
|
@@ -3212,8 +3421,32 @@ function buildEventId(kind, subjectId, timestamp) {
|
|
|
3212
3421
|
return `evt_${kind}_${subjectId}_${normalizeTimestampForEventId(timestamp)}`;
|
|
3213
3422
|
}
|
|
3214
3423
|
function createControlService(db, events) {
|
|
3215
|
-
const
|
|
3424
|
+
const resolveWorkspaceRoot = (workflowId) => {
|
|
3425
|
+
const wf = getWorkflowById(db, workflowId);
|
|
3426
|
+
if (!wf)
|
|
3427
|
+
return null;
|
|
3428
|
+
return getCollab(db, wf.collabId)?.workspaceRoot ?? null;
|
|
3429
|
+
};
|
|
3430
|
+
const workflowControl = createWorkflowControl({
|
|
3431
|
+
db,
|
|
3432
|
+
events,
|
|
3433
|
+
captureSnapshotRef: (workflowId) => {
|
|
3434
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3435
|
+
return root ? captureWorkspaceSnapshotSync(root) : null;
|
|
3436
|
+
},
|
|
3437
|
+
diffChangedFilesSinceSnapshot: (workflowId, sinceRef) => {
|
|
3438
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3439
|
+
return root ? diffChangedFilesSince(root, sinceRef) : [];
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
const isWorkflowDeliverySuspended = (handoffId) => {
|
|
3443
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(handoffId);
|
|
3444
|
+
if (!meta?.workflowId)
|
|
3445
|
+
return false;
|
|
3446
|
+
return workflowControl.getWorkflow(meta.workflowId)?.status === "paused";
|
|
3447
|
+
};
|
|
3216
3448
|
return Object.assign({
|
|
3449
|
+
isWorkflowDeliverySuspended,
|
|
3217
3450
|
startCollab(input) {
|
|
3218
3451
|
const collab = collabSchema.parse({
|
|
3219
3452
|
version: 1,
|
|
@@ -3841,6 +4074,8 @@ function createControlService(db, events) {
|
|
|
3841
4074
|
return createRelayHandoffTxn(db, input);
|
|
3842
4075
|
},
|
|
3843
4076
|
acceptRelayHandoff(input) {
|
|
4077
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4078
|
+
return;
|
|
3844
4079
|
return acceptRelayHandoffTxn(db, input);
|
|
3845
4080
|
},
|
|
3846
4081
|
deferRelayHandoff(input) {
|
|
@@ -3853,7 +4088,12 @@ function createControlService(db, events) {
|
|
|
3853
4088
|
return markRelayHandoffStaleTxn(db, input);
|
|
3854
4089
|
},
|
|
3855
4090
|
handoffBackRelay(input) {
|
|
3856
|
-
|
|
4091
|
+
const result = handoffBackRelayTxn(db, input);
|
|
4092
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(input.handoffId);
|
|
4093
|
+
if (meta?.workflowId && workflowControl.getWorkflow(meta.workflowId)?.status === "paused") {
|
|
4094
|
+
workflowControl.maybeCaptureQuiesceSnapshot({ workflowId: meta.workflowId, now: input.now });
|
|
4095
|
+
}
|
|
4096
|
+
return result;
|
|
3857
4097
|
},
|
|
3858
4098
|
failRelayHandoffOnDisconnect(input) {
|
|
3859
4099
|
return failRelayHandoffOnDisconnectTxn(db, input);
|
|
@@ -3865,6 +4105,8 @@ function createControlService(db, events) {
|
|
|
3865
4105
|
return queryLatestHandedBackHandoff(db, collabId);
|
|
3866
4106
|
},
|
|
3867
4107
|
claimRelayHandoffForOrchestration(input) {
|
|
4108
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4109
|
+
return null;
|
|
3868
4110
|
return claimRelayHandoffForOrchestrationTxn(db, input);
|
|
3869
4111
|
},
|
|
3870
4112
|
listRelayHandoffsPendingOrchestration(collabId) {
|
|
@@ -4318,8 +4560,9 @@ CREATE TABLE IF NOT EXISTS workflows (
|
|
|
4318
4560
|
updated_at TEXT NOT NULL
|
|
4319
4561
|
);
|
|
4320
4562
|
|
|
4563
|
+
DROP INDEX IF EXISTS workflows_one_running_per_collab;
|
|
4321
4564
|
CREATE UNIQUE INDEX IF NOT EXISTS workflows_one_running_per_collab
|
|
4322
|
-
ON workflows(collab_id) WHERE status
|
|
4565
|
+
ON workflows(collab_id) WHERE status IN ('running', 'paused');
|
|
4323
4566
|
|
|
4324
4567
|
CREATE TABLE IF NOT EXISTS workflow_phases (
|
|
4325
4568
|
phase_run_id TEXT PRIMARY KEY,
|