ai-whisper 0.2.1 → 0.3.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 +341 -101
- package/dist/bin/companion-agent.js +339 -99
- package/dist/bin/relay-monitor.js +339 -99
- package/dist/bin/whisper.js +373 -105
- 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 +3 -3
- 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
|
@@ -549,6 +549,100 @@ function getCollab(db, collabId2) {
|
|
|
549
549
|
});
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
553
|
+
function rowToRecord(row) {
|
|
554
|
+
return {
|
|
555
|
+
workflowId: row.workflow_id,
|
|
556
|
+
collabId: row.collab_id,
|
|
557
|
+
workflowType: row.workflow_type,
|
|
558
|
+
name: row.name,
|
|
559
|
+
specPath: row.spec_path,
|
|
560
|
+
roleBindings: JSON.parse(row.role_bindings),
|
|
561
|
+
status: row.status,
|
|
562
|
+
currentPhaseIndex: row.current_phase_index,
|
|
563
|
+
haltReason: row.halt_reason,
|
|
564
|
+
workflowContext: JSON.parse(row.workflow_context),
|
|
565
|
+
createdAt: row.created_at,
|
|
566
|
+
updatedAt: row.updated_at
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function insertWorkflow(db, input) {
|
|
570
|
+
db.prepare(`INSERT INTO workflows
|
|
571
|
+
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
572
|
+
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
573
|
+
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);
|
|
574
|
+
}
|
|
575
|
+
function getWorkflowById(db, workflowId) {
|
|
576
|
+
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
577
|
+
return row ? rowToRecord(row) : null;
|
|
578
|
+
}
|
|
579
|
+
function listWorkflows(db, filter = {}) {
|
|
580
|
+
const clauses = [];
|
|
581
|
+
const args = [];
|
|
582
|
+
if (filter.collabId) {
|
|
583
|
+
clauses.push("collab_id = ?");
|
|
584
|
+
args.push(filter.collabId);
|
|
585
|
+
}
|
|
586
|
+
if (filter.status) {
|
|
587
|
+
clauses.push("status = ?");
|
|
588
|
+
args.push(filter.status);
|
|
589
|
+
}
|
|
590
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
591
|
+
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
592
|
+
return rows.map(rowToRecord);
|
|
593
|
+
}
|
|
594
|
+
function setWorkflowStatus(db, input) {
|
|
595
|
+
db.prepare(`UPDATE workflows
|
|
596
|
+
SET status = ?, halt_reason = ?, updated_at = ?
|
|
597
|
+
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
598
|
+
}
|
|
599
|
+
function updateWorkflowContext(db, input) {
|
|
600
|
+
const existing = getWorkflowById(db, input.workflowId);
|
|
601
|
+
if (!existing) {
|
|
602
|
+
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
603
|
+
}
|
|
604
|
+
const merged = { ...existing.workflowContext, ...input.patch };
|
|
605
|
+
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
606
|
+
}
|
|
607
|
+
function incrementCurrentPhaseIndex(db, input) {
|
|
608
|
+
db.prepare(`UPDATE workflows
|
|
609
|
+
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
610
|
+
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
611
|
+
}
|
|
612
|
+
function countActiveWorkflowsForCollab(db, collabId2) {
|
|
613
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
|
|
614
|
+
return row.n;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ../broker/dist/runtime/workspace-snapshot.js
|
|
618
|
+
import { execFileSync } from "node:child_process";
|
|
619
|
+
function captureWorkspaceSnapshotSync(workspaceRoot) {
|
|
620
|
+
try {
|
|
621
|
+
const ref = execFileSync("git", ["-C", workspaceRoot, "stash", "create"], {
|
|
622
|
+
encoding: "utf8",
|
|
623
|
+
timeout: 1e4
|
|
624
|
+
}).trim();
|
|
625
|
+
if (ref)
|
|
626
|
+
return ref;
|
|
627
|
+
const head = execFileSync("git", ["-C", workspaceRoot, "rev-parse", "HEAD"], {
|
|
628
|
+
encoding: "utf8",
|
|
629
|
+
timeout: 1e4
|
|
630
|
+
}).trim();
|
|
631
|
+
return head || null;
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function diffChangedFilesSince(workspaceRoot, sinceRef) {
|
|
637
|
+
try {
|
|
638
|
+
const stdout = execFileSync("git", ["-C", workspaceRoot, "diff", "--name-only", sinceRef], { encoding: "utf8", timeout: 1e4 });
|
|
639
|
+
const files = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".ai-whisper/"));
|
|
640
|
+
return [...new Set(files)].sort();
|
|
641
|
+
} catch {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
552
646
|
// ../broker/dist/storage/repositories/reply-repository.js
|
|
553
647
|
function insertReply(db, reply) {
|
|
554
648
|
db.prepare(`INSERT INTO reply (
|
|
@@ -1014,7 +1108,7 @@ function upsertRelayTurnState(db, input) {
|
|
|
1014
1108
|
}
|
|
1015
1109
|
|
|
1016
1110
|
// ../broker/dist/storage/repositories/relay-capture-diagnostics-repository.js
|
|
1017
|
-
function
|
|
1111
|
+
function rowToRecord2(row) {
|
|
1018
1112
|
return {
|
|
1019
1113
|
captureId: row.capture_id,
|
|
1020
1114
|
handoffId: row.handoff_id,
|
|
@@ -1051,32 +1145,32 @@ function listCaptureDiagnosticsByCollab(db, collabId2, limit, opts) {
|
|
|
1051
1145
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1052
1146
|
WHERE collab_id = ?${filterClause}
|
|
1053
1147
|
ORDER BY created_at DESC`).all(collabId2, ...filterArgs);
|
|
1054
|
-
return rows2.map(
|
|
1148
|
+
return rows2.map(rowToRecord2);
|
|
1055
1149
|
}
|
|
1056
1150
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1057
1151
|
WHERE collab_id = ?${filterClause}
|
|
1058
1152
|
ORDER BY created_at DESC
|
|
1059
1153
|
LIMIT ?`).all(collabId2, ...filterArgs, limit);
|
|
1060
|
-
return rows.map(
|
|
1154
|
+
return rows.map(rowToRecord2);
|
|
1061
1155
|
}
|
|
1062
1156
|
function listCaptureDiagnosticsByCollabAndChain(db, collabId2, chainId, limit) {
|
|
1063
1157
|
if (limit === null) {
|
|
1064
1158
|
const rows2 = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1065
1159
|
WHERE collab_id = ? AND chain_id = ?
|
|
1066
1160
|
ORDER BY created_at DESC`).all(collabId2, chainId);
|
|
1067
|
-
return rows2.map(
|
|
1161
|
+
return rows2.map(rowToRecord2);
|
|
1068
1162
|
}
|
|
1069
1163
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1070
1164
|
WHERE collab_id = ? AND chain_id = ?
|
|
1071
1165
|
ORDER BY created_at DESC
|
|
1072
1166
|
LIMIT ?`).all(collabId2, chainId, limit);
|
|
1073
|
-
return rows.map(
|
|
1167
|
+
return rows.map(rowToRecord2);
|
|
1074
1168
|
}
|
|
1075
1169
|
function listCaptureDiagnosticsByHandoff(db, handoffId) {
|
|
1076
1170
|
const rows = db.prepare(`SELECT * FROM relay_capture_diagnostics
|
|
1077
1171
|
WHERE handoff_id = ?
|
|
1078
1172
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1079
|
-
return rows.map(
|
|
1173
|
+
return rows.map(rowToRecord2);
|
|
1080
1174
|
}
|
|
1081
1175
|
function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
1082
1176
|
const result = db.prepare("DELETE FROM relay_capture_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1084,7 +1178,7 @@ function deleteCaptureDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1084
1178
|
}
|
|
1085
1179
|
|
|
1086
1180
|
// ../broker/dist/storage/repositories/relay-evaluator-diagnostics-repository.js
|
|
1087
|
-
function
|
|
1181
|
+
function rowToRecord3(row) {
|
|
1088
1182
|
return {
|
|
1089
1183
|
evaluatorId: row.evaluator_id,
|
|
1090
1184
|
handoffId: row.handoff_id,
|
|
@@ -1129,32 +1223,32 @@ function listEvaluatorDiagnosticsByCollab(db, collabId2, limit, opts) {
|
|
|
1129
1223
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1130
1224
|
WHERE collab_id = ?${filterClause}
|
|
1131
1225
|
ORDER BY created_at DESC`).all(collabId2, ...filterArgs);
|
|
1132
|
-
return rows2.map(
|
|
1226
|
+
return rows2.map(rowToRecord3);
|
|
1133
1227
|
}
|
|
1134
1228
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1135
1229
|
WHERE collab_id = ?${filterClause}
|
|
1136
1230
|
ORDER BY created_at DESC
|
|
1137
1231
|
LIMIT ?`).all(collabId2, ...filterArgs, limit);
|
|
1138
|
-
return rows.map(
|
|
1232
|
+
return rows.map(rowToRecord3);
|
|
1139
1233
|
}
|
|
1140
1234
|
function listEvaluatorDiagnosticsByCollabAndChain(db, collabId2, chainId, limit) {
|
|
1141
1235
|
if (limit === null) {
|
|
1142
1236
|
const rows2 = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1143
1237
|
WHERE collab_id = ? AND chain_id = ?
|
|
1144
1238
|
ORDER BY created_at DESC`).all(collabId2, chainId);
|
|
1145
|
-
return rows2.map(
|
|
1239
|
+
return rows2.map(rowToRecord3);
|
|
1146
1240
|
}
|
|
1147
1241
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1148
1242
|
WHERE collab_id = ? AND chain_id = ?
|
|
1149
1243
|
ORDER BY created_at DESC
|
|
1150
1244
|
LIMIT ?`).all(collabId2, chainId, limit);
|
|
1151
|
-
return rows.map(
|
|
1245
|
+
return rows.map(rowToRecord3);
|
|
1152
1246
|
}
|
|
1153
1247
|
function listEvaluatorDiagnosticsByHandoff(db, handoffId) {
|
|
1154
1248
|
const rows = db.prepare(`SELECT * FROM relay_evaluator_diagnostics
|
|
1155
1249
|
WHERE handoff_id = ?
|
|
1156
1250
|
ORDER BY created_at ASC`).all(handoffId);
|
|
1157
|
-
return rows.map(
|
|
1251
|
+
return rows.map(rowToRecord3);
|
|
1158
1252
|
}
|
|
1159
1253
|
function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
1160
1254
|
const result = db.prepare("DELETE FROM relay_evaluator_diagnostics WHERE created_at < ?").run(cutoffIso);
|
|
@@ -1162,7 +1256,7 @@ function deleteEvaluatorDiagnosticsOlderThan(db, cutoffIso) {
|
|
|
1162
1256
|
}
|
|
1163
1257
|
|
|
1164
1258
|
// ../broker/dist/storage/repositories/relay-handoff-repository.js
|
|
1165
|
-
function
|
|
1259
|
+
function rowToRecord4(row) {
|
|
1166
1260
|
return {
|
|
1167
1261
|
handoffId: row.handoff_id,
|
|
1168
1262
|
collabId: row.collab_id,
|
|
@@ -1199,7 +1293,7 @@ function queryRelayHandoff(db, handoffId) {
|
|
|
1199
1293
|
FROM relay_handoff h
|
|
1200
1294
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1201
1295
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1202
|
-
return row ?
|
|
1296
|
+
return row ? rowToRecord4(row) : null;
|
|
1203
1297
|
}
|
|
1204
1298
|
function createRelayHandoffTxn(db, input) {
|
|
1205
1299
|
return db.transaction(() => {
|
|
@@ -1351,7 +1445,7 @@ function queryLatestHandedBackHandoff(db, collabId2) {
|
|
|
1351
1445
|
WHERE h.collab_id = ? AND h.status = 'handed_back'
|
|
1352
1446
|
ORDER BY h.resolved_at DESC
|
|
1353
1447
|
LIMIT 1`).get(collabId2);
|
|
1354
|
-
return row ?
|
|
1448
|
+
return row ? rowToRecord4(row) : null;
|
|
1355
1449
|
}
|
|
1356
1450
|
function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
1357
1451
|
db.transaction(() => {
|
|
@@ -1375,6 +1469,30 @@ function failRelayHandoffOnDisconnectTxn(db, input) {
|
|
|
1375
1469
|
}
|
|
1376
1470
|
})();
|
|
1377
1471
|
}
|
|
1472
|
+
function hasInFlightAcceptedHandoffForWorkflow(db, workflowId) {
|
|
1473
|
+
const row = db.prepare("SELECT COUNT(*) AS n FROM relay_handoff WHERE workflow_id = ? AND status = 'accepted'").get(workflowId);
|
|
1474
|
+
return row.n > 0;
|
|
1475
|
+
}
|
|
1476
|
+
function resetStrandedOrchestrationForWorkflow(db, workflowId) {
|
|
1477
|
+
const result = db.prepare(`UPDATE relay_handoff
|
|
1478
|
+
SET orchestrator_status = 'idle', orchestrator_claimed_at = NULL
|
|
1479
|
+
WHERE workflow_id = ?
|
|
1480
|
+
AND status = 'handed_back'
|
|
1481
|
+
AND orchestrator_status = 'pending'
|
|
1482
|
+
AND evaluator_verdict IS NULL`).run(workflowId);
|
|
1483
|
+
return result.changes;
|
|
1484
|
+
}
|
|
1485
|
+
function prependToPendingHandoffRequestText(db, input) {
|
|
1486
|
+
const row = db.prepare(`SELECT handoff_id, request_text FROM relay_handoff
|
|
1487
|
+
WHERE workflow_id = ? AND status = 'pending'
|
|
1488
|
+
ORDER BY created_at DESC LIMIT 1`).get(input.workflowId);
|
|
1489
|
+
if (!row)
|
|
1490
|
+
return false;
|
|
1491
|
+
db.prepare("UPDATE relay_handoff SET request_text = ?, last_activity_at = ? WHERE handoff_id = ?").run(`${input.prefix}
|
|
1492
|
+
|
|
1493
|
+
${row.request_text}`, input.now, row.handoff_id);
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1378
1496
|
function claimRelayHandoffForOrchestrationTxn(db, input) {
|
|
1379
1497
|
const result = db.prepare(`UPDATE relay_handoff
|
|
1380
1498
|
SET orchestrator_status = 'pending',
|
|
@@ -1393,10 +1511,12 @@ function listRelayHandoffsPendingOrchestration(db, collabId2) {
|
|
|
1393
1511
|
c.orchestrator_max_rounds AS max_rounds
|
|
1394
1512
|
FROM relay_handoff h
|
|
1395
1513
|
JOIN collab c ON c.collab_id = h.collab_id
|
|
1514
|
+
LEFT JOIN workflows w ON w.workflow_id = h.workflow_id
|
|
1396
1515
|
WHERE h.collab_id = ?
|
|
1397
1516
|
AND h.status = 'handed_back'
|
|
1398
|
-
AND h.orchestrator_status = 'idle'
|
|
1399
|
-
|
|
1517
|
+
AND h.orchestrator_status = 'idle'
|
|
1518
|
+
AND (w.status IS NULL OR w.status <> 'paused')`).all(collabId2);
|
|
1519
|
+
return rows.map(rowToRecord4);
|
|
1400
1520
|
}
|
|
1401
1521
|
function createLoopRelayHandoffTxn(db, input) {
|
|
1402
1522
|
return db.transaction(() => {
|
|
@@ -1592,7 +1712,7 @@ function getHandoffWithWorkflowMetaById(db, handoffId) {
|
|
|
1592
1712
|
WHERE h.handoff_id = ?`).get(handoffId);
|
|
1593
1713
|
if (!row)
|
|
1594
1714
|
return null;
|
|
1595
|
-
const base =
|
|
1715
|
+
const base = rowToRecord4(row);
|
|
1596
1716
|
return {
|
|
1597
1717
|
...base,
|
|
1598
1718
|
handoffStep: row.handoff_step,
|
|
@@ -1842,71 +1962,6 @@ function listRunCostRows(db, input) {
|
|
|
1842
1962
|
// ../broker/dist/control/workflow-control.js
|
|
1843
1963
|
import { randomUUID } from "node:crypto";
|
|
1844
1964
|
|
|
1845
|
-
// ../broker/dist/storage/repositories/workflow-repository.js
|
|
1846
|
-
function rowToRecord4(row) {
|
|
1847
|
-
return {
|
|
1848
|
-
workflowId: row.workflow_id,
|
|
1849
|
-
collabId: row.collab_id,
|
|
1850
|
-
workflowType: row.workflow_type,
|
|
1851
|
-
name: row.name,
|
|
1852
|
-
specPath: row.spec_path,
|
|
1853
|
-
roleBindings: JSON.parse(row.role_bindings),
|
|
1854
|
-
status: row.status,
|
|
1855
|
-
currentPhaseIndex: row.current_phase_index,
|
|
1856
|
-
haltReason: row.halt_reason,
|
|
1857
|
-
workflowContext: JSON.parse(row.workflow_context),
|
|
1858
|
-
createdAt: row.created_at,
|
|
1859
|
-
updatedAt: row.updated_at
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
function insertWorkflow(db, input) {
|
|
1863
|
-
db.prepare(`INSERT INTO workflows
|
|
1864
|
-
(workflow_id, collab_id, workflow_type, name, spec_path, role_bindings, status,
|
|
1865
|
-
current_phase_index, halt_reason, workflow_context, created_at, updated_at)
|
|
1866
|
-
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);
|
|
1867
|
-
}
|
|
1868
|
-
function getWorkflowById(db, workflowId) {
|
|
1869
|
-
const row = db.prepare("SELECT * FROM workflows WHERE workflow_id = ?").get(workflowId);
|
|
1870
|
-
return row ? rowToRecord4(row) : null;
|
|
1871
|
-
}
|
|
1872
|
-
function listWorkflows(db, filter = {}) {
|
|
1873
|
-
const clauses = [];
|
|
1874
|
-
const args = [];
|
|
1875
|
-
if (filter.collabId) {
|
|
1876
|
-
clauses.push("collab_id = ?");
|
|
1877
|
-
args.push(filter.collabId);
|
|
1878
|
-
}
|
|
1879
|
-
if (filter.status) {
|
|
1880
|
-
clauses.push("status = ?");
|
|
1881
|
-
args.push(filter.status);
|
|
1882
|
-
}
|
|
1883
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1884
|
-
const rows = db.prepare(`SELECT * FROM workflows ${where} ORDER BY created_at DESC`).all(...args);
|
|
1885
|
-
return rows.map(rowToRecord4);
|
|
1886
|
-
}
|
|
1887
|
-
function setWorkflowStatus(db, input) {
|
|
1888
|
-
db.prepare(`UPDATE workflows
|
|
1889
|
-
SET status = ?, halt_reason = ?, updated_at = ?
|
|
1890
|
-
WHERE workflow_id = ?`).run(input.status, input.haltReason, input.now, input.workflowId);
|
|
1891
|
-
}
|
|
1892
|
-
function updateWorkflowContext(db, input) {
|
|
1893
|
-
const existing = getWorkflowById(db, input.workflowId);
|
|
1894
|
-
if (!existing) {
|
|
1895
|
-
throw new Error(`updateWorkflowContext: unknown workflowId ${input.workflowId}`);
|
|
1896
|
-
}
|
|
1897
|
-
const merged = { ...existing.workflowContext, ...input.patch };
|
|
1898
|
-
db.prepare("UPDATE workflows SET workflow_context = ?, updated_at = ? WHERE workflow_id = ?").run(JSON.stringify(merged), input.now, input.workflowId);
|
|
1899
|
-
}
|
|
1900
|
-
function incrementCurrentPhaseIndex(db, input) {
|
|
1901
|
-
db.prepare(`UPDATE workflows
|
|
1902
|
-
SET current_phase_index = current_phase_index + 1, updated_at = ?
|
|
1903
|
-
WHERE workflow_id = ?`).run(input.now, input.workflowId);
|
|
1904
|
-
}
|
|
1905
|
-
function countRunningWorkflowsForCollab(db, collabId2) {
|
|
1906
|
-
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status = 'running'").get(collabId2);
|
|
1907
|
-
return row.n;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
1965
|
// ../broker/dist/storage/repositories/workflow-phase-repository.js
|
|
1911
1966
|
function rowToRecord5(row) {
|
|
1912
1967
|
return {
|
|
@@ -2027,6 +2082,14 @@ Findings: (omit this block entirely if none)
|
|
|
2027
2082
|
Non-blocking risks:
|
|
2028
2083
|
- <quality risk that does NOT block this gate, or "None.">
|
|
2029
2084
|
--- end protocol ---`;
|
|
2085
|
+
var WORKFLOW_OPERATOR_CONTROL = `--- ai-whisper operator control ---
|
|
2086
|
+
If the operator interrupts you and asks to pause the workflow (e.g. "pause the workflow, I need to fix X"):
|
|
2087
|
+
1. Find the active workflow id: run \`whisper workflow list\`.
|
|
2088
|
+
2. Run \`whisper workflow pause <id>\`.
|
|
2089
|
+
3. Acknowledge and STOP working \u2014 do not start the next change.
|
|
2090
|
+
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.
|
|
2091
|
+
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.
|
|
2092
|
+
--- end operator control ---`;
|
|
2030
2093
|
var WORKFLOW_DIAGNOSIS_PROTOCOL = `--- ai-whisper diagnosis review protocol ---
|
|
2031
2094
|
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.
|
|
2032
2095
|
|
|
@@ -2265,10 +2328,21 @@ var COMPLEX_BUG_FIXING = {
|
|
|
2265
2328
|
}
|
|
2266
2329
|
]
|
|
2267
2330
|
};
|
|
2331
|
+
function withOperatorControl(def) {
|
|
2332
|
+
return {
|
|
2333
|
+
...def,
|
|
2334
|
+
phases: def.phases.map((p) => ({
|
|
2335
|
+
...p,
|
|
2336
|
+
kickoffTemplate: `${p.kickoffTemplate}
|
|
2337
|
+
|
|
2338
|
+
${WORKFLOW_OPERATOR_CONTROL}`
|
|
2339
|
+
}))
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2268
2342
|
var REGISTRY = {
|
|
2269
|
-
[SPEC_DRIVEN_DEVELOPMENT.type]: SPEC_DRIVEN_DEVELOPMENT,
|
|
2270
|
-
[RALPH_LOOP.type]: RALPH_LOOP,
|
|
2271
|
-
[COMPLEX_BUG_FIXING.type]: COMPLEX_BUG_FIXING
|
|
2343
|
+
[SPEC_DRIVEN_DEVELOPMENT.type]: withOperatorControl(SPEC_DRIVEN_DEVELOPMENT),
|
|
2344
|
+
[RALPH_LOOP.type]: withOperatorControl(RALPH_LOOP),
|
|
2345
|
+
[COMPLEX_BUG_FIXING.type]: withOperatorControl(COMPLEX_BUG_FIXING)
|
|
2272
2346
|
};
|
|
2273
2347
|
function ralphRunDir(workspaceRoot, workflowId) {
|
|
2274
2348
|
return join(workspaceRoot, ".ai-whisper", "ralph", workflowId);
|
|
@@ -2324,6 +2398,23 @@ function safeDerivePlanPath(specPath, createdAt) {
|
|
|
2324
2398
|
return `${dir}${stem}.plan.md`;
|
|
2325
2399
|
}
|
|
2326
2400
|
}
|
|
2401
|
+
function composeResumeNotice(input) {
|
|
2402
|
+
const hasFiles = input.changedFiles.length > 0;
|
|
2403
|
+
const hasMessage = !!input.message && input.message.trim().length > 0;
|
|
2404
|
+
if (!hasFiles && !hasMessage)
|
|
2405
|
+
return null;
|
|
2406
|
+
const lines = [];
|
|
2407
|
+
if (hasFiles) {
|
|
2408
|
+
lines.push("While paused, the operator modified these files:");
|
|
2409
|
+
for (const f of input.changedFiles)
|
|
2410
|
+
lines.push(` - ${f}`);
|
|
2411
|
+
lines.push("Re-read them before continuing.");
|
|
2412
|
+
}
|
|
2413
|
+
if (hasMessage)
|
|
2414
|
+
lines.push(`Operator note: ${input.message.trim()}`);
|
|
2415
|
+
lines.push("Re-evaluate whether your current direction still holds; correct course before proceeding.");
|
|
2416
|
+
return lines.join("\n");
|
|
2417
|
+
}
|
|
2327
2418
|
function createWorkflowControl(deps) {
|
|
2328
2419
|
const { db, events } = deps;
|
|
2329
2420
|
function createWorkflow(input) {
|
|
@@ -2346,8 +2437,9 @@ function createWorkflowControl(deps) {
|
|
|
2346
2437
|
}
|
|
2347
2438
|
const workflowId = `wf_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2348
2439
|
const tx = db.transaction(() => {
|
|
2349
|
-
if (
|
|
2350
|
-
|
|
2440
|
+
if (countActiveWorkflowsForCollab(db, input.collabId) > 0) {
|
|
2441
|
+
const active = listWorkflows(db, { collabId: input.collabId }).find((w) => w.status === "running" || w.status === "paused");
|
|
2442
|
+
throw new Error(`another workflow is already active on this collab (${input.collabId})` + (active ? `: ${active.workflowId} (${active.status})` : ""));
|
|
2351
2443
|
}
|
|
2352
2444
|
insertWorkflow(db, {
|
|
2353
2445
|
workflowId,
|
|
@@ -2515,6 +2607,24 @@ function createWorkflowControl(deps) {
|
|
|
2515
2607
|
postmortemPath: bf.postmortemPath
|
|
2516
2608
|
});
|
|
2517
2609
|
}
|
|
2610
|
+
function consumeResumeNotice(input) {
|
|
2611
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
2612
|
+
const notice = wf?.workflowContext?.resumeNotice ?? null;
|
|
2613
|
+
if (notice) {
|
|
2614
|
+
updateWorkflowContext(db, {
|
|
2615
|
+
workflowId: input.workflowId,
|
|
2616
|
+
patch: { resumeNotice: null },
|
|
2617
|
+
now: input.now
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
return notice;
|
|
2621
|
+
}
|
|
2622
|
+
function withResumeNotice(workflowId, requestText, now) {
|
|
2623
|
+
const notice = consumeResumeNotice({ workflowId, now });
|
|
2624
|
+
return notice ? `${notice}
|
|
2625
|
+
|
|
2626
|
+
${requestText}` : requestText;
|
|
2627
|
+
}
|
|
2518
2628
|
function createContinuationHandoff(input) {
|
|
2519
2629
|
const handoffId = `ho_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2520
2630
|
insertWorkflowOwnedRelayHandoff(db, {
|
|
@@ -2522,7 +2632,9 @@ function createWorkflowControl(deps) {
|
|
|
2522
2632
|
collabId: input.workflow.collabId,
|
|
2523
2633
|
senderAgent: input.sender,
|
|
2524
2634
|
targetAgent: input.target,
|
|
2525
|
-
|
|
2635
|
+
// Prepend the one-time resume notice (if any) to the FIRST outgoing request
|
|
2636
|
+
// after a resume, then it is cleared — so the agent re-reads changed files.
|
|
2637
|
+
requestText: withResumeNotice(input.workflow.workflowId, input.requestText, input.now),
|
|
2526
2638
|
chainId: input.chain.chainId,
|
|
2527
2639
|
roundNumber: input.incrementRound ? input.chain.currentRound + 1 : input.chain.currentRound,
|
|
2528
2640
|
maxRounds: input.chain.maxRounds,
|
|
@@ -2582,7 +2694,9 @@ function createWorkflowControl(deps) {
|
|
|
2582
2694
|
collabId: input.workflow.collabId,
|
|
2583
2695
|
senderAgent: sender,
|
|
2584
2696
|
targetAgent: target,
|
|
2585
|
-
|
|
2697
|
+
// Prepend a pending resume notice (consumed once) so a phase-advance kickoff
|
|
2698
|
+
// after a resume also carries the operator's change notice.
|
|
2699
|
+
requestText: withResumeNotice(input.workflow.workflowId, kickoffText, input.now),
|
|
2586
2700
|
chainId,
|
|
2587
2701
|
roundNumber: 1,
|
|
2588
2702
|
maxRounds: phase.maxRounds,
|
|
@@ -3043,20 +3157,100 @@ ${findingsText}`;
|
|
|
3043
3157
|
reason: input.reason
|
|
3044
3158
|
});
|
|
3045
3159
|
}
|
|
3160
|
+
function maybeCaptureQuiesceSnapshot(input) {
|
|
3161
|
+
const wf = getWorkflowById(db, input.workflowId);
|
|
3162
|
+
if (!wf || wf.status !== "paused")
|
|
3163
|
+
return;
|
|
3164
|
+
if (wf.workflowContext.pauseSnapshotRef !== void 0) {
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
if (hasInFlightAcceptedHandoffForWorkflow(db, input.workflowId))
|
|
3168
|
+
return;
|
|
3169
|
+
const ref = deps.captureSnapshotRef ? deps.captureSnapshotRef(input.workflowId) : null;
|
|
3170
|
+
updateWorkflowContext(db, {
|
|
3171
|
+
workflowId: input.workflowId,
|
|
3172
|
+
patch: { pauseSnapshotRef: ref },
|
|
3173
|
+
now: input.now
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
function pauseWorkflow(input) {
|
|
3177
|
+
const workflow = getWorkflowById(db, input.workflowId);
|
|
3178
|
+
if (!workflow) {
|
|
3179
|
+
throw new Error(`pauseWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3180
|
+
}
|
|
3181
|
+
if (workflow.status !== "running") {
|
|
3182
|
+
throw new Error(`pauseWorkflow: workflow ${input.workflowId} is ${workflow.status}, only running workflows can be paused`);
|
|
3183
|
+
}
|
|
3184
|
+
const tx = db.transaction(() => {
|
|
3185
|
+
const current = getWorkflowById(db, input.workflowId);
|
|
3186
|
+
if (!current || current.status !== "running")
|
|
3187
|
+
return;
|
|
3188
|
+
setWorkflowStatus(db, {
|
|
3189
|
+
workflowId: input.workflowId,
|
|
3190
|
+
status: "paused",
|
|
3191
|
+
haltReason: null,
|
|
3192
|
+
now: input.now
|
|
3193
|
+
});
|
|
3194
|
+
updateWorkflowContext(db, {
|
|
3195
|
+
workflowId: input.workflowId,
|
|
3196
|
+
patch: { pausedAt: input.now },
|
|
3197
|
+
now: input.now
|
|
3198
|
+
});
|
|
3199
|
+
});
|
|
3200
|
+
tx.immediate();
|
|
3201
|
+
events.emit("workflow.paused", { workflowId: input.workflowId });
|
|
3202
|
+
maybeCaptureQuiesceSnapshot({ workflowId: input.workflowId, now: input.now });
|
|
3203
|
+
}
|
|
3204
|
+
function resumeHaltedWorkflow(workflow, now) {
|
|
3205
|
+
const tx = db.transaction(() => {
|
|
3206
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== workflow.workflowId && (w.status === "running" || w.status === "paused"));
|
|
3207
|
+
if (others.length > 0) {
|
|
3208
|
+
throw new Error(`resumeWorkflow: another workflow is already active on collab ${workflow.collabId}`);
|
|
3209
|
+
}
|
|
3210
|
+
setWorkflowStatus(db, {
|
|
3211
|
+
workflowId: workflow.workflowId,
|
|
3212
|
+
status: "running",
|
|
3213
|
+
haltReason: null,
|
|
3214
|
+
now
|
|
3215
|
+
});
|
|
3216
|
+
});
|
|
3217
|
+
tx.immediate();
|
|
3218
|
+
events.emit("workflow.resumed", {
|
|
3219
|
+
workflowId: workflow.workflowId,
|
|
3220
|
+
phaseIndex: workflow.currentPhaseIndex
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3046
3223
|
function resumeWorkflow(input) {
|
|
3047
3224
|
const workflow = getWorkflowById(db, input.workflowId);
|
|
3048
3225
|
if (!workflow) {
|
|
3049
3226
|
throw new Error(`resumeWorkflow: unknown workflowId ${input.workflowId}`);
|
|
3050
3227
|
}
|
|
3051
|
-
if (workflow.status === "
|
|
3052
|
-
|
|
3228
|
+
if (workflow.status === "halted") {
|
|
3229
|
+
resumeHaltedWorkflow(workflow, input.now);
|
|
3230
|
+
return;
|
|
3053
3231
|
}
|
|
3054
|
-
if (workflow.status !== "
|
|
3055
|
-
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is
|
|
3232
|
+
if (workflow.status !== "paused") {
|
|
3233
|
+
throw new Error(`resumeWorkflow: workflow ${input.workflowId} is ${workflow.status}, only paused or halted workflows can be resumed`);
|
|
3056
3234
|
}
|
|
3235
|
+
const ref = workflow.workflowContext.pauseSnapshotRef ?? null;
|
|
3236
|
+
const changedFiles = ref && deps.diffChangedFilesSinceSnapshot ? deps.diffChangedFilesSinceSnapshot(input.workflowId, ref) : [];
|
|
3237
|
+
const notice = composeResumeNotice({
|
|
3238
|
+
changedFiles,
|
|
3239
|
+
message: input.message ?? null
|
|
3240
|
+
});
|
|
3057
3241
|
const tx = db.transaction(() => {
|
|
3058
|
-
|
|
3059
|
-
|
|
3242
|
+
const others = listWorkflows(db, { collabId: workflow.collabId }).filter((w) => w.workflowId !== input.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
|
+
resetStrandedOrchestrationForWorkflow(db, input.workflowId);
|
|
3247
|
+
let bakedIntoPending = false;
|
|
3248
|
+
if (notice) {
|
|
3249
|
+
bakedIntoPending = prependToPendingHandoffRequestText(db, {
|
|
3250
|
+
workflowId: input.workflowId,
|
|
3251
|
+
prefix: notice,
|
|
3252
|
+
now: input.now
|
|
3253
|
+
});
|
|
3060
3254
|
}
|
|
3061
3255
|
setWorkflowStatus(db, {
|
|
3062
3256
|
workflowId: input.workflowId,
|
|
@@ -3064,6 +3258,15 @@ ${findingsText}`;
|
|
|
3064
3258
|
haltReason: null,
|
|
3065
3259
|
now: input.now
|
|
3066
3260
|
});
|
|
3261
|
+
updateWorkflowContext(db, {
|
|
3262
|
+
workflowId: input.workflowId,
|
|
3263
|
+
patch: {
|
|
3264
|
+
resumeNotice: bakedIntoPending ? null : notice,
|
|
3265
|
+
pausedAt: null,
|
|
3266
|
+
pauseSnapshotRef: null
|
|
3267
|
+
},
|
|
3268
|
+
now: input.now
|
|
3269
|
+
});
|
|
3067
3270
|
});
|
|
3068
3271
|
tx.immediate();
|
|
3069
3272
|
events.emit("workflow.resumed", {
|
|
@@ -3164,6 +3367,9 @@ ${findingsText}`;
|
|
|
3164
3367
|
beginPhaseRun,
|
|
3165
3368
|
applyOrchestratorVerdict,
|
|
3166
3369
|
haltWorkflow,
|
|
3370
|
+
pauseWorkflow,
|
|
3371
|
+
maybeCaptureQuiesceSnapshot,
|
|
3372
|
+
consumeResumeNotice,
|
|
3167
3373
|
resumeWorkflow,
|
|
3168
3374
|
cancelWorkflow,
|
|
3169
3375
|
getHandoffWithWorkflowMeta,
|
|
@@ -3179,8 +3385,32 @@ function buildEventId(kind, subjectId, timestamp) {
|
|
|
3179
3385
|
return `evt_${kind}_${subjectId}_${normalizeTimestampForEventId(timestamp)}`;
|
|
3180
3386
|
}
|
|
3181
3387
|
function createControlService(db, events) {
|
|
3182
|
-
const
|
|
3388
|
+
const resolveWorkspaceRoot = (workflowId) => {
|
|
3389
|
+
const wf = getWorkflowById(db, workflowId);
|
|
3390
|
+
if (!wf)
|
|
3391
|
+
return null;
|
|
3392
|
+
return getCollab(db, wf.collabId)?.workspaceRoot ?? null;
|
|
3393
|
+
};
|
|
3394
|
+
const workflowControl = createWorkflowControl({
|
|
3395
|
+
db,
|
|
3396
|
+
events,
|
|
3397
|
+
captureSnapshotRef: (workflowId) => {
|
|
3398
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3399
|
+
return root ? captureWorkspaceSnapshotSync(root) : null;
|
|
3400
|
+
},
|
|
3401
|
+
diffChangedFilesSinceSnapshot: (workflowId, sinceRef) => {
|
|
3402
|
+
const root = resolveWorkspaceRoot(workflowId);
|
|
3403
|
+
return root ? diffChangedFilesSince(root, sinceRef) : [];
|
|
3404
|
+
}
|
|
3405
|
+
});
|
|
3406
|
+
const isWorkflowDeliverySuspended = (handoffId) => {
|
|
3407
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(handoffId);
|
|
3408
|
+
if (!meta?.workflowId)
|
|
3409
|
+
return false;
|
|
3410
|
+
return workflowControl.getWorkflow(meta.workflowId)?.status === "paused";
|
|
3411
|
+
};
|
|
3183
3412
|
return Object.assign({
|
|
3413
|
+
isWorkflowDeliverySuspended,
|
|
3184
3414
|
startCollab(input) {
|
|
3185
3415
|
const collab2 = collabSchema.parse({
|
|
3186
3416
|
version: 1,
|
|
@@ -3808,6 +4038,8 @@ function createControlService(db, events) {
|
|
|
3808
4038
|
return createRelayHandoffTxn(db, input);
|
|
3809
4039
|
},
|
|
3810
4040
|
acceptRelayHandoff(input) {
|
|
4041
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4042
|
+
return;
|
|
3811
4043
|
return acceptRelayHandoffTxn(db, input);
|
|
3812
4044
|
},
|
|
3813
4045
|
deferRelayHandoff(input) {
|
|
@@ -3820,7 +4052,12 @@ function createControlService(db, events) {
|
|
|
3820
4052
|
return markRelayHandoffStaleTxn(db, input);
|
|
3821
4053
|
},
|
|
3822
4054
|
handoffBackRelay(input) {
|
|
3823
|
-
|
|
4055
|
+
const result = handoffBackRelayTxn(db, input);
|
|
4056
|
+
const meta = workflowControl.getHandoffWithWorkflowMeta(input.handoffId);
|
|
4057
|
+
if (meta?.workflowId && workflowControl.getWorkflow(meta.workflowId)?.status === "paused") {
|
|
4058
|
+
workflowControl.maybeCaptureQuiesceSnapshot({ workflowId: meta.workflowId, now: input.now });
|
|
4059
|
+
}
|
|
4060
|
+
return result;
|
|
3824
4061
|
},
|
|
3825
4062
|
failRelayHandoffOnDisconnect(input) {
|
|
3826
4063
|
return failRelayHandoffOnDisconnectTxn(db, input);
|
|
@@ -3832,6 +4069,8 @@ function createControlService(db, events) {
|
|
|
3832
4069
|
return queryLatestHandedBackHandoff(db, collabId2);
|
|
3833
4070
|
},
|
|
3834
4071
|
claimRelayHandoffForOrchestration(input) {
|
|
4072
|
+
if (isWorkflowDeliverySuspended(input.handoffId))
|
|
4073
|
+
return null;
|
|
3835
4074
|
return claimRelayHandoffForOrchestrationTxn(db, input);
|
|
3836
4075
|
},
|
|
3837
4076
|
listRelayHandoffsPendingOrchestration(collabId2) {
|
|
@@ -4285,8 +4524,9 @@ CREATE TABLE IF NOT EXISTS workflows (
|
|
|
4285
4524
|
updated_at TEXT NOT NULL
|
|
4286
4525
|
);
|
|
4287
4526
|
|
|
4527
|
+
DROP INDEX IF EXISTS workflows_one_running_per_collab;
|
|
4288
4528
|
CREATE UNIQUE INDEX IF NOT EXISTS workflows_one_running_per_collab
|
|
4289
|
-
ON workflows(collab_id) WHERE status
|
|
4529
|
+
ON workflows(collab_id) WHERE status IN ('running', 'paused');
|
|
4290
4530
|
|
|
4291
4531
|
CREATE TABLE IF NOT EXISTS workflow_phases (
|
|
4292
4532
|
phase_run_id TEXT PRIMARY KEY,
|
|
@@ -5820,7 +6060,7 @@ ${JSON.stringify(event.payload)}`;
|
|
|
5820
6060
|
}
|
|
5821
6061
|
|
|
5822
6062
|
// src/runtime/process-start-time.ts
|
|
5823
|
-
import { execFileSync } from "node:child_process";
|
|
6063
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5824
6064
|
import { readFileSync } from "node:fs";
|
|
5825
6065
|
function readProcessStartTime(pid) {
|
|
5826
6066
|
if (process.platform === "linux") {
|
|
@@ -5834,7 +6074,7 @@ function readProcessStartTime(pid) {
|
|
|
5834
6074
|
}
|
|
5835
6075
|
if (process.platform === "darwin") {
|
|
5836
6076
|
try {
|
|
5837
|
-
const out =
|
|
6077
|
+
const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
5838
6078
|
encoding: "utf8"
|
|
5839
6079
|
});
|
|
5840
6080
|
const trimmed = out.trim();
|