@open-code-review/cli 1.11.0 → 2.1.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/README.md +10 -4
- package/dist/dashboard/client/assets/{_basePickBy-D8RU9s_y.js → _basePickBy-B3ALyupE.js} +1 -1
- package/dist/dashboard/client/assets/{_baseUniq-CjVeYx1J.js → _baseUniq-b2RALAWc.js} +1 -1
- package/dist/dashboard/client/assets/{arc-DsFstmf9.js → arc-DcSVvhUd.js} +1 -1
- package/dist/dashboard/client/assets/{architectureDiagram-VXUJARFQ-iNJB-g1N.js → architectureDiagram-VXUJARFQ-BNUlmSCS.js} +1 -1
- package/dist/dashboard/client/assets/{blockDiagram-VD42YOAC-Zp2Aw0zR.js → blockDiagram-VD42YOAC-BmhiQVwa.js} +1 -1
- package/dist/dashboard/client/assets/{c4Diagram-YG6GDRKO-BGppUmwT.js → c4Diagram-YG6GDRKO-jyJ3WOv5.js} +1 -1
- package/dist/dashboard/client/assets/channel-D3J8-GF_.js +1 -0
- package/dist/dashboard/client/assets/{chunk-4BX2VUAB-CZcRxeE4.js → chunk-4BX2VUAB-x1dQU_s3.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-55IACEB6-CVdL59yY.js → chunk-55IACEB6-CwbsE2XQ.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-B4BG7PRW-CFPp6g6e.js → chunk-B4BG7PRW-BaE7c-ti.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-DI55MBZ5-DH9BzE6I.js → chunk-DI55MBZ5-Bw5PUaMK.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-FMBD7UC4-DZ2DTwqS.js → chunk-FMBD7UC4-B7cF6P3s.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QN33PNHL-DODPm0CR.js → chunk-QN33PNHL-OY4evNHd.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-QZHKN3VN-CNI_LxUf.js → chunk-QZHKN3VN-BpjQwIWz.js} +1 -1
- package/dist/dashboard/client/assets/{chunk-TZMSLE5B-sxZQF02c.js → chunk-TZMSLE5B-D8b_Oq9B.js} +1 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-tkFUL-1Y.js +1 -0
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-tkFUL-1Y.js +1 -0
- package/dist/dashboard/client/assets/clone-CkY5ajLr.js +1 -0
- package/dist/dashboard/client/assets/{cose-bilkent-S5V4N54A-BHa2lABH.js → cose-bilkent-S5V4N54A-C-sfP8PN.js} +1 -1
- package/dist/dashboard/client/assets/{dagre-6UL2VRFP-CvCLBtkz.js → dagre-6UL2VRFP-Cqfo0NRg.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-PSM6KHXK-Cklwd4YA.js → diagram-PSM6KHXK-BR3ppxqI.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-QEK2KX5R-3bDERTbp.js → diagram-QEK2KX5R-Dvcx6x3R.js} +1 -1
- package/dist/dashboard/client/assets/{diagram-S2PKOQOG-DbiWlPc6.js → diagram-S2PKOQOG-DoyBLnVN.js} +1 -1
- package/dist/dashboard/client/assets/{erDiagram-Q2GNP2WA-BQa_VNbt.js → erDiagram-Q2GNP2WA-hy77l1cL.js} +1 -1
- package/dist/dashboard/client/assets/{flowDiagram-NV44I4VS-BDaJyl9N.js → flowDiagram-NV44I4VS-Bz0B1rKM.js} +1 -1
- package/dist/dashboard/client/assets/{ganttDiagram-JELNMOA3-DsTnleSr.js → ganttDiagram-JELNMOA3-CLgrZPoC.js} +1 -1
- package/dist/dashboard/client/assets/{gitGraphDiagram-V2S2FVAM-BRuBadgn.js → gitGraphDiagram-V2S2FVAM-DwJ-1f-v.js} +1 -1
- package/dist/dashboard/client/assets/{graph-CYYqXm9c.js → graph-DDBMM_t2.js} +1 -1
- package/dist/dashboard/client/assets/{index-eZMoytob.js → index-Cr9yEo_B.js} +123 -123
- package/dist/dashboard/client/assets/{infoDiagram-HS3SLOUP-CHnA8k7H.js → infoDiagram-HS3SLOUP-Bhn1FmAk.js} +1 -1
- package/dist/dashboard/client/assets/{journeyDiagram-XKPGCS4Q-CAXR1-Ju.js → journeyDiagram-XKPGCS4Q-CzGbjX1y.js} +1 -1
- package/dist/dashboard/client/assets/{kanban-definition-3W4ZIXB7-Clf3HfHz.js → kanban-definition-3W4ZIXB7-Da77-WYk.js} +1 -1
- package/dist/dashboard/client/assets/{layout-DQPaNqnO.js → layout-CVwSB-GS.js} +1 -1
- package/dist/dashboard/client/assets/{linear-qUnNXvWB.js → linear-CTRAc5Jn.js} +1 -1
- package/dist/dashboard/client/assets/{mermaid-renderer-C7Se8vjl.js → mermaid-renderer-Bjo170ax.js} +4 -4
- package/dist/dashboard/client/assets/{mindmap-definition-VGOIOE7T-DBIdG0OR.js → mindmap-definition-VGOIOE7T-B55C2odl.js} +1 -1
- package/dist/dashboard/client/assets/{pieDiagram-ADFJNKIX-DXAIiG6W.js → pieDiagram-ADFJNKIX-5lrQLrSz.js} +1 -1
- package/dist/dashboard/client/assets/{quadrantDiagram-AYHSOK5B-D4yAxif0.js → quadrantDiagram-AYHSOK5B-Bg55gC30.js} +1 -1
- package/dist/dashboard/client/assets/{requirementDiagram-UZGBJVZJ-D27ME1VO.js → requirementDiagram-UZGBJVZJ-CyR4YFJY.js} +1 -1
- package/dist/dashboard/client/assets/{sankeyDiagram-TZEHDZUN-BeEaA_QM.js → sankeyDiagram-TZEHDZUN-BVWKr9_-.js} +1 -1
- package/dist/dashboard/client/assets/{sequenceDiagram-WL72ISMW-GTI12qU0.js → sequenceDiagram-WL72ISMW-D0AJg_tE.js} +1 -1
- package/dist/dashboard/client/assets/{stateDiagram-FKZM4ZOC-ClSoeZM0.js → stateDiagram-FKZM4ZOC-BuHpTgim.js} +1 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-DwAPhteN.js +1 -0
- package/dist/dashboard/client/assets/{timeline-definition-IT6M3QCI-cj5d_Kyh.js → timeline-definition-IT6M3QCI-LDhpAmDd.js} +1 -1
- package/dist/dashboard/client/assets/{treemap-GDKQZRPO-BrRT1igb.js → treemap-GDKQZRPO-Dd4gjvUl.js} +1 -1
- package/dist/dashboard/client/assets/{xychartDiagram-PRI3JC2R-DlzGitHh.js → xychartDiagram-PRI3JC2R-B9RDod39.js} +1 -1
- package/dist/dashboard/client/index.html +1 -1
- package/dist/dashboard/server.js +1232 -656
- package/dist/index.js +1905 -711
- package/dist/lib/db/index.js +794 -100
- package/package.json +3 -5
- package/dist/dashboard/client/assets/channel-C8plpfdz.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-2ON5EDUG-Dqn6u1oQ.js +0 -1
- package/dist/dashboard/client/assets/classDiagram-v2-WZHVMYZB-Dqn6u1oQ.js +0 -1
- package/dist/dashboard/client/assets/clone-BQ8hOLqM.js +0 -1
- package/dist/dashboard/client/assets/stateDiagram-v2-4FDKWEC3-Bim3s-dq.js +0 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
39
39
|
mod
|
|
40
40
|
));
|
|
41
41
|
|
|
42
|
+
// src/lib/runtime-checks.ts
|
|
43
|
+
function isSupportedNode(version) {
|
|
44
|
+
const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
45
|
+
return major > NODE_FLOOR.major || major === NODE_FLOOR.major && minor >= NODE_FLOOR.minor;
|
|
46
|
+
}
|
|
47
|
+
function nodeVersionGuardMessage(version) {
|
|
48
|
+
return `
|
|
49
|
+
Open Code Review requires Node.js >= ${NODE_FLOOR.major}.${NODE_FLOOR.minor} (it uses Node's built-in SQLite, \`node:sqlite\`).
|
|
50
|
+
You have Node ${version}. Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and re-run.
|
|
51
|
+
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
function isSuppressibleSqliteWarning(warning) {
|
|
55
|
+
const message = typeof warning === "string" ? warning : warning?.message;
|
|
56
|
+
return typeof message === "string" && message.includes("SQLite is an experimental feature");
|
|
57
|
+
}
|
|
58
|
+
var NODE_FLOOR;
|
|
59
|
+
var init_runtime_checks = __esm({
|
|
60
|
+
"src/lib/runtime-checks.ts"() {
|
|
61
|
+
"use strict";
|
|
62
|
+
NODE_FLOOR = { major: 22, minor: 5 };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
42
66
|
// ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
|
|
43
67
|
var require_error = __commonJS({
|
|
44
68
|
"../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js"(exports) {
|
|
@@ -23319,7 +23343,195 @@ var init_result_mapper = __esm({
|
|
|
23319
23343
|
}
|
|
23320
23344
|
});
|
|
23321
23345
|
|
|
23346
|
+
// src/lib/db/engine.ts
|
|
23347
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
23348
|
+
function applyEnginePreconditions() {
|
|
23349
|
+
if (_preconditionsApplied) return;
|
|
23350
|
+
_preconditionsApplied = true;
|
|
23351
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
23352
|
+
process.emitWarning = (warning, ...args) => {
|
|
23353
|
+
if (isSuppressibleSqliteWarning(warning)) return;
|
|
23354
|
+
originalEmitWarning(warning, ...args);
|
|
23355
|
+
};
|
|
23356
|
+
}
|
|
23357
|
+
function newDatabase(path2) {
|
|
23358
|
+
if (!_DatabaseSyncCtor) {
|
|
23359
|
+
applyEnginePreconditions();
|
|
23360
|
+
try {
|
|
23361
|
+
_DatabaseSyncCtor = nodeRequire("node:sqlite").DatabaseSync;
|
|
23362
|
+
} catch (e) {
|
|
23363
|
+
if (!isSupportedNode(process.versions.node)) {
|
|
23364
|
+
throw new Error(nodeVersionGuardMessage(process.versions.node).trim());
|
|
23365
|
+
}
|
|
23366
|
+
throw e;
|
|
23367
|
+
}
|
|
23368
|
+
}
|
|
23369
|
+
return new _DatabaseSyncCtor(path2);
|
|
23370
|
+
}
|
|
23371
|
+
function isBusyError(e) {
|
|
23372
|
+
const errcode = e?.errcode;
|
|
23373
|
+
return errcode === SQLITE_BUSY || errcode === SQLITE_BUSY_SNAPSHOT;
|
|
23374
|
+
}
|
|
23375
|
+
function sleepSync(ms) {
|
|
23376
|
+
Atomics.wait(SLEEP_BUF, 0, 0, ms);
|
|
23377
|
+
}
|
|
23378
|
+
function probeEngine() {
|
|
23379
|
+
try {
|
|
23380
|
+
const db = newDatabase(":memory:");
|
|
23381
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
23382
|
+
db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
|
|
23383
|
+
const row = db.prepare("SELECT sqlite_version() AS v").get();
|
|
23384
|
+
db.close();
|
|
23385
|
+
return { ok: true, version: row.v };
|
|
23386
|
+
} catch (e) {
|
|
23387
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
23388
|
+
}
|
|
23389
|
+
}
|
|
23390
|
+
function openEngine(dbPath) {
|
|
23391
|
+
const native = newDatabase(dbPath);
|
|
23392
|
+
native.exec("PRAGMA journal_mode = WAL");
|
|
23393
|
+
native.exec("PRAGMA foreign_keys = ON");
|
|
23394
|
+
native.exec("PRAGMA busy_timeout = 5000");
|
|
23395
|
+
native.exec("PRAGMA synchronous = NORMAL");
|
|
23396
|
+
return new NodeSqliteAdapter(native);
|
|
23397
|
+
}
|
|
23398
|
+
var SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT, BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, savepointName, nodeRequire, _preconditionsApplied, _DatabaseSyncCtor, SLEEP_BUF, NodeSqliteAdapter;
|
|
23399
|
+
var init_engine = __esm({
|
|
23400
|
+
"src/lib/db/engine.ts"() {
|
|
23401
|
+
"use strict";
|
|
23402
|
+
init_runtime_checks();
|
|
23403
|
+
SQLITE_BUSY = 5;
|
|
23404
|
+
SQLITE_BUSY_SNAPSHOT = 261;
|
|
23405
|
+
BUSY_RETRY_ATTEMPTS = 5;
|
|
23406
|
+
BUSY_RETRY_BACKOFF_MS = 50;
|
|
23407
|
+
savepointName = (depth) => `ocr_sp_${depth}`;
|
|
23408
|
+
nodeRequire = createRequire2(import.meta.url);
|
|
23409
|
+
_preconditionsApplied = false;
|
|
23410
|
+
SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
23411
|
+
NodeSqliteAdapter = class {
|
|
23412
|
+
raw;
|
|
23413
|
+
/**
|
|
23414
|
+
* Transaction nesting depth. `node:sqlite` has no transaction helper, so we
|
|
23415
|
+
* drive `BEGIN IMMEDIATE` ourselves and use SAVEPOINTs for nested calls
|
|
23416
|
+
* (better-sqlite3 did this automatically). 0 = no transaction open.
|
|
23417
|
+
*/
|
|
23418
|
+
txnDepth = 0;
|
|
23419
|
+
constructor(db) {
|
|
23420
|
+
this.raw = db;
|
|
23421
|
+
}
|
|
23422
|
+
exec(sql, params) {
|
|
23423
|
+
const stmt = this.raw.prepare(sql);
|
|
23424
|
+
const cols = stmt.columns();
|
|
23425
|
+
if (cols.length === 0) {
|
|
23426
|
+
stmt.run(...params ?? []);
|
|
23427
|
+
return [];
|
|
23428
|
+
}
|
|
23429
|
+
stmt.setReturnArrays(true);
|
|
23430
|
+
const values = stmt.all(...params ?? []);
|
|
23431
|
+
return values.length > 0 ? [{ columns: cols.map((c) => c.name), values }] : [];
|
|
23432
|
+
}
|
|
23433
|
+
run(sql, params) {
|
|
23434
|
+
if (params !== void 0) {
|
|
23435
|
+
this.raw.prepare(sql).run(...params);
|
|
23436
|
+
return;
|
|
23437
|
+
}
|
|
23438
|
+
this.raw.exec(sql);
|
|
23439
|
+
}
|
|
23440
|
+
prepare(sql) {
|
|
23441
|
+
return this.raw.prepare(sql);
|
|
23442
|
+
}
|
|
23443
|
+
transaction(fn) {
|
|
23444
|
+
return this.txnDepth > 0 ? this.runNested(fn) : this.runOuter(fn);
|
|
23445
|
+
}
|
|
23446
|
+
/**
|
|
23447
|
+
* Nested call: a SAVEPOINT within the outer transaction's write lock. No
|
|
23448
|
+
* busy-retry — the outer transaction already holds the lock. The savepoint
|
|
23449
|
+
* lets the inner block roll back independently while the outer continues.
|
|
23450
|
+
*/
|
|
23451
|
+
runNested(fn) {
|
|
23452
|
+
const name = savepointName(this.txnDepth);
|
|
23453
|
+
this.raw.exec(`SAVEPOINT ${name}`);
|
|
23454
|
+
this.txnDepth++;
|
|
23455
|
+
try {
|
|
23456
|
+
const result = fn();
|
|
23457
|
+
this.raw.exec(`RELEASE ${name}`);
|
|
23458
|
+
return result;
|
|
23459
|
+
} catch (e) {
|
|
23460
|
+
try {
|
|
23461
|
+
this.raw.exec(`ROLLBACK TO ${name}`);
|
|
23462
|
+
this.raw.exec(`RELEASE ${name}`);
|
|
23463
|
+
} catch {
|
|
23464
|
+
}
|
|
23465
|
+
throw e;
|
|
23466
|
+
} finally {
|
|
23467
|
+
this.txnDepth--;
|
|
23468
|
+
}
|
|
23469
|
+
}
|
|
23470
|
+
/**
|
|
23471
|
+
* Outer transaction: `BEGIN IMMEDIATE` acquires the write lock up front so
|
|
23472
|
+
* cross-process writers serialize cleanly under WAL instead of failing late
|
|
23473
|
+
* on upgrade. `busy_timeout` covers most contention; a bounded synchronous
|
|
23474
|
+
* retry absorbs the residual SQLITE_BUSY (another connection holds the lock
|
|
23475
|
+
* past the timeout, or BUSY_SNAPSHOT). Non-busy errors and the final attempt
|
|
23476
|
+
* re-throw so genuine failures propagate.
|
|
23477
|
+
*/
|
|
23478
|
+
runOuter(fn) {
|
|
23479
|
+
for (let attempt = 0; attempt < BUSY_RETRY_ATTEMPTS; attempt++) {
|
|
23480
|
+
try {
|
|
23481
|
+
return this.runOnce(fn);
|
|
23482
|
+
} catch (e) {
|
|
23483
|
+
if (!isBusyError(e) || attempt === BUSY_RETRY_ATTEMPTS - 1) throw e;
|
|
23484
|
+
sleepSync(BUSY_RETRY_BACKOFF_MS);
|
|
23485
|
+
}
|
|
23486
|
+
}
|
|
23487
|
+
throw new Error("transaction retry budget exhausted");
|
|
23488
|
+
}
|
|
23489
|
+
/** One `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK` lifecycle. */
|
|
23490
|
+
runOnce(fn) {
|
|
23491
|
+
this.raw.exec("BEGIN IMMEDIATE");
|
|
23492
|
+
this.txnDepth = 1;
|
|
23493
|
+
try {
|
|
23494
|
+
const result = fn();
|
|
23495
|
+
this.raw.exec("COMMIT");
|
|
23496
|
+
return result;
|
|
23497
|
+
} catch (e) {
|
|
23498
|
+
try {
|
|
23499
|
+
this.raw.exec("ROLLBACK");
|
|
23500
|
+
} catch {
|
|
23501
|
+
}
|
|
23502
|
+
throw e;
|
|
23503
|
+
} finally {
|
|
23504
|
+
this.txnDepth = 0;
|
|
23505
|
+
}
|
|
23506
|
+
}
|
|
23507
|
+
pragma(source) {
|
|
23508
|
+
this.raw.exec(`PRAGMA ${source}`);
|
|
23509
|
+
return void 0;
|
|
23510
|
+
}
|
|
23511
|
+
close() {
|
|
23512
|
+
try {
|
|
23513
|
+
this.raw.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
23514
|
+
} catch {
|
|
23515
|
+
}
|
|
23516
|
+
try {
|
|
23517
|
+
this.raw.close();
|
|
23518
|
+
} catch (e) {
|
|
23519
|
+
const message = e?.message ?? "";
|
|
23520
|
+
if (!/database is not open/i.test(message)) throw e;
|
|
23521
|
+
}
|
|
23522
|
+
}
|
|
23523
|
+
};
|
|
23524
|
+
}
|
|
23525
|
+
});
|
|
23526
|
+
|
|
23322
23527
|
// src/lib/db/migrations.ts
|
|
23528
|
+
function columnExists(db, table, column) {
|
|
23529
|
+
const result = db.exec(`PRAGMA table_info(${table})`);
|
|
23530
|
+
const first = result[0];
|
|
23531
|
+
if (!first) return false;
|
|
23532
|
+
const nameIdx = first.columns.indexOf("name");
|
|
23533
|
+
return first.values.some((row) => row[nameIdx] === column);
|
|
23534
|
+
}
|
|
23323
23535
|
function ensureSchemaVersionTable(db) {
|
|
23324
23536
|
db.run(`
|
|
23325
23537
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -23329,6 +23541,10 @@ function ensureSchemaVersionTable(db) {
|
|
|
23329
23541
|
);
|
|
23330
23542
|
`);
|
|
23331
23543
|
}
|
|
23544
|
+
function getSchemaVersion(db) {
|
|
23545
|
+
ensureSchemaVersionTable(db);
|
|
23546
|
+
return getCurrentVersion(db);
|
|
23547
|
+
}
|
|
23332
23548
|
function getCurrentVersion(db) {
|
|
23333
23549
|
const result = db.exec(
|
|
23334
23550
|
"SELECT MAX(version) as v FROM schema_version"
|
|
@@ -23346,9 +23562,10 @@ function runMigrations(db) {
|
|
|
23346
23562
|
if (migration.version <= currentVersion) {
|
|
23347
23563
|
continue;
|
|
23348
23564
|
}
|
|
23349
|
-
db.run("BEGIN
|
|
23565
|
+
db.run("BEGIN IMMEDIATE;");
|
|
23350
23566
|
try {
|
|
23351
|
-
db.run(migration.sql);
|
|
23567
|
+
if (migration.sql) db.run(migration.sql);
|
|
23568
|
+
migration.run?.(db);
|
|
23352
23569
|
db.run(
|
|
23353
23570
|
"INSERT INTO schema_version (version, description) VALUES (?, ?);",
|
|
23354
23571
|
[migration.version, migration.description]
|
|
@@ -23685,6 +23902,148 @@ var init_migrations = __esm({
|
|
|
23685
23902
|
DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
|
|
23686
23903
|
DROP TABLE IF EXISTS agent_sessions;
|
|
23687
23904
|
`
|
|
23905
|
+
},
|
|
23906
|
+
{
|
|
23907
|
+
version: 12,
|
|
23908
|
+
description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
|
|
23909
|
+
sql: `
|
|
23910
|
+
-- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
|
|
23911
|
+
-- The sweep filters sessions by status and rolls up MAX(created_at) per
|
|
23912
|
+
-- session over the event log; deriveNextRound does MAX(round). Index both.
|
|
23913
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
23914
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_created
|
|
23915
|
+
ON orchestration_events(session_id, created_at);
|
|
23916
|
+
|
|
23917
|
+
-- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
|
|
23918
|
+
-- orchestration_events.event_type is the spine of all lifecycle
|
|
23919
|
+
-- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
|
|
23920
|
+
-- silently break deriveNextRound and the completeness view. SQLite cannot
|
|
23921
|
+
-- add a CHECK to an existing column without a table rebuild, so enforce
|
|
23922
|
+
-- the closed vocabulary with a BEFORE INSERT trigger instead.
|
|
23923
|
+
CREATE TRIGGER IF NOT EXISTS trg_events_known_type
|
|
23924
|
+
BEFORE INSERT ON orchestration_events
|
|
23925
|
+
WHEN NEW.event_type NOT IN (
|
|
23926
|
+
'session_created', 'session_resumed', 'round_started', 'phase_transition',
|
|
23927
|
+
'round_completed', 'map_completed', 'session_closed', 'session_aborted',
|
|
23928
|
+
'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
|
|
23929
|
+
)
|
|
23930
|
+
BEGIN
|
|
23931
|
+
SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
|
|
23932
|
+
END;
|
|
23933
|
+
|
|
23934
|
+
-- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
|
|
23935
|
+
-- A session cannot transition active \u2192 closed unless its current
|
|
23936
|
+
-- round/run has a terminal artifact event, OR an explicit reason event
|
|
23937
|
+
-- (abort / auto-close-stale / sync / legacy-import) is present. Only a
|
|
23938
|
+
-- *silent* premature close is banned \u2014 every legitimate non-artifact
|
|
23939
|
+
-- close carries a reason event and passes. App-level guards in
|
|
23940
|
+
-- stateClose/finish are the primary check; this makes the illegal state
|
|
23941
|
+
-- unrepresentable even via raw SQL.
|
|
23942
|
+
--
|
|
23943
|
+
-- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
|
|
23944
|
+
-- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
|
|
23945
|
+
-- recorded for an earlier round would also satisfy a later close. The
|
|
23946
|
+
-- app-level guards ARE round-scoped (hasCompletionInvariant checks the
|
|
23947
|
+
-- current round/run), so the precise check lives in the application; this
|
|
23948
|
+
-- trigger is a coarse backstop against a *silent* premature close via raw
|
|
23949
|
+
-- SQL. Tightening it to be round-scoped would require a new migration
|
|
23950
|
+
-- (this v12 trigger is append-only and already shipped); the residual
|
|
23951
|
+
-- risk is a non-artifact close carrying a stale reason event, which is
|
|
23952
|
+
-- still an explicit, audited terminal \u2014 not the failure mode this guards.
|
|
23953
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
|
|
23954
|
+
BEFORE UPDATE OF status ON sessions
|
|
23955
|
+
WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
|
|
23956
|
+
AND NOT EXISTS (
|
|
23957
|
+
SELECT 1 FROM orchestration_events e
|
|
23958
|
+
WHERE e.session_id = NEW.id
|
|
23959
|
+
AND (
|
|
23960
|
+
(NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
|
|
23961
|
+
OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
|
|
23962
|
+
OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
23963
|
+
)
|
|
23964
|
+
)
|
|
23965
|
+
BEGIN
|
|
23966
|
+
SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
|
|
23967
|
+
END;
|
|
23968
|
+
|
|
23969
|
+
-- \u2500\u2500 session_completeness view \u2500\u2500
|
|
23970
|
+
-- The published contract for "is this session actually complete, and if
|
|
23971
|
+
-- not, what's missing". Completion is DERIVED from the event log, never a
|
|
23972
|
+
-- mutable flag: a session is complete iff it is closed AND a terminal
|
|
23973
|
+
-- artifact event exists for its current round/run. The dashboard's
|
|
23974
|
+
-- outcome derivation and the agent 'status' command read this view, so
|
|
23975
|
+
-- they cannot disagree.
|
|
23976
|
+
--
|
|
23977
|
+
-- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
|
|
23978
|
+
-- status column (marked_closed) with append-only event evidence (the
|
|
23979
|
+
-- terminal artifact event). This is sound precisely because the
|
|
23980
|
+
-- close-guard trigger above makes the status column trustworthy \u2014 a row
|
|
23981
|
+
-- can only reach status='closed' with a completed round/run or an
|
|
23982
|
+
-- explicit reason event \u2014 so reading the column is not a regression to
|
|
23983
|
+
-- the old "mutable flag that could lie" model.
|
|
23984
|
+
--
|
|
23985
|
+
-- completeness_state:
|
|
23986
|
+
-- 'complete' \u2014 closed + terminal artifact for current round/run
|
|
23987
|
+
-- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
|
|
23988
|
+
-- "completed too soon" condition)
|
|
23989
|
+
-- 'in_flight' \u2014 open with a dependent process still running
|
|
23990
|
+
-- 'open_no_artifact' \u2014 open, no in-flight dependents
|
|
23991
|
+
CREATE VIEW IF NOT EXISTS session_completeness AS
|
|
23992
|
+
SELECT
|
|
23993
|
+
s.id AS session_id,
|
|
23994
|
+
s.workflow_type AS workflow_type,
|
|
23995
|
+
s.status AS status,
|
|
23996
|
+
s.current_round AS current_round,
|
|
23997
|
+
s.current_map_run AS current_map_run,
|
|
23998
|
+
CASE WHEN EXISTS (
|
|
23999
|
+
SELECT 1 FROM orchestration_events e
|
|
24000
|
+
WHERE e.session_id = s.id
|
|
24001
|
+
AND (
|
|
24002
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
24003
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
24004
|
+
)
|
|
24005
|
+
) THEN 1 ELSE 0 END AS has_terminal_artifact,
|
|
24006
|
+
CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
|
|
24007
|
+
CASE WHEN NOT EXISTS (
|
|
24008
|
+
SELECT 1 FROM command_executions ce
|
|
24009
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
24010
|
+
) THEN 1 ELSE 0 END AS dependents_settled,
|
|
24011
|
+
CASE
|
|
24012
|
+
WHEN s.status = 'closed' AND EXISTS (
|
|
24013
|
+
SELECT 1 FROM orchestration_events e
|
|
24014
|
+
WHERE e.session_id = s.id
|
|
24015
|
+
AND (
|
|
24016
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
24017
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
24018
|
+
)
|
|
24019
|
+
) THEN 'complete'
|
|
24020
|
+
WHEN s.status = 'closed' THEN 'closed_without_artifact'
|
|
24021
|
+
WHEN EXISTS (
|
|
24022
|
+
SELECT 1 FROM command_executions ce
|
|
24023
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
24024
|
+
) THEN 'in_flight'
|
|
24025
|
+
ELSE 'open_no_artifact'
|
|
24026
|
+
END AS completeness_state
|
|
24027
|
+
FROM sessions s;
|
|
24028
|
+
`
|
|
24029
|
+
},
|
|
24030
|
+
{
|
|
24031
|
+
version: 13,
|
|
24032
|
+
description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
|
|
24033
|
+
// parent_id was reserved for an AI-instance → dashboard-spawn lineage link
|
|
24034
|
+
// that was never wired (no writer, no reader). A process's KIND (supervisor
|
|
24035
|
+
// / reviewer-instance / utility) is derived from columns that are always
|
|
24036
|
+
// present (command + last_heartbeat_at), so the dead lineage column and its
|
|
24037
|
+
// all-NULL index are removed. Re-add a wired parent_id alongside a real
|
|
24038
|
+
// consumer (e.g. a parent→child tree view) if lineage is ever needed.
|
|
24039
|
+
//
|
|
24040
|
+
// Imperative + guarded so the DROP COLUMN (which SQLite can't express as
|
|
24041
|
+
// IF EXISTS) is idempotent under re-application.
|
|
24042
|
+
run: (db) => {
|
|
24043
|
+
if (!columnExists(db, "command_executions", "parent_id")) return;
|
|
24044
|
+
db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
|
|
24045
|
+
db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
|
|
24046
|
+
}
|
|
23688
24047
|
}
|
|
23689
24048
|
];
|
|
23690
24049
|
}
|
|
@@ -23798,6 +24157,12 @@ function getLatestEventId(db) {
|
|
|
23798
24157
|
const val = result[0]?.values[0]?.[0];
|
|
23799
24158
|
return typeof val === "number" ? val : 0;
|
|
23800
24159
|
}
|
|
24160
|
+
function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
|
|
24161
|
+
db.transaction(() => {
|
|
24162
|
+
insertEvent(db, { session_id: sessionId, ...reasonEvent });
|
|
24163
|
+
updateSession(db, sessionId, projectionUpdates);
|
|
24164
|
+
});
|
|
24165
|
+
}
|
|
23801
24166
|
var init_queries = __esm({
|
|
23802
24167
|
"src/lib/db/queries.ts"() {
|
|
23803
24168
|
"use strict";
|
|
@@ -23805,7 +24170,208 @@ var init_queries = __esm({
|
|
|
23805
24170
|
}
|
|
23806
24171
|
});
|
|
23807
24172
|
|
|
24173
|
+
// src/lib/db/reconcile.ts
|
|
24174
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
24175
|
+
import { isAbsolute as isAbsolute2, join as join12, dirname as dirname4 } from "node:path";
|
|
24176
|
+
function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
|
|
24177
|
+
const eventType = workflowType === "map" ? "map_completed" : "round_completed";
|
|
24178
|
+
const round = workflowType === "map" ? currentMapRun : currentRound;
|
|
24179
|
+
const r = db.exec(
|
|
24180
|
+
`SELECT 1 FROM orchestration_events
|
|
24181
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
24182
|
+
[sessionId, eventType, round]
|
|
24183
|
+
);
|
|
24184
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
24185
|
+
}
|
|
24186
|
+
function hasReasonEvent(db, sessionId) {
|
|
24187
|
+
const r = db.exec(
|
|
24188
|
+
`SELECT 1 FROM orchestration_events
|
|
24189
|
+
WHERE session_id = ?
|
|
24190
|
+
AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
24191
|
+
LIMIT 1`,
|
|
24192
|
+
[sessionId]
|
|
24193
|
+
);
|
|
24194
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
24195
|
+
}
|
|
24196
|
+
function lastEventAgeSeconds(db, sessionId) {
|
|
24197
|
+
const r = db.exec(
|
|
24198
|
+
`SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
|
|
24199
|
+
FROM orchestration_events WHERE session_id = ?`,
|
|
24200
|
+
[sessionId]
|
|
24201
|
+
);
|
|
24202
|
+
const v = r[0]?.values[0]?.[0];
|
|
24203
|
+
return typeof v === "number" ? v : null;
|
|
24204
|
+
}
|
|
24205
|
+
function hasInFlightDependents(db, sessionId) {
|
|
24206
|
+
const r = db.exec(
|
|
24207
|
+
`SELECT 1 FROM command_executions
|
|
24208
|
+
WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
|
|
24209
|
+
[sessionId]
|
|
24210
|
+
);
|
|
24211
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
24212
|
+
}
|
|
24213
|
+
function resolveSessionDir(ocrDir, sessionDir) {
|
|
24214
|
+
if (!sessionDir) return null;
|
|
24215
|
+
if (isAbsolute2(sessionDir)) return sessionDir;
|
|
24216
|
+
return join12(dirname4(ocrDir), sessionDir);
|
|
24217
|
+
}
|
|
24218
|
+
function reconcileLegacyState(db, ocrDir, opts = {}) {
|
|
24219
|
+
const dryRun = opts.dryRun ?? false;
|
|
24220
|
+
const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
24221
|
+
const actions = [];
|
|
24222
|
+
for (const s of getAllSessions(db)) {
|
|
24223
|
+
const dir = resolveSessionDir(ocrDir, s.session_dir);
|
|
24224
|
+
if (s.status === "closed") {
|
|
24225
|
+
if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
|
|
24226
|
+
continue;
|
|
24227
|
+
}
|
|
24228
|
+
const reviewFinal = s.workflow_type === "review" && dir ? existsSync10(join12(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
|
|
24229
|
+
const mapFinal = s.workflow_type === "map" && dir ? existsSync10(join12(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
|
|
24230
|
+
if (reviewFinal) {
|
|
24231
|
+
actions.push({
|
|
24232
|
+
sessionId: s.id,
|
|
24233
|
+
kind: "synthesize-round-completed",
|
|
24234
|
+
detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
|
|
24235
|
+
});
|
|
24236
|
+
if (!dryRun) {
|
|
24237
|
+
insertEvent(db, {
|
|
24238
|
+
session_id: s.id,
|
|
24239
|
+
event_type: "round_completed",
|
|
24240
|
+
phase: "synthesis",
|
|
24241
|
+
phase_number: 7,
|
|
24242
|
+
round: s.current_round,
|
|
24243
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
|
|
24244
|
+
});
|
|
24245
|
+
}
|
|
24246
|
+
} else if (mapFinal) {
|
|
24247
|
+
actions.push({
|
|
24248
|
+
sessionId: s.id,
|
|
24249
|
+
kind: "synthesize-map-completed",
|
|
24250
|
+
detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
|
|
24251
|
+
});
|
|
24252
|
+
if (!dryRun) {
|
|
24253
|
+
insertEvent(db, {
|
|
24254
|
+
session_id: s.id,
|
|
24255
|
+
event_type: "map_completed",
|
|
24256
|
+
phase: "synthesis",
|
|
24257
|
+
phase_number: 5,
|
|
24258
|
+
round: s.current_map_run,
|
|
24259
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
|
|
24260
|
+
});
|
|
24261
|
+
}
|
|
24262
|
+
} else {
|
|
24263
|
+
actions.push({
|
|
24264
|
+
sessionId: s.id,
|
|
24265
|
+
kind: "grandfather",
|
|
24266
|
+
detail: "no provable artifact; recording session_legacy_import"
|
|
24267
|
+
});
|
|
24268
|
+
if (!dryRun) {
|
|
24269
|
+
insertEvent(db, {
|
|
24270
|
+
session_id: s.id,
|
|
24271
|
+
event_type: "session_legacy_import",
|
|
24272
|
+
phase: "complete",
|
|
24273
|
+
metadata: JSON.stringify({ source: "reconciled" })
|
|
24274
|
+
});
|
|
24275
|
+
}
|
|
24276
|
+
}
|
|
24277
|
+
continue;
|
|
24278
|
+
}
|
|
24279
|
+
const age = lastEventAgeSeconds(db, s.id);
|
|
24280
|
+
const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
|
|
24281
|
+
if (stale) {
|
|
24282
|
+
actions.push({
|
|
24283
|
+
sessionId: s.id,
|
|
24284
|
+
kind: "stale-close",
|
|
24285
|
+
detail: age === null ? "active with no events and no in-flight dependents" : `active, last event ${Math.round(age / 86400)}d ago, no in-flight dependents`
|
|
24286
|
+
});
|
|
24287
|
+
if (!dryRun) {
|
|
24288
|
+
commitReasonClose(
|
|
24289
|
+
db,
|
|
24290
|
+
s.id,
|
|
24291
|
+
{
|
|
24292
|
+
event_type: "session_auto_closed_stale",
|
|
24293
|
+
phase: "complete",
|
|
24294
|
+
metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
|
|
24295
|
+
},
|
|
24296
|
+
{ status: "closed", current_phase: "complete" }
|
|
24297
|
+
);
|
|
24298
|
+
}
|
|
24299
|
+
}
|
|
24300
|
+
}
|
|
24301
|
+
return { dryRun, actions };
|
|
24302
|
+
}
|
|
24303
|
+
var DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
24304
|
+
var init_reconcile = __esm({
|
|
24305
|
+
"src/lib/db/reconcile.ts"() {
|
|
24306
|
+
"use strict";
|
|
24307
|
+
init_queries();
|
|
24308
|
+
DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
24309
|
+
}
|
|
24310
|
+
});
|
|
24311
|
+
|
|
24312
|
+
// src/lib/db/liveness.ts
|
|
24313
|
+
function defaultIsAlive(pid) {
|
|
24314
|
+
try {
|
|
24315
|
+
process.kill(pid, 0);
|
|
24316
|
+
return true;
|
|
24317
|
+
} catch (err) {
|
|
24318
|
+
return !(err instanceof Error && "code" in err && err.code === "ESRCH");
|
|
24319
|
+
}
|
|
24320
|
+
}
|
|
24321
|
+
function sqliteUtcMs(ts) {
|
|
24322
|
+
const sqliteShape = ts.includes(" ");
|
|
24323
|
+
return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
|
|
24324
|
+
}
|
|
24325
|
+
var PID_REUSE_GUARD_MS;
|
|
24326
|
+
var init_liveness = __esm({
|
|
24327
|
+
"src/lib/db/liveness.ts"() {
|
|
24328
|
+
"use strict";
|
|
24329
|
+
PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
|
|
24330
|
+
}
|
|
24331
|
+
});
|
|
24332
|
+
|
|
24333
|
+
// src/lib/state/exit-codes.ts
|
|
24334
|
+
var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE;
|
|
24335
|
+
var init_exit_codes = __esm({
|
|
24336
|
+
"src/lib/state/exit-codes.ts"() {
|
|
24337
|
+
"use strict";
|
|
24338
|
+
STATE_EXIT = {
|
|
24339
|
+
OK: 0,
|
|
24340
|
+
USAGE: 2,
|
|
24341
|
+
AMBIGUOUS: 3,
|
|
24342
|
+
NOT_FOUND: 4,
|
|
24343
|
+
ILLEGAL_TRANSITION: 5,
|
|
24344
|
+
INVARIANT_UNMET: 6,
|
|
24345
|
+
SCHEMA_INVALID: 7,
|
|
24346
|
+
/** Database was locked past the bounded retry budget (SQLITE_BUSY). */
|
|
24347
|
+
BUSY: 8
|
|
24348
|
+
};
|
|
24349
|
+
StateError = class extends Error {
|
|
24350
|
+
constructor(code, message) {
|
|
24351
|
+
super(message);
|
|
24352
|
+
this.code = code;
|
|
24353
|
+
this.name = "StateError";
|
|
24354
|
+
}
|
|
24355
|
+
};
|
|
24356
|
+
CANCELLED_EXIT_CODE = -2;
|
|
24357
|
+
ORPHAN_EXIT_CODE = -3;
|
|
24358
|
+
CASCADE_CLOSE_EXIT_CODE = -4;
|
|
24359
|
+
}
|
|
24360
|
+
});
|
|
24361
|
+
|
|
23808
24362
|
// src/lib/db/agent-sessions.ts
|
|
24363
|
+
function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
|
|
24364
|
+
db.run(
|
|
24365
|
+
`UPDATE command_executions
|
|
24366
|
+
SET finished_at = datetime('now'),
|
|
24367
|
+
exit_code = ?,
|
|
24368
|
+
pid = NULL,
|
|
24369
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
24370
|
+
WHERE workflow_id = ?
|
|
24371
|
+
AND finished_at IS NULL`,
|
|
24372
|
+
[exitCode, note, workflowId]
|
|
24373
|
+
);
|
|
24374
|
+
}
|
|
23809
24375
|
function rowToAgentSession(row) {
|
|
23810
24376
|
return {
|
|
23811
24377
|
// The OCR-owned id is the `uid` column. Fall back to the integer
|
|
@@ -23820,6 +24386,7 @@ function rowToAgentSession(row) {
|
|
|
23820
24386
|
resolved_model: row.resolved_model,
|
|
23821
24387
|
phase: null,
|
|
23822
24388
|
status: deriveStatus(row),
|
|
24389
|
+
kind: rowKind(row),
|
|
23823
24390
|
pid: row.pid,
|
|
23824
24391
|
started_at: row.started_at,
|
|
23825
24392
|
last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
|
|
@@ -23833,7 +24400,9 @@ function deriveStatus(row) {
|
|
|
23833
24400
|
return "running";
|
|
23834
24401
|
}
|
|
23835
24402
|
if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
|
|
23836
|
-
if (row.exit_code === CANCELLED_EXIT_CODE)
|
|
24403
|
+
if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
|
|
24404
|
+
return "cancelled";
|
|
24405
|
+
}
|
|
23837
24406
|
if (row.exit_code === 0) return "done";
|
|
23838
24407
|
return "crashed";
|
|
23839
24408
|
}
|
|
@@ -23849,7 +24418,7 @@ function insertAgentSession(db, params) {
|
|
|
23849
24418
|
pid = null,
|
|
23850
24419
|
notes = null
|
|
23851
24420
|
} = params;
|
|
23852
|
-
const command = persona && instance_index !== null ?
|
|
24421
|
+
const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
|
|
23853
24422
|
db.run(
|
|
23854
24423
|
`INSERT INTO command_executions
|
|
23855
24424
|
(uid, command, args, workflow_id, vendor, persona, instance_index, name,
|
|
@@ -24054,63 +24623,139 @@ function updateAgentSession(db, id, params) {
|
|
|
24054
24623
|
values
|
|
24055
24624
|
);
|
|
24056
24625
|
}
|
|
24057
|
-
function sweepStaleAgentSessions(db, thresholdSeconds) {
|
|
24058
|
-
const
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
|
|
24063
|
-
|
|
24064
|
-
|
|
24065
|
-
|
|
24626
|
+
function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
|
|
24627
|
+
const candidates = resultToRows(
|
|
24628
|
+
db.exec(
|
|
24629
|
+
`SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
|
|
24630
|
+
FROM command_executions
|
|
24631
|
+
WHERE finished_at IS NULL
|
|
24632
|
+
AND pid IS NOT NULL
|
|
24633
|
+
AND last_heartbeat_at IS NOT NULL
|
|
24634
|
+
AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
|
|
24635
|
+
[thresholdSeconds]
|
|
24636
|
+
)
|
|
24066
24637
|
);
|
|
24067
|
-
if (
|
|
24068
|
-
return { orphanedIds: [] };
|
|
24638
|
+
if (candidates.length === 0) {
|
|
24639
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
24640
|
+
}
|
|
24641
|
+
const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
|
|
24642
|
+
const dead = candidates.filter((row) => {
|
|
24643
|
+
if (row.pid === null) return false;
|
|
24644
|
+
if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
|
|
24645
|
+
return !isAlive(row.pid);
|
|
24646
|
+
});
|
|
24647
|
+
if (dead.length === 0) {
|
|
24648
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
24069
24649
|
}
|
|
24070
24650
|
const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
|
|
24071
|
-
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
|
|
24079
|
-
|
|
24080
|
-
|
|
24651
|
+
const placeholders = dead.map(() => "?").join(", ");
|
|
24652
|
+
const cascadedWorkflowIds = [];
|
|
24653
|
+
db.transaction(() => {
|
|
24654
|
+
db.run(
|
|
24655
|
+
`UPDATE command_executions
|
|
24656
|
+
SET finished_at = datetime('now'),
|
|
24657
|
+
exit_code = ?,
|
|
24658
|
+
pid = NULL,
|
|
24659
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
24660
|
+
WHERE id IN (${placeholders})
|
|
24661
|
+
AND finished_at IS NULL`,
|
|
24662
|
+
[ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
|
|
24663
|
+
);
|
|
24664
|
+
for (const row of dead) {
|
|
24665
|
+
if (row.workflow_id && rowKind(row) === "supervisor") {
|
|
24666
|
+
cascadeTerminateExecutions(
|
|
24667
|
+
db,
|
|
24668
|
+
row.workflow_id,
|
|
24669
|
+
CASCADE_CLOSE_EXIT_CODE,
|
|
24670
|
+
"cascade-closed: workflow process orphaned by liveness sweep"
|
|
24671
|
+
);
|
|
24672
|
+
cascadedWorkflowIds.push(row.workflow_id);
|
|
24673
|
+
}
|
|
24674
|
+
}
|
|
24675
|
+
});
|
|
24081
24676
|
return {
|
|
24082
|
-
orphanedIds:
|
|
24677
|
+
orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
|
|
24678
|
+
cascadedWorkflowIds
|
|
24083
24679
|
};
|
|
24084
24680
|
}
|
|
24085
|
-
|
|
24681
|
+
function rowKind(row) {
|
|
24682
|
+
if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
|
|
24683
|
+
return "instance";
|
|
24684
|
+
}
|
|
24685
|
+
return row.last_heartbeat_at == null ? "utility" : "supervisor";
|
|
24686
|
+
}
|
|
24687
|
+
function sweepStaleSessions(db, thresholdSeconds) {
|
|
24688
|
+
const sql = `
|
|
24689
|
+
SELECT s.id
|
|
24690
|
+
FROM sessions s
|
|
24691
|
+
LEFT JOIN (
|
|
24692
|
+
SELECT session_id, MAX(created_at) AS last_event_at
|
|
24693
|
+
FROM orchestration_events
|
|
24694
|
+
GROUP BY session_id
|
|
24695
|
+
) e ON e.session_id = s.id
|
|
24696
|
+
WHERE s.status = 'active'
|
|
24697
|
+
AND (
|
|
24698
|
+
e.last_event_at IS NULL
|
|
24699
|
+
OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
|
|
24700
|
+
)
|
|
24701
|
+
AND NOT EXISTS (
|
|
24702
|
+
SELECT 1 FROM command_executions ce
|
|
24703
|
+
WHERE ce.workflow_id = s.id
|
|
24704
|
+
AND ce.finished_at IS NULL
|
|
24705
|
+
)
|
|
24706
|
+
`;
|
|
24707
|
+
const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
|
|
24708
|
+
if (rows.length === 0) {
|
|
24709
|
+
return { closedSessionIds: [] };
|
|
24710
|
+
}
|
|
24711
|
+
for (const row of rows) {
|
|
24712
|
+
commitReasonClose(
|
|
24713
|
+
db,
|
|
24714
|
+
row.id,
|
|
24715
|
+
{
|
|
24716
|
+
event_type: "session_auto_closed_stale",
|
|
24717
|
+
phase: "complete",
|
|
24718
|
+
metadata: JSON.stringify({
|
|
24719
|
+
reason: "no events past threshold; no in-flight dependents",
|
|
24720
|
+
threshold_seconds: thresholdSeconds
|
|
24721
|
+
})
|
|
24722
|
+
},
|
|
24723
|
+
{ status: "closed", current_phase: "complete" }
|
|
24724
|
+
);
|
|
24725
|
+
}
|
|
24726
|
+
return { closedSessionIds: rows.map((r) => r.id) };
|
|
24727
|
+
}
|
|
24728
|
+
var NOTE_ORPHAN_PREFIX, INSTANCE_COMMAND;
|
|
24086
24729
|
var init_agent_sessions = __esm({
|
|
24087
24730
|
"src/lib/db/agent-sessions.ts"() {
|
|
24088
24731
|
"use strict";
|
|
24089
24732
|
init_result_mapper();
|
|
24090
|
-
|
|
24091
|
-
|
|
24733
|
+
init_queries();
|
|
24734
|
+
init_liveness();
|
|
24735
|
+
init_exit_codes();
|
|
24092
24736
|
NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
|
|
24737
|
+
INSTANCE_COMMAND = "session-instance";
|
|
24093
24738
|
}
|
|
24094
24739
|
});
|
|
24095
24740
|
|
|
24096
24741
|
// src/lib/db/command-log.ts
|
|
24097
|
-
import { appendFileSync, existsSync as
|
|
24098
|
-
import { dirname as
|
|
24742
|
+
import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
24743
|
+
import { dirname as dirname5, join as join13 } from "node:path";
|
|
24099
24744
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
24100
24745
|
function generateCommandUid() {
|
|
24101
24746
|
return randomUUID2();
|
|
24102
24747
|
}
|
|
24103
24748
|
function cacheDir(ocrDir) {
|
|
24104
|
-
return
|
|
24749
|
+
return join13(ocrDir, "data", CACHE_DIR);
|
|
24105
24750
|
}
|
|
24106
24751
|
function commandLogPath(ocrDir) {
|
|
24107
|
-
return
|
|
24752
|
+
return join13(cacheDir(ocrDir), FILENAME);
|
|
24108
24753
|
}
|
|
24109
24754
|
function appendCommandLog(ocrDir, entry) {
|
|
24110
24755
|
try {
|
|
24111
24756
|
const filePath = commandLogPath(ocrDir);
|
|
24112
|
-
const dir =
|
|
24113
|
-
if (!
|
|
24757
|
+
const dir = dirname5(filePath);
|
|
24758
|
+
if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
|
|
24114
24759
|
const line = JSON.stringify(entry) + "\n";
|
|
24115
24760
|
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
24116
24761
|
if (approxLineCount >= 0) approxLineCount++;
|
|
@@ -24120,7 +24765,7 @@ function appendCommandLog(ocrDir, entry) {
|
|
|
24120
24765
|
}
|
|
24121
24766
|
function readCommandLog(ocrDir) {
|
|
24122
24767
|
const filePath = commandLogPath(ocrDir);
|
|
24123
|
-
if (!
|
|
24768
|
+
if (!existsSync11(filePath)) return [];
|
|
24124
24769
|
const content = readFileSync9(filePath, "utf-8");
|
|
24125
24770
|
const entries = [];
|
|
24126
24771
|
for (const line of content.split("\n")) {
|
|
@@ -24199,16 +24844,25 @@ var init_command_log = __esm({
|
|
|
24199
24844
|
// src/lib/db/index.ts
|
|
24200
24845
|
var db_exports = {};
|
|
24201
24846
|
__export(db_exports, {
|
|
24847
|
+
CANCELLED_EXIT_CODE: () => CANCELLED_EXIT_CODE,
|
|
24848
|
+
CASCADE_CLOSE_EXIT_CODE: () => CASCADE_CLOSE_EXIT_CODE,
|
|
24202
24849
|
MIGRATIONS: () => MIGRATIONS,
|
|
24850
|
+
ORPHAN_EXIT_CODE: () => ORPHAN_EXIT_CODE,
|
|
24851
|
+
PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
|
|
24852
|
+
STATE_EXIT: () => STATE_EXIT,
|
|
24853
|
+
StateError: () => StateError,
|
|
24203
24854
|
appendCommandLog: () => appendCommandLog,
|
|
24204
|
-
applyPragmas: () => applyPragmas,
|
|
24205
24855
|
bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
|
|
24206
24856
|
bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
|
|
24207
24857
|
cacheDir: () => cacheDir,
|
|
24858
|
+
cascadeTerminateExecutions: () => cascadeTerminateExecutions,
|
|
24208
24859
|
closeAllDatabases: () => closeAllDatabases,
|
|
24209
24860
|
closeDatabase: () => closeDatabase,
|
|
24210
24861
|
commandLogPath: () => commandLogPath,
|
|
24862
|
+
commitReasonClose: () => commitReasonClose,
|
|
24863
|
+
defaultIsAlive: () => defaultIsAlive,
|
|
24211
24864
|
ensureDatabase: () => ensureDatabase,
|
|
24865
|
+
formatUpgradeNotice: () => formatUpgradeNotice,
|
|
24212
24866
|
generateCommandUid: () => generateCommandUid,
|
|
24213
24867
|
getAgentSession: () => getAgentSession,
|
|
24214
24868
|
getAllSessions: () => getAllSessions,
|
|
@@ -24217,119 +24871,153 @@ __export(db_exports, {
|
|
|
24217
24871
|
getLatestActiveSession: () => getLatestActiveSession,
|
|
24218
24872
|
getLatestAgentSessionWithVendorId: () => getLatestAgentSessionWithVendorId,
|
|
24219
24873
|
getLatestEventId: () => getLatestEventId,
|
|
24874
|
+
getSchemaVersion: () => getSchemaVersion,
|
|
24220
24875
|
getSession: () => getSession,
|
|
24221
24876
|
insertAgentSession: () => insertAgentSession,
|
|
24222
24877
|
insertEvent: () => insertEvent,
|
|
24223
24878
|
insertSession: () => insertSession,
|
|
24879
|
+
isBusyError: () => isBusyError,
|
|
24224
24880
|
linkDashboardInvocationToWorkflow: () => linkDashboardInvocationToWorkflow,
|
|
24225
24881
|
listAgentSessionsForWorkflow: () => listAgentSessionsForWorkflow,
|
|
24226
|
-
locateWasm: () => locateWasm,
|
|
24227
24882
|
openDatabase: () => openDatabase,
|
|
24883
|
+
probeEngine: () => probeEngine,
|
|
24884
|
+
probeWrite: () => probeWrite,
|
|
24228
24885
|
readCommandLog: () => readCommandLog,
|
|
24886
|
+
reconcileLegacyState: () => reconcileLegacyState,
|
|
24229
24887
|
recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
|
|
24230
24888
|
replayCommandLog: () => replayCommandLog,
|
|
24231
24889
|
resultToRow: () => resultToRow,
|
|
24232
24890
|
resultToRows: () => resultToRows,
|
|
24891
|
+
rowKind: () => rowKind,
|
|
24233
24892
|
runMigrations: () => runMigrations,
|
|
24234
|
-
saveDatabase: () => saveDatabase,
|
|
24235
24893
|
setAgentSessionStatus: () => setAgentSessionStatus,
|
|
24236
24894
|
setAgentSessionVendorId: () => setAgentSessionVendorId,
|
|
24895
|
+
sqliteUtcMs: () => sqliteUtcMs,
|
|
24237
24896
|
sweepStaleAgentSessions: () => sweepStaleAgentSessions,
|
|
24897
|
+
sweepStaleSessions: () => sweepStaleSessions,
|
|
24238
24898
|
updateAgentSession: () => updateAgentSession,
|
|
24239
24899
|
updateSession: () => updateSession,
|
|
24240
24900
|
walCheckpointTruncate: () => walCheckpointTruncate
|
|
24241
24901
|
});
|
|
24242
|
-
import {
|
|
24243
|
-
|
|
24244
|
-
|
|
24245
|
-
|
|
24246
|
-
|
|
24247
|
-
|
|
24248
|
-
|
|
24249
|
-
|
|
24250
|
-
|
|
24251
|
-
}
|
|
24252
|
-
function
|
|
24253
|
-
|
|
24254
|
-
|
|
24255
|
-
|
|
24902
|
+
import {
|
|
24903
|
+
existsSync as existsSync12,
|
|
24904
|
+
mkdirSync as mkdirSync4,
|
|
24905
|
+
copyFileSync,
|
|
24906
|
+
statSync,
|
|
24907
|
+
mkdtempSync,
|
|
24908
|
+
rmSync
|
|
24909
|
+
} from "node:fs";
|
|
24910
|
+
import { tmpdir } from "node:os";
|
|
24911
|
+
import { dirname as dirname6, join as join14 } from "node:path";
|
|
24912
|
+
function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
|
|
24913
|
+
if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
|
|
24914
|
+
const bakPath = `${dbPath}.bak.v${fromVersion}`;
|
|
24915
|
+
if (existsSync12(bakPath)) return bakPath;
|
|
24916
|
+
try {
|
|
24917
|
+
if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
|
|
24918
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
24919
|
+
copyFileSync(dbPath, bakPath);
|
|
24920
|
+
return bakPath;
|
|
24921
|
+
} catch {
|
|
24922
|
+
return null;
|
|
24923
|
+
}
|
|
24924
|
+
}
|
|
24925
|
+
function formatUpgradeNotice(bakPath, reconcile) {
|
|
24926
|
+
const lines = [
|
|
24927
|
+
"Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
|
|
24928
|
+
];
|
|
24929
|
+
if (bakPath) {
|
|
24930
|
+
lines.push(` A backup of your previous database was saved to: ${bakPath}`);
|
|
24931
|
+
}
|
|
24932
|
+
const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
|
|
24933
|
+
if (repairs.length > 0) {
|
|
24934
|
+
const n = (kind) => repairs.filter((a) => a.kind === kind).length;
|
|
24935
|
+
const parts = [];
|
|
24936
|
+
const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
|
|
24937
|
+
if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
|
|
24938
|
+
if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
|
|
24939
|
+
if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
|
|
24940
|
+
lines.push(
|
|
24941
|
+
` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
|
|
24942
|
+
);
|
|
24943
|
+
}
|
|
24944
|
+
lines.push(" Run `ocr doctor` to verify the storage engine.");
|
|
24945
|
+
return lines.map((l) => `[ocr] ${l}`).join("\n");
|
|
24256
24946
|
}
|
|
24257
24947
|
async function openDatabase(dbPath) {
|
|
24258
24948
|
const cached = connections.get(dbPath);
|
|
24259
24949
|
if (cached) {
|
|
24260
24950
|
return cached;
|
|
24261
24951
|
}
|
|
24262
|
-
const
|
|
24263
|
-
|
|
24264
|
-
|
|
24265
|
-
wasmBuffer.byteOffset + wasmBuffer.byteLength
|
|
24266
|
-
);
|
|
24267
|
-
const SQL = await initSqlJs({
|
|
24268
|
-
wasmBinary
|
|
24269
|
-
});
|
|
24270
|
-
let db;
|
|
24271
|
-
if (existsSync11(dbPath)) {
|
|
24272
|
-
const fileBuffer = readFileSync10(dbPath);
|
|
24273
|
-
db = new SQL.Database(fileBuffer);
|
|
24274
|
-
} else {
|
|
24275
|
-
db = new SQL.Database();
|
|
24952
|
+
const dir = dirname6(dbPath);
|
|
24953
|
+
if (!existsSync12(dir)) {
|
|
24954
|
+
mkdirSync4(dir, { recursive: true });
|
|
24276
24955
|
}
|
|
24277
|
-
|
|
24956
|
+
const db = openEngine(dbPath);
|
|
24278
24957
|
connections.set(dbPath, db);
|
|
24279
24958
|
return db;
|
|
24280
24959
|
}
|
|
24281
|
-
function saveDatabase(db, dbPath) {
|
|
24282
|
-
const data = db.export();
|
|
24283
|
-
const dir = dirname5(dbPath);
|
|
24284
|
-
if (!existsSync11(dir)) {
|
|
24285
|
-
mkdirSync4(dir, { recursive: true });
|
|
24286
|
-
}
|
|
24287
|
-
const tmpPath = `${dbPath}.${process.pid}.tmp`;
|
|
24288
|
-
writeFileSync7(tmpPath, Buffer.from(data));
|
|
24289
|
-
renameSync2(tmpPath, dbPath);
|
|
24290
|
-
}
|
|
24291
24960
|
async function getDb(ocrDir) {
|
|
24292
|
-
const dbPath =
|
|
24961
|
+
const dbPath = join14(ocrDir, "data", "ocr.db");
|
|
24293
24962
|
return openDatabase(dbPath);
|
|
24294
24963
|
}
|
|
24295
24964
|
async function ensureDatabase(ocrDir) {
|
|
24296
|
-
const dataDir =
|
|
24297
|
-
if (!
|
|
24965
|
+
const dataDir = join14(ocrDir, "data");
|
|
24966
|
+
if (!existsSync12(dataDir)) {
|
|
24298
24967
|
mkdirSync4(dataDir, { recursive: true });
|
|
24299
24968
|
}
|
|
24300
|
-
const dbPath =
|
|
24969
|
+
const dbPath = join14(dataDir, "ocr.db");
|
|
24301
24970
|
const db = await openDatabase(dbPath);
|
|
24971
|
+
let before = 0;
|
|
24972
|
+
try {
|
|
24973
|
+
before = getSchemaVersion(db);
|
|
24974
|
+
} catch {
|
|
24975
|
+
before = 0;
|
|
24976
|
+
}
|
|
24977
|
+
const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
|
|
24978
|
+
const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
|
|
24302
24979
|
runMigrations(db);
|
|
24303
|
-
|
|
24980
|
+
let reconcile;
|
|
24981
|
+
if (before < V2_SCHEMA_VERSION) {
|
|
24982
|
+
try {
|
|
24983
|
+
reconcile = reconcileLegacyState(db, ocrDir);
|
|
24984
|
+
} catch (err) {
|
|
24985
|
+
console.error(
|
|
24986
|
+
`[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
24987
|
+
);
|
|
24988
|
+
}
|
|
24989
|
+
}
|
|
24990
|
+
if (isLegacyUpgrade) {
|
|
24991
|
+
const notice = formatUpgradeNotice(bakPath, reconcile);
|
|
24992
|
+
if (notice) console.error(notice);
|
|
24993
|
+
}
|
|
24304
24994
|
return db;
|
|
24305
24995
|
}
|
|
24306
24996
|
function walCheckpointTruncate(dbPath) {
|
|
24307
|
-
if (!
|
|
24997
|
+
if (!existsSync12(dbPath)) {
|
|
24308
24998
|
return "skipped";
|
|
24309
24999
|
}
|
|
24310
|
-
|
|
24311
|
-
|
|
24312
|
-
|
|
24313
|
-
|
|
24314
|
-
|
|
24315
|
-
|
|
24316
|
-
return "
|
|
25000
|
+
const cached = connections.get(dbPath);
|
|
25001
|
+
if (cached) {
|
|
25002
|
+
try {
|
|
25003
|
+
cached.pragma("wal_checkpoint(TRUNCATE)");
|
|
25004
|
+
return "checkpointed";
|
|
25005
|
+
} catch {
|
|
25006
|
+
return "failed";
|
|
24317
25007
|
}
|
|
24318
|
-
} catch {
|
|
24319
|
-
return "skipped";
|
|
24320
25008
|
}
|
|
25009
|
+
let transient;
|
|
24321
25010
|
try {
|
|
24322
|
-
|
|
24323
|
-
|
|
24324
|
-
|
|
24325
|
-
{
|
|
24326
|
-
stdio: "ignore",
|
|
24327
|
-
timeout: 5e3
|
|
24328
|
-
}
|
|
24329
|
-
);
|
|
24330
|
-
return result.status === 0 ? "checkpointed" : "failed";
|
|
25011
|
+
transient = openEngine(dbPath);
|
|
25012
|
+
transient.pragma("wal_checkpoint(TRUNCATE)");
|
|
25013
|
+
return "checkpointed";
|
|
24331
25014
|
} catch {
|
|
24332
25015
|
return "failed";
|
|
25016
|
+
} finally {
|
|
25017
|
+
try {
|
|
25018
|
+
transient?.close();
|
|
25019
|
+
} catch {
|
|
25020
|
+
}
|
|
24333
25021
|
}
|
|
24334
25022
|
}
|
|
24335
25023
|
function closeDatabase(dbPath) {
|
|
@@ -24345,20 +25033,70 @@ function closeAllDatabases() {
|
|
|
24345
25033
|
connections.delete(path2);
|
|
24346
25034
|
}
|
|
24347
25035
|
}
|
|
24348
|
-
|
|
25036
|
+
function probeWrite() {
|
|
25037
|
+
let dir;
|
|
25038
|
+
try {
|
|
25039
|
+
dir = mkdtempSync(join14(tmpdir(), "ocr-probe-"));
|
|
25040
|
+
const db = openEngine(join14(dir, "probe.db"));
|
|
25041
|
+
try {
|
|
25042
|
+
db.run("CREATE TABLE _probe_write (id INTEGER PRIMARY KEY, v TEXT)");
|
|
25043
|
+
db.transaction(() => {
|
|
25044
|
+
db.run("INSERT INTO _probe_write (v) VALUES (?)", ["written-in-txn"]);
|
|
25045
|
+
});
|
|
25046
|
+
const value = db.exec("SELECT v FROM _probe_write")[0]?.values[0]?.[0];
|
|
25047
|
+
if (value !== "written-in-txn") {
|
|
25048
|
+
return { ok: false, error: `unexpected probe value: ${String(value)}` };
|
|
25049
|
+
}
|
|
25050
|
+
return { ok: true };
|
|
25051
|
+
} finally {
|
|
25052
|
+
db.close();
|
|
25053
|
+
}
|
|
25054
|
+
} catch (e) {
|
|
25055
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
25056
|
+
} finally {
|
|
25057
|
+
if (dir) rmDirBestEffort(dir);
|
|
25058
|
+
}
|
|
25059
|
+
}
|
|
25060
|
+
function rmDirBestEffort(dir) {
|
|
25061
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
25062
|
+
try {
|
|
25063
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25064
|
+
return;
|
|
25065
|
+
} catch {
|
|
25066
|
+
if (attempt === 2) return;
|
|
25067
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
25068
|
+
}
|
|
25069
|
+
}
|
|
25070
|
+
}
|
|
25071
|
+
var V2_SCHEMA_VERSION, connections;
|
|
24349
25072
|
var init_db = __esm({
|
|
24350
25073
|
"src/lib/db/index.ts"() {
|
|
24351
25074
|
"use strict";
|
|
25075
|
+
init_engine();
|
|
24352
25076
|
init_migrations();
|
|
25077
|
+
init_reconcile();
|
|
24353
25078
|
init_queries();
|
|
24354
25079
|
init_agent_sessions();
|
|
25080
|
+
init_liveness();
|
|
25081
|
+
init_exit_codes();
|
|
24355
25082
|
init_migrations();
|
|
24356
25083
|
init_result_mapper();
|
|
25084
|
+
init_engine();
|
|
25085
|
+
init_reconcile();
|
|
25086
|
+
init_migrations();
|
|
24357
25087
|
init_command_log();
|
|
25088
|
+
V2_SCHEMA_VERSION = 12;
|
|
24358
25089
|
connections = /* @__PURE__ */ new Map();
|
|
24359
25090
|
}
|
|
24360
25091
|
});
|
|
24361
25092
|
|
|
25093
|
+
// src/lib/runtime-guard.ts
|
|
25094
|
+
init_runtime_checks();
|
|
25095
|
+
if (!isSupportedNode(process.versions.node)) {
|
|
25096
|
+
process.stderr.write(nodeVersionGuardMessage(process.versions.node));
|
|
25097
|
+
process.exit(1);
|
|
25098
|
+
}
|
|
25099
|
+
|
|
24362
25100
|
// ../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
|
|
24363
25101
|
var import_index = __toESM(require_commander(), 1);
|
|
24364
25102
|
var {
|
|
@@ -28763,7 +29501,7 @@ ${hint}
|
|
|
28763
29501
|
}
|
|
28764
29502
|
|
|
28765
29503
|
// src/lib/version.ts
|
|
28766
|
-
var CLI_VERSION = true ? "1.
|
|
29504
|
+
var CLI_VERSION = true ? "2.1.0" : createRequire(import.meta.url)("../../package.json").version;
|
|
28767
29505
|
|
|
28768
29506
|
// ../shared/platform/src/index.ts
|
|
28769
29507
|
import { pathToFileURL } from "node:url";
|
|
@@ -29822,9 +30560,9 @@ var NodeFsHandler = class {
|
|
|
29822
30560
|
if (this.fsw.closed) {
|
|
29823
30561
|
return;
|
|
29824
30562
|
}
|
|
29825
|
-
const
|
|
30563
|
+
const dirname8 = sysPath.dirname(file);
|
|
29826
30564
|
const basename8 = sysPath.basename(file);
|
|
29827
|
-
const parent = this.fsw._getWatchedDir(
|
|
30565
|
+
const parent = this.fsw._getWatchedDir(dirname8);
|
|
29828
30566
|
let prevStats = stats;
|
|
29829
30567
|
if (parent.has(basename8))
|
|
29830
30568
|
return;
|
|
@@ -29851,7 +30589,7 @@ var NodeFsHandler = class {
|
|
|
29851
30589
|
prevStats = newStats2;
|
|
29852
30590
|
}
|
|
29853
30591
|
} catch (error) {
|
|
29854
|
-
this.fsw._remove(
|
|
30592
|
+
this.fsw._remove(dirname8, basename8);
|
|
29855
30593
|
}
|
|
29856
30594
|
} else if (parent.has(basename8)) {
|
|
29857
30595
|
const at = newStats.atimeMs;
|
|
@@ -30784,8 +31522,8 @@ function watch(paths, options = {}) {
|
|
|
30784
31522
|
}
|
|
30785
31523
|
|
|
30786
31524
|
// src/commands/progress.ts
|
|
30787
|
-
import { existsSync as
|
|
30788
|
-
import { join as
|
|
31525
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
31526
|
+
import { join as join15, basename as basename7 } from "node:path";
|
|
30789
31527
|
|
|
30790
31528
|
// ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
|
|
30791
31529
|
import process12 from "node:process";
|
|
@@ -32421,15 +33159,15 @@ function debounce(fn, delay) {
|
|
|
32421
33159
|
};
|
|
32422
33160
|
}
|
|
32423
33161
|
function findLatestActiveSession(sessionsDir) {
|
|
32424
|
-
if (!
|
|
33162
|
+
if (!existsSync13(sessionsDir)) {
|
|
32425
33163
|
return null;
|
|
32426
33164
|
}
|
|
32427
33165
|
const sessions = readdirSync5(sessionsDir).filter((name) => {
|
|
32428
|
-
const sessionPath =
|
|
32429
|
-
return
|
|
33166
|
+
const sessionPath = join15(sessionsDir, name);
|
|
33167
|
+
return statSync2(sessionPath).isDirectory();
|
|
32430
33168
|
}).sort().reverse();
|
|
32431
33169
|
for (const session of sessions) {
|
|
32432
|
-
const sessionPath =
|
|
33170
|
+
const sessionPath = join15(sessionsDir, session);
|
|
32433
33171
|
if (isSessionActive(sessionPath)) {
|
|
32434
33172
|
return session;
|
|
32435
33173
|
}
|
|
@@ -32444,8 +33182,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
|
|
|
32444
33182
|
return getStrategy(workflowType) ?? null;
|
|
32445
33183
|
}
|
|
32446
33184
|
async function initProgressDb(ocrDir) {
|
|
32447
|
-
const dbPath =
|
|
32448
|
-
if (!
|
|
33185
|
+
const dbPath = join15(ocrDir, "data", "ocr.db");
|
|
33186
|
+
if (!existsSync13(dbPath)) {
|
|
32449
33187
|
return;
|
|
32450
33188
|
}
|
|
32451
33189
|
try {
|
|
@@ -32470,11 +33208,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32470
33208
|
const targetDir = process.cwd();
|
|
32471
33209
|
requireOcrSetup(targetDir);
|
|
32472
33210
|
const sessionsDir = ensureSessionsDir(targetDir);
|
|
32473
|
-
const ocrDir =
|
|
33211
|
+
const ocrDir = join15(targetDir, ".ocr");
|
|
32474
33212
|
await initProgressDb(ocrDir);
|
|
32475
33213
|
if (options.session) {
|
|
32476
|
-
const sessionPath =
|
|
32477
|
-
if (!
|
|
33214
|
+
const sessionPath = join15(sessionsDir, options.session);
|
|
33215
|
+
if (!existsSync13(sessionPath)) {
|
|
32478
33216
|
console.error(source_default.red(`Session not found: ${options.session}`));
|
|
32479
33217
|
process.exit(1);
|
|
32480
33218
|
}
|
|
@@ -32499,7 +33237,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32499
33237
|
);
|
|
32500
33238
|
console.error(
|
|
32501
33239
|
source_default.dim(
|
|
32502
|
-
`The orchestrating agent must create state via 'ocr state
|
|
33240
|
+
`The orchestrating agent must create state via 'ocr state begin' for progress tracking.`
|
|
32503
33241
|
)
|
|
32504
33242
|
);
|
|
32505
33243
|
process.exit(1);
|
|
@@ -32535,7 +33273,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32535
33273
|
return;
|
|
32536
33274
|
}
|
|
32537
33275
|
let currentSession = findLatestActiveSession(sessionsDir);
|
|
32538
|
-
let currentSessionPath = currentSession ?
|
|
33276
|
+
let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
|
|
32539
33277
|
let sessionWatcher = null;
|
|
32540
33278
|
const preservedStartTimes = {
|
|
32541
33279
|
review: void 0,
|
|
@@ -32543,11 +33281,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32543
33281
|
};
|
|
32544
33282
|
let currentStrategy = null;
|
|
32545
33283
|
const updateDisplayImpl = () => {
|
|
32546
|
-
if (!currentSessionPath || !
|
|
33284
|
+
if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
|
|
32547
33285
|
const latestActive = findLatestActiveSession(sessionsDir);
|
|
32548
33286
|
if (latestActive && latestActive !== currentSession) {
|
|
32549
33287
|
currentSession = latestActive;
|
|
32550
|
-
currentSessionPath =
|
|
33288
|
+
currentSessionPath = join15(sessionsDir, latestActive);
|
|
32551
33289
|
preservedStartTimes.review = void 0;
|
|
32552
33290
|
preservedStartTimes.map = void 0;
|
|
32553
33291
|
currentStrategy = null;
|
|
@@ -32560,7 +33298,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32560
33298
|
currentStrategy = null;
|
|
32561
33299
|
}
|
|
32562
33300
|
}
|
|
32563
|
-
if (currentSessionPath &&
|
|
33301
|
+
if (currentSessionPath && existsSync13(currentSessionPath)) {
|
|
32564
33302
|
if (!options.workflow) {
|
|
32565
33303
|
const activeWorkflows = detectActiveWorkflows(currentSessionPath);
|
|
32566
33304
|
if (activeWorkflows.length > 1) {
|
|
@@ -32614,15 +33352,15 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32614
33352
|
watchSession(currentSessionPath);
|
|
32615
33353
|
}
|
|
32616
33354
|
const timerInterval = setInterval(updateDisplay, 1e3);
|
|
32617
|
-
const watchDir =
|
|
33355
|
+
const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
|
|
32618
33356
|
const dirWatcher = watch(watchDir, {
|
|
32619
33357
|
persistent: true,
|
|
32620
33358
|
ignoreInitial: true,
|
|
32621
33359
|
depth: 3
|
|
32622
33360
|
});
|
|
32623
33361
|
dirWatcher.on("addDir", (dirPath) => {
|
|
32624
|
-
const parentDir =
|
|
32625
|
-
const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(
|
|
33362
|
+
const parentDir = join15(dirPath, "..");
|
|
33363
|
+
const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join15(".ocr", "sessions"));
|
|
32626
33364
|
if (isDirectChild && !dirPath.endsWith("sessions")) {
|
|
32627
33365
|
const newSession = basename7(dirPath);
|
|
32628
33366
|
currentSession = newSession;
|
|
@@ -32724,226 +33462,115 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
|
|
|
32724
33462
|
}
|
|
32725
33463
|
|
|
32726
33464
|
// src/commands/state.ts
|
|
32727
|
-
import { existsSync as
|
|
32728
|
-
import { join as
|
|
33465
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
33466
|
+
import { join as join17 } from "node:path";
|
|
32729
33467
|
|
|
32730
33468
|
// src/lib/state/index.ts
|
|
32731
33469
|
init_db();
|
|
33470
|
+
init_exit_codes();
|
|
32732
33471
|
import {
|
|
32733
|
-
existsSync as
|
|
33472
|
+
existsSync as existsSync14,
|
|
32734
33473
|
mkdirSync as mkdirSync5,
|
|
32735
33474
|
readdirSync as readdirSync6,
|
|
32736
|
-
readFileSync as
|
|
32737
|
-
statSync as
|
|
32738
|
-
writeFileSync as
|
|
33475
|
+
readFileSync as readFileSync10,
|
|
33476
|
+
statSync as statSync3,
|
|
33477
|
+
writeFileSync as writeFileSync7
|
|
32739
33478
|
} from "node:fs";
|
|
32740
|
-
import { join as
|
|
32741
|
-
|
|
32742
|
-
|
|
32743
|
-
|
|
32744
|
-
|
|
32745
|
-
|
|
32746
|
-
|
|
32747
|
-
|
|
32748
|
-
|
|
32749
|
-
|
|
32750
|
-
|
|
32751
|
-
|
|
32752
|
-
|
|
32753
|
-
}
|
|
32754
|
-
|
|
32755
|
-
|
|
32756
|
-
|
|
32757
|
-
|
|
32758
|
-
|
|
32759
|
-
|
|
32760
|
-
|
|
32761
|
-
|
|
32762
|
-
|
|
32763
|
-
|
|
32764
|
-
|
|
32765
|
-
|
|
32766
|
-
|
|
32767
|
-
|
|
32768
|
-
|
|
32769
|
-
|
|
32770
|
-
}
|
|
32771
|
-
}
|
|
32772
|
-
updateSession(db, sessionId, {
|
|
32773
|
-
status: "active",
|
|
32774
|
-
current_phase: "context",
|
|
32775
|
-
phase_number: 1,
|
|
32776
|
-
current_round: nextRound
|
|
32777
|
-
});
|
|
32778
|
-
insertEvent(db, {
|
|
32779
|
-
session_id: sessionId,
|
|
32780
|
-
event_type: nextRound > (existing.current_round ?? 1) ? "round_started" : "session_resumed",
|
|
32781
|
-
phase: "context",
|
|
32782
|
-
phase_number: 1,
|
|
32783
|
-
round: nextRound
|
|
32784
|
-
});
|
|
32785
|
-
saveDatabase(db, dbPath);
|
|
32786
|
-
return sessionId;
|
|
32787
|
-
}
|
|
32788
|
-
insertSession(db, {
|
|
32789
|
-
id: sessionId,
|
|
32790
|
-
branch,
|
|
32791
|
-
workflow_type: workflowType,
|
|
32792
|
-
current_phase: "context",
|
|
32793
|
-
phase_number: 1,
|
|
32794
|
-
current_round: 1,
|
|
32795
|
-
current_map_run: 1,
|
|
32796
|
-
session_dir: sessionDir
|
|
32797
|
-
});
|
|
32798
|
-
insertEvent(db, {
|
|
32799
|
-
session_id: sessionId,
|
|
32800
|
-
event_type: "session_created",
|
|
32801
|
-
phase: "context",
|
|
32802
|
-
phase_number: 1,
|
|
32803
|
-
round: 1
|
|
32804
|
-
});
|
|
32805
|
-
saveDatabase(db, dbPath);
|
|
32806
|
-
return sessionId;
|
|
32807
|
-
}
|
|
32808
|
-
async function stateTransition(params) {
|
|
32809
|
-
const { sessionId, phase, phaseNumber, round, mapRun, ocrDir } = params;
|
|
32810
|
-
const db = await ensureDatabase(ocrDir);
|
|
32811
|
-
const dbPath = join15(ocrDir, "data", "ocr.db");
|
|
32812
|
-
const existing = getSession(db, sessionId);
|
|
32813
|
-
if (!existing) {
|
|
32814
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
32815
|
-
}
|
|
32816
|
-
const previousRound = existing.current_round;
|
|
32817
|
-
updateSession(db, sessionId, {
|
|
32818
|
-
current_phase: phase,
|
|
32819
|
-
phase_number: phaseNumber,
|
|
32820
|
-
...round !== void 0 ? { current_round: round } : {},
|
|
32821
|
-
...mapRun !== void 0 ? { current_map_run: mapRun } : {}
|
|
32822
|
-
});
|
|
32823
|
-
insertEvent(db, {
|
|
32824
|
-
session_id: sessionId,
|
|
32825
|
-
event_type: "phase_transition",
|
|
32826
|
-
phase,
|
|
32827
|
-
phase_number: phaseNumber,
|
|
32828
|
-
round: round ?? existing.current_round
|
|
32829
|
-
});
|
|
32830
|
-
if (round !== void 0 && round !== previousRound) {
|
|
32831
|
-
insertEvent(db, {
|
|
32832
|
-
session_id: sessionId,
|
|
32833
|
-
event_type: "round_started",
|
|
32834
|
-
phase,
|
|
32835
|
-
phase_number: phaseNumber,
|
|
32836
|
-
round
|
|
32837
|
-
});
|
|
33479
|
+
import { join as join16 } from "node:path";
|
|
33480
|
+
|
|
33481
|
+
// src/lib/state/phase-graph.ts
|
|
33482
|
+
init_exit_codes();
|
|
33483
|
+
var REVIEW_PHASE_NUMBERS = {
|
|
33484
|
+
context: 1,
|
|
33485
|
+
"change-context": 2,
|
|
33486
|
+
analysis: 3,
|
|
33487
|
+
reviews: 4,
|
|
33488
|
+
aggregation: 5,
|
|
33489
|
+
discourse: 6,
|
|
33490
|
+
synthesis: 7,
|
|
33491
|
+
complete: 8
|
|
33492
|
+
};
|
|
33493
|
+
var MAP_PHASE_NUMBERS = {
|
|
33494
|
+
"map-context": 1,
|
|
33495
|
+
topology: 2,
|
|
33496
|
+
"flow-analysis": 3,
|
|
33497
|
+
"requirements-mapping": 4,
|
|
33498
|
+
synthesis: 5,
|
|
33499
|
+
complete: 6
|
|
33500
|
+
};
|
|
33501
|
+
function phaseNumberFor(workflowType, phase) {
|
|
33502
|
+
const map = workflowType === "map" ? MAP_PHASE_NUMBERS : REVIEW_PHASE_NUMBERS;
|
|
33503
|
+
const n = map[phase];
|
|
33504
|
+
if (n === void 0) {
|
|
33505
|
+
throw new StateError(
|
|
33506
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33507
|
+
`Invalid phase "${phase}" for workflow_type "${workflowType}". Valid: ${Object.keys(map).join(", ")}`
|
|
33508
|
+
);
|
|
32838
33509
|
}
|
|
32839
|
-
|
|
33510
|
+
return n;
|
|
32840
33511
|
}
|
|
32841
|
-
|
|
32842
|
-
|
|
32843
|
-
|
|
32844
|
-
|
|
32845
|
-
|
|
32846
|
-
|
|
32847
|
-
|
|
32848
|
-
|
|
32849
|
-
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
|
|
32855
|
-
|
|
32856
|
-
|
|
32857
|
-
|
|
32858
|
-
|
|
32859
|
-
|
|
32860
|
-
|
|
33512
|
+
var REVIEW_PHASE_GRAPH = {
|
|
33513
|
+
context: ["change-context"],
|
|
33514
|
+
"change-context": ["analysis"],
|
|
33515
|
+
analysis: ["reviews"],
|
|
33516
|
+
reviews: ["aggregation"],
|
|
33517
|
+
aggregation: ["discourse"],
|
|
33518
|
+
discourse: ["synthesis"],
|
|
33519
|
+
synthesis: ["complete"],
|
|
33520
|
+
complete: ["context"]
|
|
33521
|
+
};
|
|
33522
|
+
var MAP_PHASE_GRAPH = {
|
|
33523
|
+
"map-context": ["topology"],
|
|
33524
|
+
topology: ["flow-analysis"],
|
|
33525
|
+
"flow-analysis": ["requirements-mapping"],
|
|
33526
|
+
"requirements-mapping": ["synthesis"],
|
|
33527
|
+
synthesis: ["complete"],
|
|
33528
|
+
complete: ["map-context"]
|
|
33529
|
+
};
|
|
33530
|
+
function graphFor(workflowType) {
|
|
33531
|
+
return workflowType === "review" ? REVIEW_PHASE_GRAPH : MAP_PHASE_GRAPH;
|
|
32861
33532
|
}
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
try {
|
|
32865
|
-
db = await ensureDatabase(ocrDir);
|
|
32866
|
-
} catch {
|
|
32867
|
-
return null;
|
|
32868
|
-
}
|
|
32869
|
-
const session = sessionId ? getSession(db, sessionId) : getLatestActiveSession(db);
|
|
32870
|
-
if (!session) {
|
|
32871
|
-
return null;
|
|
32872
|
-
}
|
|
32873
|
-
const events = getEventsForSession(db, session.id);
|
|
32874
|
-
return {
|
|
32875
|
-
session: {
|
|
32876
|
-
id: session.id,
|
|
32877
|
-
branch: session.branch,
|
|
32878
|
-
status: session.status,
|
|
32879
|
-
workflow_type: session.workflow_type,
|
|
32880
|
-
current_phase: session.current_phase,
|
|
32881
|
-
phase_number: session.phase_number,
|
|
32882
|
-
current_round: session.current_round,
|
|
32883
|
-
current_map_run: session.current_map_run,
|
|
32884
|
-
started_at: session.started_at,
|
|
32885
|
-
updated_at: session.updated_at
|
|
32886
|
-
},
|
|
32887
|
-
events: events.map((e) => ({
|
|
32888
|
-
id: e.id,
|
|
32889
|
-
event_type: e.event_type,
|
|
32890
|
-
phase: e.phase,
|
|
32891
|
-
phase_number: e.phase_number,
|
|
32892
|
-
round: e.round,
|
|
32893
|
-
metadata: e.metadata,
|
|
32894
|
-
created_at: e.created_at
|
|
32895
|
-
}))
|
|
32896
|
-
};
|
|
33533
|
+
function initialPhaseFor(workflowType) {
|
|
33534
|
+
return workflowType === "map" ? "map-context" : "context";
|
|
32897
33535
|
}
|
|
32898
|
-
|
|
32899
|
-
const
|
|
32900
|
-
|
|
32901
|
-
|
|
32902
|
-
throw new
|
|
33536
|
+
function validatePhaseTransition(workflowType, source, target, isRoundBoundary) {
|
|
33537
|
+
const graph = graphFor(workflowType);
|
|
33538
|
+
if (!(target in graph)) {
|
|
33539
|
+
const validPhases = Object.keys(graph).join(", ");
|
|
33540
|
+
throw new StateError(
|
|
33541
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33542
|
+
`Invalid phase "${target}" for workflow_type "${workflowType}". Valid phases: ${validPhases}`
|
|
33543
|
+
);
|
|
32903
33544
|
}
|
|
32904
|
-
return
|
|
32905
|
-
|
|
32906
|
-
|
|
32907
|
-
|
|
32908
|
-
|
|
32909
|
-
|
|
32910
|
-
|
|
32911
|
-
|
|
32912
|
-
throw new Error(`File not found: ${params.filePath}`);
|
|
32913
|
-
}
|
|
32914
|
-
return readFileSync11(params.filePath, "utf-8");
|
|
33545
|
+
if (source === target) return;
|
|
33546
|
+
if (isRoundBoundary) {
|
|
33547
|
+
const initial = initialPhaseFor(workflowType);
|
|
33548
|
+
if (target === initial) return;
|
|
33549
|
+
throw new StateError(
|
|
33550
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33551
|
+
`Illegal round-boundary transition: a new round/run must reset to "${initial}", not "${target}".`
|
|
33552
|
+
);
|
|
32915
33553
|
}
|
|
32916
|
-
|
|
32917
|
-
|
|
32918
|
-
|
|
32919
|
-
|
|
32920
|
-
|
|
32921
|
-
} catch (err) {
|
|
32922
|
-
throw new Error(
|
|
32923
|
-
`Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
|
|
33554
|
+
const allowed = graph[source];
|
|
33555
|
+
if (!allowed || !allowed.includes(target)) {
|
|
33556
|
+
throw new StateError(
|
|
33557
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33558
|
+
`Illegal phase transition: ${source} \u2192 ${target}. From "${source}", only ${allowed && allowed.length > 0 ? allowed.join(", ") : "(no edges)"} are reachable. Pass --current-round to start a new round if the workflow is resetting.`
|
|
32924
33559
|
);
|
|
32925
33560
|
}
|
|
32926
33561
|
}
|
|
32927
|
-
|
|
32928
|
-
|
|
32929
|
-
|
|
32930
|
-
|
|
32931
|
-
|
|
32932
|
-
|
|
32933
|
-
|
|
32934
|
-
|
|
32935
|
-
|
|
32936
|
-
};
|
|
32937
|
-
}
|
|
32938
|
-
const active = getLatestActiveSession(db);
|
|
32939
|
-
if (!active) throw new Error("No active session found");
|
|
32940
|
-
return {
|
|
32941
|
-
id: active.id,
|
|
32942
|
-
session_dir: active.session_dir,
|
|
32943
|
-
current_round: active.current_round,
|
|
32944
|
-
current_map_run: active.current_map_run
|
|
32945
|
-
};
|
|
33562
|
+
|
|
33563
|
+
// src/lib/state/meta-util.ts
|
|
33564
|
+
var DEFAULT_METADATA_MAX_LEN = 4096;
|
|
33565
|
+
function sanitizeMetadataString(s, opts = {}) {
|
|
33566
|
+
const maxLen = opts.maxLen ?? DEFAULT_METADATA_MAX_LEN;
|
|
33567
|
+
let out = s.replace(/[\x00-\x08\x0b-\x1f]/g, "");
|
|
33568
|
+
out = out.replace(/^\s*\[ocr\]\s*/i, "");
|
|
33569
|
+
if (out.length > maxLen) out = out.slice(0, maxLen);
|
|
33570
|
+
return out;
|
|
32946
33571
|
}
|
|
33572
|
+
|
|
33573
|
+
// src/lib/state/round-meta.ts
|
|
32947
33574
|
var VALID_CATEGORIES = /* @__PURE__ */ new Set(["blocker", "should_fix", "suggestion", "style"]);
|
|
32948
33575
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
|
|
32949
33576
|
function validateRoundMeta(meta) {
|
|
@@ -32959,6 +33586,7 @@ function validateRoundMeta(meta) {
|
|
|
32959
33586
|
if (typeof obj.verdict !== "string" || obj.verdict.trim().length === 0) {
|
|
32960
33587
|
throw new Error("round-meta.json must contain a non-empty verdict string");
|
|
32961
33588
|
}
|
|
33589
|
+
obj.verdict = sanitizeMetadataString(obj.verdict);
|
|
32962
33590
|
if (!Array.isArray(obj.reviewers)) {
|
|
32963
33591
|
throw new Error("round-meta.json must contain a reviewers array");
|
|
32964
33592
|
}
|
|
@@ -32984,6 +33612,7 @@ function validateRoundMeta(meta) {
|
|
|
32984
33612
|
if (typeof f.title !== "string" || f.title.trim().length === 0) {
|
|
32985
33613
|
throw new Error("Each finding must have a non-empty title");
|
|
32986
33614
|
}
|
|
33615
|
+
f.title = sanitizeMetadataString(f.title);
|
|
32987
33616
|
if (typeof f.category !== "string" || !VALID_CATEGORIES.has(f.category)) {
|
|
32988
33617
|
throw new Error(
|
|
32989
33618
|
`Finding "${f.title}" has invalid category: "${String(f.category)}". Must be one of: ${[...VALID_CATEGORIES].join(", ")}`
|
|
@@ -32997,6 +33626,7 @@ function validateRoundMeta(meta) {
|
|
|
32997
33626
|
if (typeof f.summary !== "string") {
|
|
32998
33627
|
throw new Error(`Finding "${f.title}" must have a summary string`);
|
|
32999
33628
|
}
|
|
33629
|
+
f.summary = sanitizeMetadataString(f.summary);
|
|
33000
33630
|
if (f.file_path !== void 0 && typeof f.file_path !== "string") {
|
|
33001
33631
|
throw new Error(`Finding "${f.title}" has invalid file_path: expected string`);
|
|
33002
33632
|
}
|
|
@@ -33042,43 +33672,8 @@ function computeRoundCounts(meta) {
|
|
|
33042
33672
|
totalFindingCount: allFindings.length
|
|
33043
33673
|
};
|
|
33044
33674
|
}
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
const db = await ensureDatabase(ocrDir);
|
|
33048
|
-
const dbPath = join15(ocrDir, "data", "ocr.db");
|
|
33049
|
-
const rawJsonString = readJsonFromSource(params);
|
|
33050
|
-
const label = params.source === "file" ? params.filePath : "stdin";
|
|
33051
|
-
const raw = parseRawJson(rawJsonString, label);
|
|
33052
|
-
const meta = validateRoundMeta(raw);
|
|
33053
|
-
const counts = computeRoundCounts(meta);
|
|
33054
|
-
const session = resolveSessionForCompletion(db, params.sessionId);
|
|
33055
|
-
const roundNumber = params.round ?? session.current_round;
|
|
33056
|
-
let metaPath;
|
|
33057
|
-
if (params.source === "stdin") {
|
|
33058
|
-
const roundDir = join15(session.session_dir, "rounds", `round-${roundNumber}`);
|
|
33059
|
-
mkdirSync5(roundDir, { recursive: true });
|
|
33060
|
-
metaPath = join15(roundDir, "round-meta.json");
|
|
33061
|
-
writeFileSync8(metaPath, JSON.stringify(meta, null, 2));
|
|
33062
|
-
}
|
|
33063
|
-
insertEvent(db, {
|
|
33064
|
-
session_id: session.id,
|
|
33065
|
-
event_type: "round_completed",
|
|
33066
|
-
phase: "synthesis",
|
|
33067
|
-
phase_number: 7,
|
|
33068
|
-
round: roundNumber,
|
|
33069
|
-
metadata: JSON.stringify({
|
|
33070
|
-
verdict: meta.verdict,
|
|
33071
|
-
blocker_count: counts.blockerCount,
|
|
33072
|
-
should_fix_count: counts.shouldFixCount,
|
|
33073
|
-
suggestion_count: counts.suggestionCount,
|
|
33074
|
-
reviewer_count: counts.reviewerCount,
|
|
33075
|
-
total_finding_count: counts.totalFindingCount,
|
|
33076
|
-
source: "orchestrator"
|
|
33077
|
-
})
|
|
33078
|
-
});
|
|
33079
|
-
saveDatabase(db, dbPath);
|
|
33080
|
-
return { sessionId: session.id, round: roundNumber, metaPath };
|
|
33081
|
-
}
|
|
33675
|
+
|
|
33676
|
+
// src/lib/state/map-meta.ts
|
|
33082
33677
|
function validateMapMeta(meta) {
|
|
33083
33678
|
if (!meta || typeof meta !== "object") {
|
|
33084
33679
|
throw new Error("map-meta.json must be a JSON object");
|
|
@@ -33103,6 +33698,13 @@ function validateMapMeta(meta) {
|
|
|
33103
33698
|
if (typeof s.title !== "string" || s.title.trim().length === 0) {
|
|
33104
33699
|
throw new Error("Each section must have a non-empty title");
|
|
33105
33700
|
}
|
|
33701
|
+
s.title = sanitizeMetadataString(s.title);
|
|
33702
|
+
if (s.description !== void 0) {
|
|
33703
|
+
if (typeof s.description !== "string") {
|
|
33704
|
+
throw new Error(`Section "${s.title}" description must be a string if provided`);
|
|
33705
|
+
}
|
|
33706
|
+
s.description = sanitizeMetadataString(s.description);
|
|
33707
|
+
}
|
|
33106
33708
|
if (!Array.isArray(s.files)) {
|
|
33107
33709
|
throw new Error(`Section "${s.title}" must have a files array`);
|
|
33108
33710
|
}
|
|
@@ -33117,6 +33719,7 @@ function validateMapMeta(meta) {
|
|
|
33117
33719
|
if (typeof f.role !== "string") {
|
|
33118
33720
|
throw new Error(`File "${f.file_path}" must have a role string`);
|
|
33119
33721
|
}
|
|
33722
|
+
f.role = sanitizeMetadataString(f.role);
|
|
33120
33723
|
if (typeof f.lines_added !== "number") {
|
|
33121
33724
|
throw new Error(`File "${f.file_path}" must have a lines_added number`);
|
|
33122
33725
|
}
|
|
@@ -33136,53 +33739,597 @@ function computeMapCounts(meta) {
|
|
|
33136
33739
|
fileCount: meta.sections.reduce((sum, s) => sum + s.files.length, 0)
|
|
33137
33740
|
};
|
|
33138
33741
|
}
|
|
33139
|
-
|
|
33742
|
+
|
|
33743
|
+
// src/lib/state/projection.ts
|
|
33744
|
+
init_db();
|
|
33745
|
+
var REASON_EVENT_TYPES = [
|
|
33746
|
+
"session_aborted",
|
|
33747
|
+
"session_auto_closed_stale",
|
|
33748
|
+
"session_synced",
|
|
33749
|
+
"session_legacy_import"
|
|
33750
|
+
];
|
|
33751
|
+
var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
33752
|
+
"session_closed",
|
|
33753
|
+
...REASON_EVENT_TYPES
|
|
33754
|
+
]);
|
|
33755
|
+
function hasCompletionInvariant(db, session) {
|
|
33756
|
+
const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
|
|
33757
|
+
const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
|
|
33758
|
+
const r = db.exec(
|
|
33759
|
+
`SELECT 1 FROM orchestration_events
|
|
33760
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
33761
|
+
[session.id, eventType, round]
|
|
33762
|
+
);
|
|
33763
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
33764
|
+
}
|
|
33765
|
+
function getCompletenessState(db, sessionId) {
|
|
33766
|
+
const r = db.exec(
|
|
33767
|
+
"SELECT completeness_state FROM session_completeness WHERE session_id = ?",
|
|
33768
|
+
[sessionId]
|
|
33769
|
+
);
|
|
33770
|
+
return r[0]?.values[0]?.[0] ?? null;
|
|
33771
|
+
}
|
|
33772
|
+
|
|
33773
|
+
// src/lib/state/index.ts
|
|
33774
|
+
init_exit_codes();
|
|
33775
|
+
function deriveNextRound(db, sessionId, fallbackRound) {
|
|
33776
|
+
const result = db.exec(
|
|
33777
|
+
`SELECT MAX(round) FROM orchestration_events
|
|
33778
|
+
WHERE session_id = ? AND event_type = 'round_completed'`,
|
|
33779
|
+
[sessionId]
|
|
33780
|
+
);
|
|
33781
|
+
const max = result[0]?.values[0]?.[0];
|
|
33782
|
+
if (typeof max === "number") return max + 1;
|
|
33783
|
+
return fallbackRound;
|
|
33784
|
+
}
|
|
33785
|
+
function hasArtifacts(dir) {
|
|
33786
|
+
try {
|
|
33787
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
33788
|
+
if (entry.isDirectory()) {
|
|
33789
|
+
if (hasArtifacts(join16(dir, entry.name))) return true;
|
|
33790
|
+
} else if (/\.(md|json)$/.test(entry.name)) {
|
|
33791
|
+
return true;
|
|
33792
|
+
}
|
|
33793
|
+
}
|
|
33794
|
+
} catch {
|
|
33795
|
+
}
|
|
33796
|
+
return false;
|
|
33797
|
+
}
|
|
33798
|
+
function readJsonFromSource(params) {
|
|
33799
|
+
if (params.source === "file") {
|
|
33800
|
+
if (!existsSync14(params.filePath)) {
|
|
33801
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
|
|
33802
|
+
}
|
|
33803
|
+
return readFileSync10(params.filePath, "utf-8");
|
|
33804
|
+
}
|
|
33805
|
+
return params.data;
|
|
33806
|
+
}
|
|
33807
|
+
function parseRawJson(raw, label) {
|
|
33808
|
+
try {
|
|
33809
|
+
return JSON.parse(raw);
|
|
33810
|
+
} catch (err) {
|
|
33811
|
+
throw new StateError(
|
|
33812
|
+
STATE_EXIT.SCHEMA_INVALID,
|
|
33813
|
+
`Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
|
|
33814
|
+
);
|
|
33815
|
+
}
|
|
33816
|
+
}
|
|
33817
|
+
async function stateInit(params) {
|
|
33818
|
+
const { sessionId, branch, workflowType, sessionDir, ocrDir } = params;
|
|
33819
|
+
const db = await ensureDatabase(ocrDir);
|
|
33820
|
+
const existing = getSession(db, sessionId);
|
|
33821
|
+
if (existing) {
|
|
33822
|
+
if (existing.workflow_type !== workflowType) {
|
|
33823
|
+
throw new StateError(
|
|
33824
|
+
STATE_EXIT.USAGE,
|
|
33825
|
+
`Cannot re-open session ${sessionId} as workflow_type "${workflowType}": existing workflow_type is "${existing.workflow_type}". Maps and reviews have disjoint phase graphs.`
|
|
33826
|
+
);
|
|
33827
|
+
}
|
|
33828
|
+
const nextRound = deriveNextRound(db, sessionId, existing.current_round);
|
|
33829
|
+
const initialPhase2 = workflowType === "map" ? "map-context" : "context";
|
|
33830
|
+
db.transaction(() => {
|
|
33831
|
+
updateSession(db, sessionId, {
|
|
33832
|
+
status: "active",
|
|
33833
|
+
current_phase: initialPhase2,
|
|
33834
|
+
phase_number: 1,
|
|
33835
|
+
current_round: nextRound
|
|
33836
|
+
});
|
|
33837
|
+
insertEvent(db, {
|
|
33838
|
+
session_id: sessionId,
|
|
33839
|
+
event_type: nextRound > (existing.current_round ?? 1) ? "round_started" : "session_resumed",
|
|
33840
|
+
phase: initialPhase2,
|
|
33841
|
+
phase_number: 1,
|
|
33842
|
+
round: nextRound
|
|
33843
|
+
});
|
|
33844
|
+
});
|
|
33845
|
+
return sessionId;
|
|
33846
|
+
}
|
|
33847
|
+
const initialPhase = workflowType === "map" ? "map-context" : "context";
|
|
33848
|
+
db.transaction(() => {
|
|
33849
|
+
insertSession(db, {
|
|
33850
|
+
id: sessionId,
|
|
33851
|
+
branch,
|
|
33852
|
+
workflow_type: workflowType,
|
|
33853
|
+
current_phase: initialPhase,
|
|
33854
|
+
phase_number: 1,
|
|
33855
|
+
current_round: 1,
|
|
33856
|
+
current_map_run: 1,
|
|
33857
|
+
session_dir: sessionDir
|
|
33858
|
+
});
|
|
33859
|
+
insertEvent(db, {
|
|
33860
|
+
session_id: sessionId,
|
|
33861
|
+
event_type: "session_created",
|
|
33862
|
+
phase: initialPhase,
|
|
33863
|
+
phase_number: 1,
|
|
33864
|
+
round: 1
|
|
33865
|
+
});
|
|
33866
|
+
});
|
|
33867
|
+
return sessionId;
|
|
33868
|
+
}
|
|
33869
|
+
async function stateTransition(params, db) {
|
|
33870
|
+
const { sessionId, phase, phaseNumber, round, mapRun, ocrDir } = params;
|
|
33871
|
+
db ??= await ensureDatabase(ocrDir);
|
|
33872
|
+
const existing = getSession(db, sessionId);
|
|
33873
|
+
if (!existing) {
|
|
33874
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
|
|
33875
|
+
}
|
|
33876
|
+
const previousRound = existing.current_round;
|
|
33877
|
+
const previousMapRun = existing.current_map_run;
|
|
33878
|
+
const isRoundBoundary = round !== void 0 && round !== previousRound || mapRun !== void 0 && mapRun !== previousMapRun;
|
|
33879
|
+
validatePhaseTransition(
|
|
33880
|
+
existing.workflow_type,
|
|
33881
|
+
existing.current_phase,
|
|
33882
|
+
phase,
|
|
33883
|
+
isRoundBoundary
|
|
33884
|
+
);
|
|
33885
|
+
db.transaction(() => {
|
|
33886
|
+
updateSession(db, sessionId, {
|
|
33887
|
+
current_phase: phase,
|
|
33888
|
+
phase_number: phaseNumber,
|
|
33889
|
+
...round !== void 0 ? { current_round: round } : {},
|
|
33890
|
+
...mapRun !== void 0 ? { current_map_run: mapRun } : {}
|
|
33891
|
+
});
|
|
33892
|
+
insertEvent(db, {
|
|
33893
|
+
session_id: sessionId,
|
|
33894
|
+
event_type: "phase_transition",
|
|
33895
|
+
phase,
|
|
33896
|
+
phase_number: phaseNumber,
|
|
33897
|
+
round: round ?? existing.current_round
|
|
33898
|
+
});
|
|
33899
|
+
if (round !== void 0 && round !== previousRound) {
|
|
33900
|
+
insertEvent(db, {
|
|
33901
|
+
session_id: sessionId,
|
|
33902
|
+
event_type: "round_started",
|
|
33903
|
+
phase,
|
|
33904
|
+
phase_number: phaseNumber,
|
|
33905
|
+
round
|
|
33906
|
+
});
|
|
33907
|
+
}
|
|
33908
|
+
});
|
|
33909
|
+
}
|
|
33910
|
+
async function stateClose(params) {
|
|
33911
|
+
const { sessionId, ocrDir, abort } = params;
|
|
33912
|
+
const db = await ensureDatabase(ocrDir);
|
|
33913
|
+
const existing = getSession(db, sessionId);
|
|
33914
|
+
if (!existing) {
|
|
33915
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
|
|
33916
|
+
}
|
|
33917
|
+
if (existing.status === "closed") {
|
|
33918
|
+
console.error(`[ocr] Session already closed: ${sessionId}`);
|
|
33919
|
+
return;
|
|
33920
|
+
}
|
|
33921
|
+
if (!abort && !hasCompletionInvariant(db, existing)) {
|
|
33922
|
+
const what = existing.workflow_type === "map" ? `map run ${existing.current_map_run} has no map_completed event` : `round ${existing.current_round} has no round_completed event`;
|
|
33923
|
+
throw new StateError(
|
|
33924
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
33925
|
+
`Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
|
|
33926
|
+
);
|
|
33927
|
+
}
|
|
33928
|
+
const note = "closed by parent workflow close";
|
|
33929
|
+
db.transaction(() => {
|
|
33930
|
+
if (abort) {
|
|
33931
|
+
insertEvent(db, {
|
|
33932
|
+
session_id: sessionId,
|
|
33933
|
+
event_type: "session_aborted",
|
|
33934
|
+
phase: existing.current_phase,
|
|
33935
|
+
phase_number: existing.phase_number,
|
|
33936
|
+
round: existing.current_round
|
|
33937
|
+
});
|
|
33938
|
+
}
|
|
33939
|
+
updateSession(db, sessionId, {
|
|
33940
|
+
status: "closed",
|
|
33941
|
+
current_phase: "complete"
|
|
33942
|
+
});
|
|
33943
|
+
if (!abort) {
|
|
33944
|
+
insertEvent(db, {
|
|
33945
|
+
session_id: sessionId,
|
|
33946
|
+
event_type: "session_closed",
|
|
33947
|
+
phase: "complete",
|
|
33948
|
+
phase_number: existing.phase_number,
|
|
33949
|
+
round: existing.current_round
|
|
33950
|
+
});
|
|
33951
|
+
}
|
|
33952
|
+
cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
|
|
33953
|
+
});
|
|
33954
|
+
}
|
|
33955
|
+
async function stateShow(ocrDir, sessionId) {
|
|
33956
|
+
let db;
|
|
33957
|
+
try {
|
|
33958
|
+
db = await ensureDatabase(ocrDir);
|
|
33959
|
+
} catch {
|
|
33960
|
+
return null;
|
|
33961
|
+
}
|
|
33962
|
+
const session = sessionId ? getSession(db, sessionId) : getLatestActiveSession(db);
|
|
33963
|
+
if (!session) {
|
|
33964
|
+
return null;
|
|
33965
|
+
}
|
|
33966
|
+
const events = getEventsForSession(db, session.id);
|
|
33967
|
+
return {
|
|
33968
|
+
session: {
|
|
33969
|
+
id: session.id,
|
|
33970
|
+
branch: session.branch,
|
|
33971
|
+
status: session.status,
|
|
33972
|
+
workflow_type: session.workflow_type,
|
|
33973
|
+
current_phase: session.current_phase,
|
|
33974
|
+
phase_number: session.phase_number,
|
|
33975
|
+
current_round: session.current_round,
|
|
33976
|
+
current_map_run: session.current_map_run,
|
|
33977
|
+
started_at: session.started_at,
|
|
33978
|
+
updated_at: session.updated_at
|
|
33979
|
+
},
|
|
33980
|
+
events: events.map((e) => ({
|
|
33981
|
+
id: e.id,
|
|
33982
|
+
event_type: e.event_type,
|
|
33983
|
+
phase: e.phase,
|
|
33984
|
+
phase_number: e.phase_number,
|
|
33985
|
+
round: e.round,
|
|
33986
|
+
metadata: e.metadata,
|
|
33987
|
+
created_at: e.created_at
|
|
33988
|
+
}))
|
|
33989
|
+
};
|
|
33990
|
+
}
|
|
33991
|
+
function resolveSession(db, explicitId) {
|
|
33992
|
+
if (explicitId) {
|
|
33993
|
+
const s = getSession(db, explicitId);
|
|
33994
|
+
if (!s) throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${explicitId}`);
|
|
33995
|
+
return {
|
|
33996
|
+
id: s.id,
|
|
33997
|
+
session_dir: s.session_dir,
|
|
33998
|
+
current_round: s.current_round,
|
|
33999
|
+
current_map_run: s.current_map_run,
|
|
34000
|
+
workflow_type: s.workflow_type,
|
|
34001
|
+
status: s.status,
|
|
34002
|
+
current_phase: s.current_phase,
|
|
34003
|
+
phase_number: s.phase_number,
|
|
34004
|
+
branch: s.branch,
|
|
34005
|
+
decision: "explicit"
|
|
34006
|
+
};
|
|
34007
|
+
}
|
|
34008
|
+
const uid = process.env["OCR_DASHBOARD_EXECUTION_UID"];
|
|
34009
|
+
if (uid) {
|
|
34010
|
+
const result = db.exec(
|
|
34011
|
+
"SELECT workflow_id FROM command_executions WHERE uid = ?",
|
|
34012
|
+
[uid]
|
|
34013
|
+
);
|
|
34014
|
+
const workflowId = result[0]?.values[0]?.[0];
|
|
34015
|
+
if (workflowId) {
|
|
34016
|
+
const s = getSession(db, workflowId);
|
|
34017
|
+
if (s) {
|
|
34018
|
+
return {
|
|
34019
|
+
id: s.id,
|
|
34020
|
+
session_dir: s.session_dir,
|
|
34021
|
+
current_round: s.current_round,
|
|
34022
|
+
current_map_run: s.current_map_run,
|
|
34023
|
+
workflow_type: s.workflow_type,
|
|
34024
|
+
status: s.status,
|
|
34025
|
+
current_phase: s.current_phase,
|
|
34026
|
+
phase_number: s.phase_number,
|
|
34027
|
+
branch: s.branch,
|
|
34028
|
+
decision: "dashboard-uid"
|
|
34029
|
+
};
|
|
34030
|
+
}
|
|
34031
|
+
}
|
|
34032
|
+
}
|
|
34033
|
+
const activeRows = db.exec(
|
|
34034
|
+
`SELECT id, session_dir, current_round, current_map_run, workflow_type,
|
|
34035
|
+
status, current_phase, phase_number, branch
|
|
34036
|
+
FROM sessions
|
|
34037
|
+
WHERE status = 'active'
|
|
34038
|
+
ORDER BY started_at DESC`
|
|
34039
|
+
);
|
|
34040
|
+
const rows = activeRows[0]?.values ?? [];
|
|
34041
|
+
if (rows.length === 0) throw new StateError(STATE_EXIT.NOT_FOUND, "No active session found");
|
|
34042
|
+
if (rows.length > 1) {
|
|
34043
|
+
const ids = rows.map((r) => r[0]);
|
|
34044
|
+
throw new StateError(
|
|
34045
|
+
STATE_EXIT.AMBIGUOUS,
|
|
34046
|
+
`Ambiguous auto-detect: ${rows.length} active sessions exist. Pass --session-id explicitly. Candidates: ${ids.join(", ")}`
|
|
34047
|
+
);
|
|
34048
|
+
}
|
|
34049
|
+
const row = rows[0];
|
|
34050
|
+
return {
|
|
34051
|
+
id: row[0],
|
|
34052
|
+
session_dir: row[1],
|
|
34053
|
+
current_round: row[2],
|
|
34054
|
+
current_map_run: row[3],
|
|
34055
|
+
workflow_type: row[4],
|
|
34056
|
+
status: row[5],
|
|
34057
|
+
current_phase: row[6],
|
|
34058
|
+
phase_number: row[7],
|
|
34059
|
+
branch: row[8],
|
|
34060
|
+
decision: "latest-active"
|
|
34061
|
+
};
|
|
34062
|
+
}
|
|
34063
|
+
function announceResolveDecision(r) {
|
|
34064
|
+
if (r.decision === "explicit") return;
|
|
34065
|
+
const path2 = r.decision === "dashboard-uid" ? "via OCR_DASHBOARD_EXECUTION_UID" : "via latest-active";
|
|
34066
|
+
console.error(`[ocr] Auto-detected session: ${r.id} (${path2})`);
|
|
34067
|
+
}
|
|
34068
|
+
async function resolveActiveSession(ocrDir, explicitId) {
|
|
34069
|
+
const db = await ensureDatabase(ocrDir);
|
|
34070
|
+
const result = resolveSession(db, explicitId);
|
|
34071
|
+
announceResolveDecision(result);
|
|
34072
|
+
return {
|
|
34073
|
+
id: result.id,
|
|
34074
|
+
sessionDir: result.session_dir,
|
|
34075
|
+
decision: result.decision
|
|
34076
|
+
};
|
|
34077
|
+
}
|
|
34078
|
+
async function stateBegin(params) {
|
|
34079
|
+
const id = await stateInit(params);
|
|
34080
|
+
const db = await ensureDatabase(params.ocrDir);
|
|
34081
|
+
const s = getSession(db, id);
|
|
34082
|
+
return {
|
|
34083
|
+
schema_version: 1,
|
|
34084
|
+
session_id: id,
|
|
34085
|
+
round: s?.current_round ?? 1,
|
|
34086
|
+
phase: s?.current_phase ?? "context",
|
|
34087
|
+
completeness: getCompletenessState(db, id)
|
|
34088
|
+
};
|
|
34089
|
+
}
|
|
34090
|
+
async function stateAdvance(params) {
|
|
34091
|
+
const db = await ensureDatabase(params.ocrDir);
|
|
34092
|
+
const existing = getSession(db, params.sessionId);
|
|
34093
|
+
if (!existing) {
|
|
34094
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${params.sessionId}`);
|
|
34095
|
+
}
|
|
34096
|
+
const phaseNumber = phaseNumberFor(existing.workflow_type, params.phase);
|
|
34097
|
+
await stateTransition(
|
|
34098
|
+
{
|
|
34099
|
+
sessionId: params.sessionId,
|
|
34100
|
+
phase: params.phase,
|
|
34101
|
+
phaseNumber,
|
|
34102
|
+
round: params.round,
|
|
34103
|
+
mapRun: params.mapRun,
|
|
34104
|
+
ocrDir: params.ocrDir
|
|
34105
|
+
},
|
|
34106
|
+
db
|
|
34107
|
+
);
|
|
34108
|
+
}
|
|
34109
|
+
async function stateCompleteRound(params) {
|
|
33140
34110
|
const { ocrDir } = params;
|
|
33141
34111
|
const db = await ensureDatabase(ocrDir);
|
|
33142
|
-
|
|
33143
|
-
|
|
33144
|
-
|
|
33145
|
-
|
|
33146
|
-
|
|
33147
|
-
|
|
33148
|
-
|
|
33149
|
-
|
|
34112
|
+
let meta;
|
|
34113
|
+
let counts;
|
|
34114
|
+
try {
|
|
34115
|
+
const rawJsonString = readJsonFromSource(params);
|
|
34116
|
+
const label = params.source === "file" ? params.filePath : "stdin";
|
|
34117
|
+
meta = validateRoundMeta(parseRawJson(rawJsonString, label));
|
|
34118
|
+
counts = computeRoundCounts(meta);
|
|
34119
|
+
} catch (e) {
|
|
34120
|
+
throw new StateError(
|
|
34121
|
+
STATE_EXIT.SCHEMA_INVALID,
|
|
34122
|
+
e instanceof Error ? e.message : "invalid round metadata"
|
|
34123
|
+
);
|
|
34124
|
+
}
|
|
34125
|
+
const resolved = resolveSession(db, params.sessionId);
|
|
34126
|
+
const roundNumber = params.round ?? resolved.current_round;
|
|
34127
|
+
const roundMetaPath = join16(
|
|
34128
|
+
resolved.session_dir,
|
|
34129
|
+
"rounds",
|
|
34130
|
+
`round-${roundNumber}`,
|
|
34131
|
+
"round-meta.json"
|
|
34132
|
+
);
|
|
34133
|
+
const already = db.exec(
|
|
34134
|
+
`SELECT 1 FROM orchestration_events
|
|
34135
|
+
WHERE session_id = ? AND event_type = 'round_completed' AND round = ? LIMIT 1`,
|
|
34136
|
+
[resolved.id, roundNumber]
|
|
34137
|
+
);
|
|
34138
|
+
if ((already[0]?.values.length ?? 0) > 0) {
|
|
34139
|
+
return { sessionId: resolved.id, round: roundNumber, metaPath: roundMetaPath, schema_version: 1 };
|
|
34140
|
+
}
|
|
34141
|
+
if (resolved.current_phase !== "synthesis") {
|
|
34142
|
+
throw new StateError(
|
|
34143
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
34144
|
+
`Cannot complete round: workflow is at "${resolved.current_phase}", not "synthesis". Advance through the phases first.`
|
|
34145
|
+
);
|
|
34146
|
+
}
|
|
34147
|
+
if (params.requireFinal) {
|
|
34148
|
+
const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
|
|
34149
|
+
if (!existsSync14(finalPath)) {
|
|
34150
|
+
throw new StateError(
|
|
34151
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
34152
|
+
`Cannot complete round: --require-final set but ${finalPath} is missing.`
|
|
34153
|
+
);
|
|
34154
|
+
}
|
|
34155
|
+
}
|
|
34156
|
+
let metaPath;
|
|
34157
|
+
if (params.source === "stdin") {
|
|
34158
|
+
const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
|
|
34159
|
+
mkdirSync5(roundDir, { recursive: true });
|
|
34160
|
+
metaPath = roundMetaPath;
|
|
34161
|
+
writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
|
|
34162
|
+
}
|
|
34163
|
+
db.transaction(() => {
|
|
34164
|
+
insertEvent(db, {
|
|
34165
|
+
session_id: resolved.id,
|
|
34166
|
+
event_type: "round_completed",
|
|
34167
|
+
phase: "synthesis",
|
|
34168
|
+
phase_number: 7,
|
|
34169
|
+
round: roundNumber,
|
|
34170
|
+
metadata: JSON.stringify({
|
|
34171
|
+
verdict: meta.verdict,
|
|
34172
|
+
blocker_count: counts.blockerCount,
|
|
34173
|
+
should_fix_count: counts.shouldFixCount,
|
|
34174
|
+
suggestion_count: counts.suggestionCount,
|
|
34175
|
+
reviewer_count: counts.reviewerCount,
|
|
34176
|
+
total_finding_count: counts.totalFindingCount,
|
|
34177
|
+
source: "orchestrator"
|
|
34178
|
+
})
|
|
34179
|
+
});
|
|
34180
|
+
if (roundNumber >= resolved.current_round) {
|
|
34181
|
+
updateSession(db, resolved.id, { current_round: roundNumber });
|
|
34182
|
+
}
|
|
34183
|
+
validatePhaseTransition("review", resolved.current_phase, "complete", false);
|
|
34184
|
+
updateSession(db, resolved.id, { current_phase: "complete", phase_number: 8 });
|
|
34185
|
+
insertEvent(db, {
|
|
34186
|
+
session_id: resolved.id,
|
|
34187
|
+
event_type: "phase_transition",
|
|
34188
|
+
phase: "complete",
|
|
34189
|
+
phase_number: 8,
|
|
34190
|
+
round: roundNumber
|
|
34191
|
+
});
|
|
34192
|
+
});
|
|
34193
|
+
return { sessionId: resolved.id, round: roundNumber, metaPath, schema_version: 1 };
|
|
34194
|
+
}
|
|
34195
|
+
async function stateCompleteMap(params) {
|
|
34196
|
+
const { ocrDir } = params;
|
|
34197
|
+
const db = await ensureDatabase(ocrDir);
|
|
34198
|
+
let meta;
|
|
34199
|
+
let counts;
|
|
34200
|
+
try {
|
|
34201
|
+
const rawJsonString = readJsonFromSource(params);
|
|
34202
|
+
const label = params.source === "file" ? params.filePath : "stdin";
|
|
34203
|
+
meta = validateMapMeta(parseRawJson(rawJsonString, label));
|
|
34204
|
+
counts = computeMapCounts(meta);
|
|
34205
|
+
} catch (e) {
|
|
34206
|
+
throw new StateError(
|
|
34207
|
+
STATE_EXIT.SCHEMA_INVALID,
|
|
34208
|
+
e instanceof Error ? e.message : "invalid map metadata"
|
|
34209
|
+
);
|
|
34210
|
+
}
|
|
34211
|
+
const resolved = resolveSession(db, params.sessionId);
|
|
34212
|
+
const mapRunNumber = params.mapRun ?? resolved.current_map_run;
|
|
34213
|
+
const mapMetaPath = join16(
|
|
34214
|
+
resolved.session_dir,
|
|
34215
|
+
"map",
|
|
34216
|
+
"runs",
|
|
34217
|
+
`run-${mapRunNumber}`,
|
|
34218
|
+
"map-meta.json"
|
|
34219
|
+
);
|
|
34220
|
+
const already = db.exec(
|
|
34221
|
+
`SELECT 1 FROM orchestration_events
|
|
34222
|
+
WHERE session_id = ? AND event_type = 'map_completed' AND round = ? LIMIT 1`,
|
|
34223
|
+
[resolved.id, mapRunNumber]
|
|
34224
|
+
);
|
|
34225
|
+
if ((already[0]?.values.length ?? 0) > 0) {
|
|
34226
|
+
return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath: mapMetaPath, schema_version: 1 };
|
|
34227
|
+
}
|
|
34228
|
+
if (resolved.current_phase !== "synthesis") {
|
|
34229
|
+
throw new StateError(
|
|
34230
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
34231
|
+
`Cannot complete map: workflow is at "${resolved.current_phase}", not "synthesis". Advance first.`
|
|
34232
|
+
);
|
|
34233
|
+
}
|
|
33150
34234
|
let metaPath;
|
|
33151
34235
|
if (params.source === "stdin") {
|
|
33152
|
-
const runDir =
|
|
34236
|
+
const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
|
|
33153
34237
|
mkdirSync5(runDir, { recursive: true });
|
|
33154
|
-
metaPath =
|
|
33155
|
-
|
|
33156
|
-
}
|
|
33157
|
-
|
|
33158
|
-
|
|
33159
|
-
|
|
33160
|
-
|
|
33161
|
-
|
|
33162
|
-
|
|
33163
|
-
|
|
33164
|
-
|
|
33165
|
-
|
|
33166
|
-
|
|
33167
|
-
|
|
34238
|
+
metaPath = mapMetaPath;
|
|
34239
|
+
writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
|
|
34240
|
+
}
|
|
34241
|
+
db.transaction(() => {
|
|
34242
|
+
insertEvent(db, {
|
|
34243
|
+
session_id: resolved.id,
|
|
34244
|
+
event_type: "map_completed",
|
|
34245
|
+
phase: "synthesis",
|
|
34246
|
+
phase_number: 5,
|
|
34247
|
+
round: mapRunNumber,
|
|
34248
|
+
metadata: JSON.stringify({
|
|
34249
|
+
section_count: counts.sectionCount,
|
|
34250
|
+
file_count: counts.fileCount,
|
|
34251
|
+
source: "orchestrator"
|
|
34252
|
+
})
|
|
34253
|
+
});
|
|
34254
|
+
validatePhaseTransition("map", resolved.current_phase, "complete", false);
|
|
34255
|
+
updateSession(db, resolved.id, { current_phase: "complete", phase_number: 6 });
|
|
34256
|
+
insertEvent(db, {
|
|
34257
|
+
session_id: resolved.id,
|
|
34258
|
+
event_type: "phase_transition",
|
|
34259
|
+
phase: "complete",
|
|
34260
|
+
phase_number: 6,
|
|
34261
|
+
round: mapRunNumber
|
|
34262
|
+
});
|
|
33168
34263
|
});
|
|
33169
|
-
|
|
33170
|
-
|
|
34264
|
+
return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath, schema_version: 1 };
|
|
34265
|
+
}
|
|
34266
|
+
async function stateStatus(ocrDir, sessionId) {
|
|
34267
|
+
const db = await ensureDatabase(ocrDir);
|
|
34268
|
+
const resolved = resolveSession(db, sessionId);
|
|
34269
|
+
const view = db.exec(
|
|
34270
|
+
`SELECT completeness_state, has_terminal_artifact, marked_closed, dependents_settled
|
|
34271
|
+
FROM session_completeness WHERE session_id = ?`,
|
|
34272
|
+
[resolved.id]
|
|
34273
|
+
);
|
|
34274
|
+
const row = view[0]?.values[0];
|
|
34275
|
+
const completenessState = row?.[0] ?? null;
|
|
34276
|
+
const hasTerminalArtifact = row?.[1] === 1;
|
|
34277
|
+
let nextAction;
|
|
34278
|
+
let nextActionKind;
|
|
34279
|
+
switch (completenessState) {
|
|
34280
|
+
case "complete":
|
|
34281
|
+
nextAction = "none \u2014 session is complete";
|
|
34282
|
+
nextActionKind = "none";
|
|
34283
|
+
break;
|
|
34284
|
+
case "closed_without_artifact":
|
|
34285
|
+
nextAction = "re-open and finalize: this session was closed without a completed round/run";
|
|
34286
|
+
nextActionKind = "reopen";
|
|
34287
|
+
break;
|
|
34288
|
+
case "in_flight":
|
|
34289
|
+
nextAction = "wait for in-flight agent processes to finish";
|
|
34290
|
+
nextActionKind = "wait";
|
|
34291
|
+
break;
|
|
34292
|
+
default:
|
|
34293
|
+
if (hasTerminalArtifact) {
|
|
34294
|
+
nextAction = "run 'ocr state finish' to close the workflow";
|
|
34295
|
+
nextActionKind = "finish";
|
|
34296
|
+
} else if (resolved.current_phase === "synthesis") {
|
|
34297
|
+
nextAction = "pipe round metadata to 'ocr state complete-round --stdin'";
|
|
34298
|
+
nextActionKind = "complete_round";
|
|
34299
|
+
} else {
|
|
34300
|
+
nextAction = "advance through the phases, then 'ocr state complete-round'";
|
|
34301
|
+
nextActionKind = "advance";
|
|
34302
|
+
}
|
|
34303
|
+
}
|
|
34304
|
+
return {
|
|
34305
|
+
schema_version: 1,
|
|
34306
|
+
session_id: resolved.id,
|
|
34307
|
+
workflow_type: resolved.workflow_type,
|
|
34308
|
+
status: resolved.status,
|
|
34309
|
+
current_phase: resolved.current_phase,
|
|
34310
|
+
current_round: resolved.current_round,
|
|
34311
|
+
current_map_run: resolved.current_map_run,
|
|
34312
|
+
completeness_state: completenessState,
|
|
34313
|
+
has_terminal_artifact: hasTerminalArtifact,
|
|
34314
|
+
marked_closed: row?.[2] === 1,
|
|
34315
|
+
dependents_settled: row?.[3] === 1,
|
|
34316
|
+
next_action: nextAction,
|
|
34317
|
+
next_action_kind: nextActionKind
|
|
34318
|
+
};
|
|
33171
34319
|
}
|
|
33172
34320
|
async function stateSync(ocrDir) {
|
|
33173
34321
|
const db = await ensureDatabase(ocrDir);
|
|
33174
|
-
const
|
|
33175
|
-
|
|
33176
|
-
if (!existsSync13(sessionsRoot)) {
|
|
34322
|
+
const sessionsRoot = join16(ocrDir, "sessions");
|
|
34323
|
+
if (!existsSync14(sessionsRoot)) {
|
|
33177
34324
|
return 0;
|
|
33178
34325
|
}
|
|
33179
34326
|
const entries = readdirSync6(sessionsRoot).filter((name) => {
|
|
33180
|
-
const fullPath =
|
|
33181
|
-
return
|
|
34327
|
+
const fullPath = join16(sessionsRoot, name);
|
|
34328
|
+
return statSync3(fullPath).isDirectory();
|
|
33182
34329
|
});
|
|
33183
34330
|
let synced = 0;
|
|
33184
34331
|
for (const dirName of entries) {
|
|
33185
|
-
const dirPath =
|
|
34332
|
+
const dirPath = join16(sessionsRoot, dirName);
|
|
33186
34333
|
const existing = getSession(db, dirName);
|
|
33187
34334
|
if (existing) {
|
|
33188
34335
|
continue;
|
|
@@ -33190,28 +34337,41 @@ async function stateSync(ocrDir) {
|
|
|
33190
34337
|
if (!hasArtifacts(dirPath)) {
|
|
33191
34338
|
continue;
|
|
33192
34339
|
}
|
|
33193
|
-
const hasRoundsDir =
|
|
33194
|
-
const hasMapDir =
|
|
34340
|
+
const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
|
|
34341
|
+
const hasMapDir = existsSync14(join16(dirPath, "map"));
|
|
33195
34342
|
const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
|
|
33196
34343
|
const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
33197
34344
|
const branch = branchMatch?.[1] ?? dirName;
|
|
33198
34345
|
let inferredPhase = "context";
|
|
34346
|
+
let inferredPhaseNumber = 1;
|
|
34347
|
+
let inferredRound = 1;
|
|
34348
|
+
let inferredMapRun = 1;
|
|
33199
34349
|
if (workflowType === "review") {
|
|
33200
|
-
const roundsDir =
|
|
33201
|
-
if (
|
|
33202
|
-
const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).
|
|
33203
|
-
const
|
|
33204
|
-
if (
|
|
33205
|
-
|
|
34350
|
+
const roundsDir = join16(dirPath, "rounds");
|
|
34351
|
+
if (existsSync14(roundsDir)) {
|
|
34352
|
+
const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).map((d) => parseInt(d.replace("round-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
34353
|
+
const latestRoundNum = roundDirs[roundDirs.length - 1];
|
|
34354
|
+
if (latestRoundNum !== void 0) {
|
|
34355
|
+
inferredRound = latestRoundNum;
|
|
34356
|
+
if (existsSync14(
|
|
34357
|
+
join16(roundsDir, `round-${latestRoundNum}`, "final.md")
|
|
34358
|
+
)) {
|
|
34359
|
+
inferredPhase = "complete";
|
|
34360
|
+
inferredPhaseNumber = 8;
|
|
34361
|
+
}
|
|
33206
34362
|
}
|
|
33207
34363
|
}
|
|
33208
34364
|
} else if (workflowType === "map") {
|
|
33209
|
-
const runsDir =
|
|
33210
|
-
if (
|
|
33211
|
-
const runDirs = readdirSync6(runsDir).filter((d) => /^run-\d+$/.test(d)).
|
|
33212
|
-
const
|
|
33213
|
-
if (
|
|
33214
|
-
|
|
34365
|
+
const runsDir = join16(dirPath, "map", "runs");
|
|
34366
|
+
if (existsSync14(runsDir)) {
|
|
34367
|
+
const runDirs = readdirSync6(runsDir).filter((d) => /^run-\d+$/.test(d)).map((d) => parseInt(d.replace("run-", ""), 10)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
34368
|
+
const latestRunNum = runDirs[runDirs.length - 1];
|
|
34369
|
+
if (latestRunNum !== void 0) {
|
|
34370
|
+
inferredMapRun = latestRunNum;
|
|
34371
|
+
if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
|
|
34372
|
+
inferredPhase = "complete";
|
|
34373
|
+
inferredPhaseNumber = 6;
|
|
34374
|
+
}
|
|
33215
34375
|
}
|
|
33216
34376
|
}
|
|
33217
34377
|
}
|
|
@@ -33220,35 +34380,36 @@ async function stateSync(ocrDir) {
|
|
|
33220
34380
|
branch,
|
|
33221
34381
|
workflow_type: workflowType,
|
|
33222
34382
|
current_phase: inferredPhase,
|
|
33223
|
-
phase_number:
|
|
33224
|
-
current_round:
|
|
33225
|
-
current_map_run:
|
|
34383
|
+
phase_number: inferredPhaseNumber,
|
|
34384
|
+
current_round: inferredRound,
|
|
34385
|
+
current_map_run: inferredMapRun,
|
|
33226
34386
|
session_dir: dirPath
|
|
33227
34387
|
});
|
|
33228
|
-
|
|
33229
|
-
|
|
33230
|
-
|
|
33231
|
-
|
|
33232
|
-
|
|
33233
|
-
|
|
33234
|
-
|
|
33235
|
-
|
|
34388
|
+
commitReasonClose(
|
|
34389
|
+
db,
|
|
34390
|
+
dirName,
|
|
34391
|
+
{
|
|
34392
|
+
event_type: "session_synced",
|
|
34393
|
+
phase: inferredPhase,
|
|
34394
|
+
phase_number: 1,
|
|
34395
|
+
metadata: JSON.stringify({ source: "filesystem_backfill" })
|
|
34396
|
+
},
|
|
34397
|
+
{ status: "closed" }
|
|
34398
|
+
);
|
|
33236
34399
|
synced++;
|
|
33237
34400
|
}
|
|
33238
|
-
if (synced > 0) {
|
|
33239
|
-
saveDatabase(db, dbPath);
|
|
33240
|
-
}
|
|
33241
34401
|
return synced;
|
|
33242
34402
|
}
|
|
33243
34403
|
|
|
33244
34404
|
// src/commands/state.ts
|
|
33245
34405
|
init_command_log();
|
|
33246
34406
|
init_db();
|
|
34407
|
+
init_db();
|
|
33247
34408
|
function readDashboardSpawnMarker(ocrDir) {
|
|
33248
|
-
const path2 =
|
|
34409
|
+
const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
|
|
33249
34410
|
let raw;
|
|
33250
34411
|
try {
|
|
33251
|
-
raw =
|
|
34412
|
+
raw = readFileSync11(path2, "utf-8");
|
|
33252
34413
|
} catch {
|
|
33253
34414
|
return null;
|
|
33254
34415
|
}
|
|
@@ -33280,143 +34441,37 @@ async function readStdin() {
|
|
|
33280
34441
|
}
|
|
33281
34442
|
return data;
|
|
33282
34443
|
}
|
|
33283
|
-
|
|
33284
|
-
|
|
33285
|
-
|
|
33286
|
-
(
|
|
33287
|
-
|
|
33288
|
-
|
|
33289
|
-
`
|
|
33290
|
-
)
|
|
33291
|
-
|
|
33292
|
-
return
|
|
33293
|
-
}
|
|
33294
|
-
).option("--session-dir <dir>", "Session directory path (auto-resolved if omitted)").option(
|
|
33295
|
-
"--dashboard-uid <uid>",
|
|
33296
|
-
"Dashboard command_executions uid to link this workflow to. Takes precedence over the OCR_DASHBOARD_EXECUTION_UID env var so AI shells that strip env vars can still wire the linkage."
|
|
33297
|
-
).action(
|
|
33298
|
-
async (options) => {
|
|
33299
|
-
const targetDir = process.cwd();
|
|
33300
|
-
requireOcrSetup(targetDir);
|
|
33301
|
-
const ocrDir = join16(targetDir, ".ocr");
|
|
33302
|
-
const sessionDir = options.sessionDir ?? join16(ocrDir, "sessions", options.sessionId);
|
|
33303
|
-
if (!existsSync14(sessionDir)) {
|
|
33304
|
-
mkdirSync6(sessionDir, { recursive: true });
|
|
33305
|
-
}
|
|
33306
|
-
try {
|
|
33307
|
-
const sessionId = await stateInit({
|
|
33308
|
-
sessionId: options.sessionId,
|
|
33309
|
-
branch: options.branch,
|
|
33310
|
-
workflowType: options.workflowType,
|
|
33311
|
-
sessionDir,
|
|
33312
|
-
ocrDir
|
|
33313
|
-
});
|
|
33314
|
-
const markerUid = readDashboardSpawnMarker(ocrDir)?.execution_uid;
|
|
33315
|
-
const dashboardUid = options.dashboardUid ?? process.env["OCR_DASHBOARD_EXECUTION_UID"] ?? markerUid;
|
|
33316
|
-
if (dashboardUid) {
|
|
33317
|
-
try {
|
|
33318
|
-
const db = await getDb(ocrDir);
|
|
33319
|
-
linkDashboardInvocationToWorkflow(db, dashboardUid, sessionId);
|
|
33320
|
-
saveDatabase(db, join16(ocrDir, "data", "ocr.db"));
|
|
33321
|
-
console.error(
|
|
33322
|
-
source_default.gray(
|
|
33323
|
-
`[state init] linked workflow_id=${sessionId} \u2192 dashboard uid=${dashboardUid}`
|
|
33324
|
-
)
|
|
33325
|
-
);
|
|
33326
|
-
} catch (linkErr) {
|
|
33327
|
-
console.error(
|
|
33328
|
-
source_default.yellow(
|
|
33329
|
-
`Warning: failed to link dashboard command_execution to session: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`
|
|
33330
|
-
)
|
|
33331
|
-
);
|
|
33332
|
-
}
|
|
33333
|
-
} else {
|
|
33334
|
-
console.error(
|
|
33335
|
-
source_default.gray(
|
|
33336
|
-
`[state init] no dashboard linkage available (flag, env var, and marker file all absent \u2014 CLI-only invocation)`
|
|
33337
|
-
)
|
|
33338
|
-
);
|
|
33339
|
-
}
|
|
33340
|
-
console.log(sessionId);
|
|
33341
|
-
} catch (error) {
|
|
33342
|
-
console.error(
|
|
33343
|
-
source_default.red(
|
|
33344
|
-
`Error: ${error instanceof Error ? error.message : "Failed to initialize session"}`
|
|
33345
|
-
)
|
|
33346
|
-
);
|
|
33347
|
-
process.exit(1);
|
|
33348
|
-
}
|
|
33349
|
-
}
|
|
33350
|
-
);
|
|
33351
|
-
var transitionSubcommand = new Command("transition").description("Transition session to a new phase").option("--session-id <id>", "Session ID (auto-detects latest active if omitted)").requiredOption("--phase <phase>", "Target phase name").requiredOption("--phase-number <number>", "Phase number", parseInt).option("--current-round <number>", "Round number", parseInt).option("--current-map-run <number>", "Map run number", parseInt).action(
|
|
33352
|
-
async (options) => {
|
|
33353
|
-
const targetDir = process.cwd();
|
|
33354
|
-
requireOcrSetup(targetDir);
|
|
33355
|
-
const ocrDir = join16(targetDir, ".ocr");
|
|
33356
|
-
const VALID_PHASES = /* @__PURE__ */ new Set([
|
|
33357
|
-
"context",
|
|
33358
|
-
"change-context",
|
|
33359
|
-
"analysis",
|
|
33360
|
-
"reviews",
|
|
33361
|
-
"aggregation",
|
|
33362
|
-
"discourse",
|
|
33363
|
-
"synthesis",
|
|
33364
|
-
"complete",
|
|
33365
|
-
"map-context",
|
|
33366
|
-
"topology",
|
|
33367
|
-
"flow-analysis",
|
|
33368
|
-
"requirements-mapping"
|
|
33369
|
-
]);
|
|
33370
|
-
if (!VALID_PHASES.has(options.phase)) {
|
|
33371
|
-
throw new Error(`Invalid phase: "${options.phase}". Must be one of: ${[...VALID_PHASES].join(", ")}`);
|
|
33372
|
-
}
|
|
33373
|
-
try {
|
|
33374
|
-
const sessionId = options.sessionId ?? (await resolveActiveSession(ocrDir)).id;
|
|
33375
|
-
await stateTransition({
|
|
33376
|
-
sessionId,
|
|
33377
|
-
phase: options.phase,
|
|
33378
|
-
phaseNumber: options.phaseNumber,
|
|
33379
|
-
round: options.currentRound,
|
|
33380
|
-
mapRun: options.currentMapRun,
|
|
33381
|
-
ocrDir
|
|
33382
|
-
});
|
|
33383
|
-
console.log(
|
|
33384
|
-
`${sessionId}: ${options.phase} (phase ${options.phaseNumber})`
|
|
33385
|
-
);
|
|
33386
|
-
} catch (error) {
|
|
33387
|
-
console.error(
|
|
33388
|
-
source_default.red(
|
|
33389
|
-
`Error: ${error instanceof Error ? error.message : "Failed to transition"}`
|
|
33390
|
-
)
|
|
33391
|
-
);
|
|
33392
|
-
process.exit(1);
|
|
33393
|
-
}
|
|
34444
|
+
async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
|
|
34445
|
+
const markerUid = readDashboardSpawnMarker(ocrDir)?.execution_uid;
|
|
34446
|
+
const dashboardUid = explicitUid ?? process.env["OCR_DASHBOARD_EXECUTION_UID"] ?? markerUid;
|
|
34447
|
+
if (!dashboardUid) {
|
|
34448
|
+
console.error(
|
|
34449
|
+
source_default.gray(
|
|
34450
|
+
`[state ${label}] no dashboard linkage available (flag, env var, and marker file all absent \u2014 CLI-only invocation)`
|
|
34451
|
+
)
|
|
34452
|
+
);
|
|
34453
|
+
return;
|
|
33394
34454
|
}
|
|
33395
|
-
);
|
|
33396
|
-
var closeSubcommand = new Command("close").description("Close a session").option("--session-id <id>", "Session ID (auto-detects latest active if omitted)").action(async (options) => {
|
|
33397
|
-
const targetDir = process.cwd();
|
|
33398
|
-
requireOcrSetup(targetDir);
|
|
33399
|
-
const ocrDir = join16(targetDir, ".ocr");
|
|
33400
34455
|
try {
|
|
33401
|
-
const
|
|
33402
|
-
|
|
33403
|
-
sessionId,
|
|
33404
|
-
ocrDir
|
|
33405
|
-
});
|
|
33406
|
-
console.log(`${sessionId}: closed`);
|
|
33407
|
-
} catch (error) {
|
|
34456
|
+
const db = await getDb(ocrDir);
|
|
34457
|
+
linkDashboardInvocationToWorkflow(db, dashboardUid, sessionId);
|
|
33408
34458
|
console.error(
|
|
33409
|
-
source_default.
|
|
33410
|
-
`
|
|
34459
|
+
source_default.gray(
|
|
34460
|
+
`[state ${label}] linked workflow_id=${sessionId} \u2192 dashboard uid=${dashboardUid}`
|
|
34461
|
+
)
|
|
34462
|
+
);
|
|
34463
|
+
} catch (linkErr) {
|
|
34464
|
+
console.error(
|
|
34465
|
+
source_default.yellow(
|
|
34466
|
+
`Warning: failed to link dashboard command_execution to session: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`
|
|
33411
34467
|
)
|
|
33412
34468
|
);
|
|
33413
|
-
process.exit(1);
|
|
33414
34469
|
}
|
|
33415
|
-
}
|
|
34470
|
+
}
|
|
33416
34471
|
var showSubcommand = new Command("show").description("Show current session state").option("--session-id <id>", "Session ID (defaults to latest active)").option("--json", "Output as JSON").action(async (options) => {
|
|
33417
34472
|
const targetDir = process.cwd();
|
|
33418
34473
|
requireOcrSetup(targetDir);
|
|
33419
|
-
const ocrDir =
|
|
34474
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
33420
34475
|
try {
|
|
33421
34476
|
const result = await stateShow(ocrDir, options.sessionId);
|
|
33422
34477
|
if (!result) {
|
|
@@ -33485,7 +34540,7 @@ var showSubcommand = new Command("show").description("Show current session state
|
|
|
33485
34540
|
var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
|
|
33486
34541
|
const targetDir = process.cwd();
|
|
33487
34542
|
requireOcrSetup(targetDir);
|
|
33488
|
-
const ocrDir =
|
|
34543
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
33489
34544
|
try {
|
|
33490
34545
|
const synced = await stateSync(ocrDir);
|
|
33491
34546
|
console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
|
|
@@ -33495,7 +34550,6 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
|
|
|
33495
34550
|
if (totalCmds === 0) {
|
|
33496
34551
|
const recovered = replayCommandLog(db, ocrDir);
|
|
33497
34552
|
if (recovered > 0) {
|
|
33498
|
-
saveDatabase(db, join16(ocrDir, "data", "ocr.db"));
|
|
33499
34553
|
console.log(`Recovered ${recovered} command${recovered !== 1 ? "s" : ""} from backup log.`);
|
|
33500
34554
|
}
|
|
33501
34555
|
}
|
|
@@ -33508,123 +34562,216 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
|
|
|
33508
34562
|
process.exit(1);
|
|
33509
34563
|
}
|
|
33510
34564
|
});
|
|
33511
|
-
var
|
|
34565
|
+
var reconcileSubcommand = new Command("reconcile").description(
|
|
34566
|
+
"Heal legacy/drifted session state by deriving truth from events + artifacts"
|
|
34567
|
+
).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
|
|
34568
|
+
const targetDir = process.cwd();
|
|
34569
|
+
requireOcrSetup(targetDir);
|
|
34570
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34571
|
+
try {
|
|
34572
|
+
const db = await ensureDatabase(ocrDir);
|
|
34573
|
+
const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
|
|
34574
|
+
if (options.json) {
|
|
34575
|
+
console.log(JSON.stringify(result, null, 2));
|
|
34576
|
+
return;
|
|
34577
|
+
}
|
|
34578
|
+
const repairs = result.actions.filter((a) => a.kind !== "ok");
|
|
34579
|
+
if (repairs.length === 0) {
|
|
34580
|
+
console.log(source_default.dim("Nothing to reconcile \u2014 all sessions consistent."));
|
|
34581
|
+
return;
|
|
34582
|
+
}
|
|
34583
|
+
console.log(
|
|
34584
|
+
result.dryRun ? source_default.bold(`Reconciliation plan (${repairs.length} change(s), dry run):`) : source_default.bold(`Reconciled ${repairs.length} session(s):`)
|
|
34585
|
+
);
|
|
34586
|
+
for (const a of repairs) {
|
|
34587
|
+
console.log(` ${source_default.cyan(a.kind)} ${a.sessionId}`);
|
|
34588
|
+
console.log(` ${source_default.dim(a.detail)}`);
|
|
34589
|
+
}
|
|
34590
|
+
} catch (error) {
|
|
34591
|
+
console.error(
|
|
34592
|
+
source_default.red(
|
|
34593
|
+
`Error: ${error instanceof Error ? error.message : "Failed to reconcile"}`
|
|
34594
|
+
)
|
|
34595
|
+
);
|
|
34596
|
+
process.exit(1);
|
|
34597
|
+
}
|
|
34598
|
+
});
|
|
34599
|
+
function exitFromStateError(error, fallback2) {
|
|
34600
|
+
if (error instanceof StateError) {
|
|
34601
|
+
console.error(source_default.red(`Error: ${error.message}`));
|
|
34602
|
+
process.exit(error.code);
|
|
34603
|
+
}
|
|
34604
|
+
if (isBusyError(error)) {
|
|
34605
|
+
console.error(
|
|
34606
|
+
source_default.red(
|
|
34607
|
+
`Error: database is busy (locked past retry budget): ${error instanceof Error ? error.message : String(error)}`
|
|
34608
|
+
)
|
|
34609
|
+
);
|
|
34610
|
+
process.exit(STATE_EXIT.BUSY);
|
|
34611
|
+
}
|
|
34612
|
+
console.error(
|
|
34613
|
+
source_default.red(`Error: ${error instanceof Error ? error.message : fallback2}`)
|
|
34614
|
+
);
|
|
34615
|
+
process.exit(1);
|
|
34616
|
+
}
|
|
34617
|
+
var beginSubcommand = new Command("begin").description("Start or resume a workflow and report where it stands").requiredOption("--session-id <id>", "Session ID").requiredOption("--branch <branch>", "Branch name").requiredOption("--workflow-type <type>", "Workflow type (review or map)", (v) => {
|
|
34618
|
+
if (v !== "review" && v !== "map") {
|
|
34619
|
+
throw new Error(`Invalid workflow type: "${v}". Must be "review" or "map".`);
|
|
34620
|
+
}
|
|
34621
|
+
return v;
|
|
34622
|
+
}).option("--session-dir <dir>", "Session directory path (auto-resolved if omitted)").option(
|
|
34623
|
+
"--dashboard-uid <uid>",
|
|
34624
|
+
"Dashboard command_executions uid to link this workflow to (takes precedence over OCR_DASHBOARD_EXECUTION_UID)"
|
|
34625
|
+
).option("--json", "Output the result as JSON").action(
|
|
33512
34626
|
async (options) => {
|
|
33513
34627
|
const targetDir = process.cwd();
|
|
33514
34628
|
requireOcrSetup(targetDir);
|
|
33515
|
-
const ocrDir =
|
|
33516
|
-
|
|
33517
|
-
|
|
33518
|
-
process.exit(1);
|
|
33519
|
-
}
|
|
33520
|
-
if (options.file && options.stdin) {
|
|
33521
|
-
console.error(source_default.red("Error: --file and --stdin are mutually exclusive"));
|
|
33522
|
-
process.exit(1);
|
|
33523
|
-
}
|
|
34629
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34630
|
+
const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
|
|
34631
|
+
if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
|
|
33524
34632
|
try {
|
|
33525
|
-
|
|
33526
|
-
|
|
33527
|
-
|
|
33528
|
-
|
|
33529
|
-
|
|
33530
|
-
|
|
33531
|
-
|
|
33532
|
-
|
|
33533
|
-
|
|
33534
|
-
})
|
|
33535
|
-
} else if (options.file) {
|
|
33536
|
-
result = await stateRoundComplete({
|
|
33537
|
-
source: "file",
|
|
33538
|
-
ocrDir,
|
|
33539
|
-
filePath: options.file,
|
|
33540
|
-
sessionId: options.sessionId,
|
|
33541
|
-
round: options.round
|
|
33542
|
-
});
|
|
33543
|
-
} else {
|
|
33544
|
-
process.exit(1);
|
|
33545
|
-
}
|
|
33546
|
-
console.log(source_default.green("Round data imported successfully."));
|
|
33547
|
-
if (result.metaPath) {
|
|
33548
|
-
console.log(source_default.dim(`Wrote ${result.metaPath}`));
|
|
33549
|
-
}
|
|
33550
|
-
} catch (error) {
|
|
33551
|
-
console.error(
|
|
33552
|
-
source_default.red(
|
|
33553
|
-
`Error: ${error instanceof Error ? error.message : "Failed to import round data"}`
|
|
33554
|
-
)
|
|
34633
|
+
const result = await stateBegin({
|
|
34634
|
+
sessionId: options.sessionId,
|
|
34635
|
+
branch: options.branch,
|
|
34636
|
+
workflowType: options.workflowType,
|
|
34637
|
+
sessionDir,
|
|
34638
|
+
ocrDir
|
|
34639
|
+
});
|
|
34640
|
+
await linkDashboardInvocation(ocrDir, result.session_id, options.dashboardUid, "begin");
|
|
34641
|
+
console.log(
|
|
34642
|
+
options.json ? JSON.stringify(result, null, 2) : `${result.session_id}: round ${result.round}, phase ${result.phase} (${result.completeness ?? "unknown"})`
|
|
33555
34643
|
);
|
|
33556
|
-
|
|
34644
|
+
} catch (error) {
|
|
34645
|
+
exitFromStateError(error, "Failed to begin session");
|
|
33557
34646
|
}
|
|
33558
34647
|
}
|
|
33559
34648
|
);
|
|
33560
|
-
var
|
|
34649
|
+
var advanceSubcommand = new Command("advance").description("Advance the workflow to a phase (graph-validated; phase number derived)").requiredOption("--phase <phase>", "Target phase name").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--current-round <number>", "Round number", parseInt).option("--current-map-run <number>", "Map run number", parseInt).option("--phase-number <number>", "(ignored \u2014 derived from --phase)", parseInt).action(
|
|
33561
34650
|
async (options) => {
|
|
33562
34651
|
const targetDir = process.cwd();
|
|
33563
34652
|
requireOcrSetup(targetDir);
|
|
33564
|
-
const ocrDir =
|
|
33565
|
-
|
|
33566
|
-
|
|
33567
|
-
|
|
33568
|
-
|
|
33569
|
-
|
|
33570
|
-
|
|
33571
|
-
|
|
34653
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34654
|
+
try {
|
|
34655
|
+
const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
|
|
34656
|
+
await stateAdvance({
|
|
34657
|
+
sessionId,
|
|
34658
|
+
phase: options.phase,
|
|
34659
|
+
round: options.currentRound,
|
|
34660
|
+
mapRun: options.currentMapRun,
|
|
34661
|
+
ocrDir
|
|
34662
|
+
});
|
|
34663
|
+
console.log(`${sessionId}: ${options.phase}`);
|
|
34664
|
+
} catch (error) {
|
|
34665
|
+
exitFromStateError(error, "Failed to advance");
|
|
33572
34666
|
}
|
|
34667
|
+
}
|
|
34668
|
+
);
|
|
34669
|
+
var completeRoundSubcommand = new Command("complete-round").description("Atomically finalize a review round (validate + record + transition)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--round <number>", "Round number (defaults to current)", parseInt).option("--stdin", "Read round metadata JSON from stdin").option("--file <path>", "Read round metadata JSON from a file").option("--require-final", "Require rounds/round-N/final.md to exist").option("--json", "Output the result as JSON").action(
|
|
34670
|
+
async (options) => {
|
|
34671
|
+
const targetDir = process.cwd();
|
|
34672
|
+
requireOcrSetup(targetDir);
|
|
34673
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
33573
34674
|
try {
|
|
33574
|
-
|
|
33575
|
-
|
|
33576
|
-
|
|
33577
|
-
|
|
33578
|
-
|
|
33579
|
-
|
|
33580
|
-
|
|
33581
|
-
|
|
33582
|
-
|
|
33583
|
-
|
|
33584
|
-
|
|
33585
|
-
result
|
|
33586
|
-
|
|
33587
|
-
ocrDir,
|
|
33588
|
-
filePath: options.file,
|
|
33589
|
-
sessionId: options.sessionId,
|
|
33590
|
-
mapRun: options.mapRun
|
|
33591
|
-
});
|
|
33592
|
-
} else {
|
|
33593
|
-
process.exit(1);
|
|
33594
|
-
}
|
|
33595
|
-
console.log(source_default.green("Map data imported successfully."));
|
|
33596
|
-
if (result.metaPath) {
|
|
33597
|
-
console.log(source_default.dim(`Wrote ${result.metaPath}`));
|
|
33598
|
-
}
|
|
34675
|
+
const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
|
|
34676
|
+
throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
|
|
34677
|
+
})();
|
|
34678
|
+
const result = await stateCompleteRound({
|
|
34679
|
+
...base,
|
|
34680
|
+
ocrDir,
|
|
34681
|
+
sessionId: options.sessionId,
|
|
34682
|
+
round: options.round,
|
|
34683
|
+
requireFinal: options.requireFinal
|
|
34684
|
+
});
|
|
34685
|
+
console.log(
|
|
34686
|
+
options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: round ${result.round} complete`
|
|
34687
|
+
);
|
|
33599
34688
|
} catch (error) {
|
|
33600
|
-
|
|
33601
|
-
|
|
33602
|
-
|
|
33603
|
-
|
|
34689
|
+
exitFromStateError(error, "Failed to complete round");
|
|
34690
|
+
}
|
|
34691
|
+
}
|
|
34692
|
+
);
|
|
34693
|
+
var completeMapSubcommand = new Command("complete-map").description("Atomically finalize a map run (validate + record + transition)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--map-run <number>", "Map run number (defaults to current)", parseInt).option("--stdin", "Read map metadata JSON from stdin").option("--file <path>", "Read map metadata JSON from a file").option("--json", "Output the result as JSON").action(
|
|
34694
|
+
async (options) => {
|
|
34695
|
+
const targetDir = process.cwd();
|
|
34696
|
+
requireOcrSetup(targetDir);
|
|
34697
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34698
|
+
try {
|
|
34699
|
+
const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
|
|
34700
|
+
throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
|
|
34701
|
+
})();
|
|
34702
|
+
const result = await stateCompleteMap({
|
|
34703
|
+
...base,
|
|
34704
|
+
ocrDir,
|
|
34705
|
+
sessionId: options.sessionId,
|
|
34706
|
+
mapRun: options.mapRun
|
|
34707
|
+
});
|
|
34708
|
+
console.log(
|
|
34709
|
+
options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: map run ${result.mapRun} complete`
|
|
33604
34710
|
);
|
|
33605
|
-
|
|
34711
|
+
} catch (error) {
|
|
34712
|
+
exitFromStateError(error, "Failed to complete map");
|
|
33606
34713
|
}
|
|
33607
34714
|
}
|
|
33608
34715
|
);
|
|
33609
|
-
var
|
|
34716
|
+
var finishSubcommand = new Command("finish").description("Close a workflow (refuses unless the current round/run is complete)").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--abort", "Abandon the session \u2014 records a distinct, non-success terminal").action(async (options) => {
|
|
34717
|
+
const targetDir = process.cwd();
|
|
34718
|
+
requireOcrSetup(targetDir);
|
|
34719
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34720
|
+
try {
|
|
34721
|
+
const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
|
|
34722
|
+
await stateClose({ sessionId, ocrDir, abort: options.abort });
|
|
34723
|
+
console.log(`${sessionId}: ${options.abort ? "aborted" : "finished"}`);
|
|
34724
|
+
} catch (error) {
|
|
34725
|
+
exitFromStateError(error, "Failed to finish");
|
|
34726
|
+
}
|
|
34727
|
+
});
|
|
34728
|
+
var statusSubcommand = new Command("status").description("Report whether a session is complete and, if not, what's missing").option("--session-id <id>", "Session ID (auto-detects active if omitted)").option("--json", "Output the result as JSON").action(async (options) => {
|
|
34729
|
+
const targetDir = process.cwd();
|
|
34730
|
+
requireOcrSetup(targetDir);
|
|
34731
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34732
|
+
try {
|
|
34733
|
+
const result = await stateStatus(ocrDir, options.sessionId);
|
|
34734
|
+
if (options.json) {
|
|
34735
|
+
console.log(JSON.stringify(result, null, 2));
|
|
34736
|
+
} else {
|
|
34737
|
+
console.log(`${result.session_id}: ${result.completeness_state}`);
|
|
34738
|
+
console.log(source_default.dim(` next: ${result.next_action}`));
|
|
34739
|
+
}
|
|
34740
|
+
} catch (error) {
|
|
34741
|
+
exitFromStateError(error, "Failed to read status");
|
|
34742
|
+
}
|
|
34743
|
+
});
|
|
34744
|
+
var RETIRED_STATE_VERBS = {
|
|
34745
|
+
init: "begin",
|
|
34746
|
+
transition: "advance",
|
|
34747
|
+
"round-complete": "complete-round",
|
|
34748
|
+
"map-complete": "complete-map",
|
|
34749
|
+
close: "finish"
|
|
34750
|
+
};
|
|
34751
|
+
var stateCommand = new Command("state").description("Manage OCR session state").addCommand(beginSubcommand).addCommand(advanceSubcommand).addCommand(completeRoundSubcommand).addCommand(completeMapSubcommand).addCommand(finishSubcommand).addCommand(statusSubcommand).addCommand(showSubcommand).addCommand(syncSubcommand).addCommand(reconcileSubcommand).showSuggestionAfterError(false).on("command:*", (operands) => {
|
|
34752
|
+
const verb = operands[0] ?? "";
|
|
34753
|
+
const replacement = RETIRED_STATE_VERBS[verb];
|
|
34754
|
+
const msg = replacement ? `'ocr state ${verb}' was retired in v2.0 \u2014 use 'ocr state ${replacement}'. See 'ocr state --help'.` : `Unknown 'ocr state' subcommand: '${verb}'. See 'ocr state --help'.`;
|
|
34755
|
+
exitFromStateError(new StateError(STATE_EXIT.USAGE, msg), msg);
|
|
34756
|
+
});
|
|
33610
34757
|
|
|
33611
34758
|
// src/commands/session.ts
|
|
33612
34759
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
33613
|
-
import { join as
|
|
34760
|
+
import { join as join19 } from "node:path";
|
|
33614
34761
|
init_db();
|
|
33615
34762
|
|
|
33616
34763
|
// src/lib/runtime-config.ts
|
|
33617
|
-
import { existsSync as
|
|
33618
|
-
import { join as
|
|
34764
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
34765
|
+
import { join as join18 } from "node:path";
|
|
33619
34766
|
var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
|
|
33620
34767
|
function getAgentHeartbeatSeconds(ocrDir) {
|
|
33621
|
-
const configPath =
|
|
33622
|
-
if (!
|
|
34768
|
+
const configPath = join18(ocrDir, "config.yaml");
|
|
34769
|
+
if (!existsSync16(configPath)) {
|
|
33623
34770
|
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
33624
34771
|
}
|
|
33625
34772
|
let content;
|
|
33626
34773
|
try {
|
|
33627
|
-
content =
|
|
34774
|
+
content = readFileSync12(configPath, "utf-8");
|
|
33628
34775
|
} catch {
|
|
33629
34776
|
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
33630
34777
|
}
|
|
@@ -33663,16 +34810,18 @@ function fail(message) {
|
|
|
33663
34810
|
async function setup() {
|
|
33664
34811
|
const targetDir = process.cwd();
|
|
33665
34812
|
requireOcrSetup(targetDir);
|
|
33666
|
-
const ocrDir =
|
|
33667
|
-
|
|
33668
|
-
return { ocrDir, dbPath };
|
|
34813
|
+
const ocrDir = join19(targetDir, ".ocr");
|
|
34814
|
+
return { ocrDir };
|
|
33669
34815
|
}
|
|
33670
34816
|
var startInstanceSubcommand = new Command("start-instance").description("Journal a new agent-CLI process spawned for the active review").option("--workflow <id>", "Workflow session id (auto-detects active if omitted)").option("--persona <name>", "Reviewer persona, e.g. 'principal'").option("--instance <number>", "Instance index within (workflow, persona)", parseInt).option("--name <name>", "Human-friendly name (default: '{persona}-{instance}')").requiredOption("--vendor <vendor>", "Underlying CLI vendor (e.g. 'claude', 'opencode')").option("--model <id>", "Resolved model id passed to the CLI's --model flag").option("--phase <phase>", "Workflow phase this instance is doing").option("--pid <pid>", "Process id of the spawned process", parseInt).option("--note <text>", "Free-form note to attach").action(
|
|
33671
34817
|
async (options) => {
|
|
33672
|
-
const { ocrDir
|
|
34818
|
+
const { ocrDir } = await setup();
|
|
33673
34819
|
const db = await ensureDatabase(ocrDir);
|
|
33674
34820
|
try {
|
|
33675
|
-
const workflowId =
|
|
34821
|
+
const { id: workflowId } = await resolveActiveSession(
|
|
34822
|
+
ocrDir,
|
|
34823
|
+
options.workflow
|
|
34824
|
+
);
|
|
33676
34825
|
const id = randomUUID3();
|
|
33677
34826
|
const persona = options.persona ?? null;
|
|
33678
34827
|
const instanceIndex = options.instance ?? null;
|
|
@@ -33691,7 +34840,6 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
|
|
|
33691
34840
|
pid: options.pid ?? null,
|
|
33692
34841
|
notes: options.note ?? null
|
|
33693
34842
|
});
|
|
33694
|
-
saveDatabase(db, dbPath);
|
|
33695
34843
|
console.log(id);
|
|
33696
34844
|
} catch (error) {
|
|
33697
34845
|
fail(error instanceof Error ? error.message : "Failed to start agent session");
|
|
@@ -33699,18 +34847,17 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
|
|
|
33699
34847
|
}
|
|
33700
34848
|
);
|
|
33701
34849
|
var bindVendorIdSubcommand = new Command("bind-vendor-id").description("Bind the underlying CLI's session id to an OCR agent session").argument("<agent-session-id>", "OCR agent session id").argument("<vendor-session-id>", "Underlying CLI's session id").action(async (agentId, vendorId) => {
|
|
33702
|
-
const { ocrDir
|
|
34850
|
+
const { ocrDir } = await setup();
|
|
33703
34851
|
const db = await ensureDatabase(ocrDir);
|
|
33704
34852
|
try {
|
|
33705
34853
|
setAgentSessionVendorId(db, agentId, vendorId);
|
|
33706
|
-
saveDatabase(db, dbPath);
|
|
33707
34854
|
console.log(`${agentId}: vendor_session_id=${vendorId}`);
|
|
33708
34855
|
} catch (error) {
|
|
33709
34856
|
fail(error instanceof Error ? error.message : "Failed to bind vendor session id");
|
|
33710
34857
|
}
|
|
33711
34858
|
});
|
|
33712
34859
|
var beatSubcommand = new Command("beat").description("Bump last_heartbeat_at on an agent session").argument("<agent-session-id>", "OCR agent session id").action(async (agentId) => {
|
|
33713
|
-
const { ocrDir
|
|
34860
|
+
const { ocrDir } = await setup();
|
|
33714
34861
|
const db = await ensureDatabase(ocrDir);
|
|
33715
34862
|
try {
|
|
33716
34863
|
const existing = getAgentSession(db, agentId);
|
|
@@ -33718,7 +34865,6 @@ var beatSubcommand = new Command("beat").description("Bump last_heartbeat_at on
|
|
|
33718
34865
|
fail(`Agent session not found: ${agentId}`);
|
|
33719
34866
|
}
|
|
33720
34867
|
bumpAgentSessionHeartbeat(db, agentId);
|
|
33721
|
-
saveDatabase(db, dbPath);
|
|
33722
34868
|
console.log(`${agentId}: heartbeat`);
|
|
33723
34869
|
} catch (error) {
|
|
33724
34870
|
fail(error instanceof Error ? error.message : "Failed to bump heartbeat");
|
|
@@ -33729,7 +34875,7 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
|
|
|
33729
34875
|
"Terminal status (done | crashed | cancelled). Default inferred from --exit-code (0 \u2192 done, non-zero \u2192 crashed)"
|
|
33730
34876
|
).option("--exit-code <code>", "Process exit code", parseInt).option("--note <text>", "Free-form note to append").action(
|
|
33731
34877
|
async (agentId, options) => {
|
|
33732
|
-
const { ocrDir
|
|
34878
|
+
const { ocrDir } = await setup();
|
|
33733
34879
|
const db = await ensureDatabase(ocrDir);
|
|
33734
34880
|
try {
|
|
33735
34881
|
const existing = getAgentSession(db, agentId);
|
|
@@ -33760,7 +34906,6 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
|
|
|
33760
34906
|
exitCode: options.exitCode ?? null,
|
|
33761
34907
|
note: options.note
|
|
33762
34908
|
});
|
|
33763
|
-
saveDatabase(db, dbPath);
|
|
33764
34909
|
console.log(`${agentId}: ${status}`);
|
|
33765
34910
|
} catch (error) {
|
|
33766
34911
|
fail(error instanceof Error ? error.message : "Failed to end agent session");
|
|
@@ -33771,7 +34916,10 @@ var listSubcommand = new Command("list").description("List agent sessions for a
|
|
|
33771
34916
|
const { ocrDir } = await setup();
|
|
33772
34917
|
const db = await ensureDatabase(ocrDir);
|
|
33773
34918
|
try {
|
|
33774
|
-
const workflowId =
|
|
34919
|
+
const { id: workflowId } = await resolveActiveSession(
|
|
34920
|
+
ocrDir,
|
|
34921
|
+
options.workflow
|
|
34922
|
+
);
|
|
33775
34923
|
const rows = listAgentSessionsForWorkflow(db, workflowId);
|
|
33776
34924
|
if (options.json) {
|
|
33777
34925
|
console.log(JSON.stringify(rows, null, 2));
|
|
@@ -33917,8 +35065,8 @@ var modelsCommand = new Command("models").description("Inspect models available
|
|
|
33917
35065
|
|
|
33918
35066
|
// src/commands/team.ts
|
|
33919
35067
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
33920
|
-
import { existsSync as
|
|
33921
|
-
import { join as
|
|
35068
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35069
|
+
import { join as join20 } from "node:path";
|
|
33922
35070
|
async function readStdin2() {
|
|
33923
35071
|
const chunks = [];
|
|
33924
35072
|
for await (const chunk of process.stdin) {
|
|
@@ -33977,7 +35125,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
|
|
|
33977
35125
|
async (options) => {
|
|
33978
35126
|
const targetDir = process.cwd();
|
|
33979
35127
|
requireOcrSetup(targetDir);
|
|
33980
|
-
const ocrDir =
|
|
35128
|
+
const ocrDir = join20(targetDir, ".ocr");
|
|
33981
35129
|
try {
|
|
33982
35130
|
const { team } = loadTeamConfig(ocrDir);
|
|
33983
35131
|
let override;
|
|
@@ -34016,8 +35164,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
|
|
|
34016
35164
|
}
|
|
34017
35165
|
const targetDir = process.cwd();
|
|
34018
35166
|
requireOcrSetup(targetDir);
|
|
34019
|
-
const ocrDir =
|
|
34020
|
-
const configPath =
|
|
35167
|
+
const ocrDir = join20(targetDir, ".ocr");
|
|
35168
|
+
const configPath = join20(ocrDir, "config.yaml");
|
|
34021
35169
|
try {
|
|
34022
35170
|
const raw = await readStdin2();
|
|
34023
35171
|
const team = parseSessionOverride(raw);
|
|
@@ -34027,17 +35175,17 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
|
|
|
34027
35175
|
list.push(inst);
|
|
34028
35176
|
byPersona.set(inst.persona, list);
|
|
34029
35177
|
}
|
|
34030
|
-
const doc =
|
|
35178
|
+
const doc = existsSync17(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
|
|
34031
35179
|
applyDefaultTeamSurgically(doc, byPersona);
|
|
34032
35180
|
const yamlOutput = doc.toString({ lineWidth: 0 });
|
|
34033
|
-
|
|
34034
|
-
const reviewersDir =
|
|
34035
|
-
const metaPath =
|
|
35181
|
+
writeFileSync8(configPath, yamlOutput, "utf-8");
|
|
35182
|
+
const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
|
|
35183
|
+
const metaPath = join20(ocrDir, "reviewers-meta.json");
|
|
34036
35184
|
let metaWritten = false;
|
|
34037
35185
|
try {
|
|
34038
35186
|
const meta = generateReviewersMeta(reviewersDir, configPath);
|
|
34039
35187
|
if (meta) {
|
|
34040
|
-
|
|
35188
|
+
writeFileSync8(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
34041
35189
|
metaWritten = true;
|
|
34042
35190
|
}
|
|
34043
35191
|
} catch (err) {
|
|
@@ -34128,7 +35276,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
|
|
|
34128
35276
|
|
|
34129
35277
|
// src/commands/review.ts
|
|
34130
35278
|
import { spawn as spawn3 } from "node:child_process";
|
|
34131
|
-
import { join as
|
|
35279
|
+
import { join as join21 } from "node:path";
|
|
34132
35280
|
init_db();
|
|
34133
35281
|
|
|
34134
35282
|
// src/lib/vendor-resume.ts
|
|
@@ -34167,7 +35315,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
|
|
|
34167
35315
|
}
|
|
34168
35316
|
const targetDir = process.cwd();
|
|
34169
35317
|
requireOcrSetup(targetDir);
|
|
34170
|
-
const ocrDir =
|
|
35318
|
+
const ocrDir = join21(targetDir, ".ocr");
|
|
34171
35319
|
const db = await ensureDatabase(ocrDir);
|
|
34172
35320
|
const session = getSession(db, options.resume);
|
|
34173
35321
|
if (!session) {
|
|
@@ -34209,16 +35357,16 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
|
|
|
34209
35357
|
});
|
|
34210
35358
|
|
|
34211
35359
|
// src/commands/update.ts
|
|
34212
|
-
import { existsSync as
|
|
34213
|
-
import { join as
|
|
35360
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
35361
|
+
import { join as join22 } from "node:path";
|
|
34214
35362
|
function detectConfiguredTools(targetDir) {
|
|
34215
35363
|
return AI_TOOLS.filter((tool) => {
|
|
34216
35364
|
if (tool.commandStrategy === "subdirectory") {
|
|
34217
|
-
const ocrDir =
|
|
34218
|
-
return
|
|
35365
|
+
const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
|
|
35366
|
+
return existsSync18(ocrDir);
|
|
34219
35367
|
} else {
|
|
34220
|
-
const reviewCmd =
|
|
34221
|
-
return
|
|
35368
|
+
const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
|
|
35369
|
+
return existsSync18(reviewCmd);
|
|
34222
35370
|
}
|
|
34223
35371
|
});
|
|
34224
35372
|
}
|
|
@@ -34292,7 +35440,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
|
|
|
34292
35440
|
const result = installForTool(tool, targetDir);
|
|
34293
35441
|
results.push(result);
|
|
34294
35442
|
}
|
|
34295
|
-
ensureGitignore(
|
|
35443
|
+
ensureGitignore(join22(targetDir, ".ocr"));
|
|
34296
35444
|
spinner.stop();
|
|
34297
35445
|
const successful = results.filter((r) => r.success);
|
|
34298
35446
|
const failed = results.filter((r) => !r.success);
|
|
@@ -34328,10 +35476,10 @@ var updateCommand = new Command("update").description("Update OCR assets after p
|
|
|
34328
35476
|
if (updateInject) {
|
|
34329
35477
|
if (options.dryRun) {
|
|
34330
35478
|
console.log(source_default.dim(" Would update:"));
|
|
34331
|
-
if (
|
|
35479
|
+
if (existsSync18(join22(targetDir, "AGENTS.md"))) {
|
|
34332
35480
|
console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
|
|
34333
35481
|
}
|
|
34334
|
-
if (
|
|
35482
|
+
if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
|
|
34335
35483
|
console.log(source_default.dim(" \u2022 CLAUDE.md (OCR managed block)"));
|
|
34336
35484
|
}
|
|
34337
35485
|
console.log();
|
|
@@ -34363,14 +35511,14 @@ var updateCommand = new Command("update").description("Update OCR assets after p
|
|
|
34363
35511
|
});
|
|
34364
35512
|
|
|
34365
35513
|
// src/commands/dashboard.ts
|
|
34366
|
-
import { existsSync as
|
|
34367
|
-
import { join as
|
|
35514
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
35515
|
+
import { join as join23, dirname as dirname7 } from "node:path";
|
|
34368
35516
|
import { fileURLToPath } from "node:url";
|
|
34369
35517
|
init_db();
|
|
34370
35518
|
var __filename = fileURLToPath(import.meta.url);
|
|
34371
|
-
var __dirname =
|
|
35519
|
+
var __dirname = dirname7(__filename);
|
|
34372
35520
|
function resolveServerPath() {
|
|
34373
|
-
return
|
|
35521
|
+
return join23(__dirname, "dashboard", "server.js");
|
|
34374
35522
|
}
|
|
34375
35523
|
var dashboardCommand = new Command("dashboard").description("Start the OCR dashboard web interface").option("-p, --port <port>", "Port to run the server on", "4173").option("--no-open", "Don't open the browser automatically").action(
|
|
34376
35524
|
async (options) => {
|
|
@@ -34381,7 +35529,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
|
|
|
34381
35529
|
console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
|
|
34382
35530
|
process.exit(1);
|
|
34383
35531
|
}
|
|
34384
|
-
const ocrDir =
|
|
35532
|
+
const ocrDir = join23(targetDir, ".ocr");
|
|
34385
35533
|
try {
|
|
34386
35534
|
await ensureDatabase(ocrDir);
|
|
34387
35535
|
closeAllDatabases();
|
|
@@ -34395,7 +35543,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
|
|
|
34395
35543
|
process.exit(1);
|
|
34396
35544
|
}
|
|
34397
35545
|
const serverPath = resolveServerPath();
|
|
34398
|
-
if (!
|
|
35546
|
+
if (!existsSync19(serverPath)) {
|
|
34399
35547
|
console.error(source_default.red("Error: Dashboard server bundle not found."));
|
|
34400
35548
|
console.error(
|
|
34401
35549
|
source_default.dim(` Expected at: ${serverPath}`)
|
|
@@ -34429,10 +35577,53 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
|
|
|
34429
35577
|
);
|
|
34430
35578
|
|
|
34431
35579
|
// src/commands/doctor.ts
|
|
34432
|
-
import { existsSync as
|
|
34433
|
-
import { join as
|
|
34434
|
-
|
|
35580
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
35581
|
+
import { join as join24 } from "node:path";
|
|
35582
|
+
init_db();
|
|
35583
|
+
function printStorageEngine(probeWriteEnabled) {
|
|
35584
|
+
console.log();
|
|
35585
|
+
console.log(source_default.bold(" Storage Engine"));
|
|
35586
|
+
console.log();
|
|
35587
|
+
const engine = probeEngine();
|
|
35588
|
+
if (!engine.ok) {
|
|
35589
|
+
console.log(` ${source_default.red("\u2717")} node:sqlite unavailable`);
|
|
35590
|
+
console.log(` ${source_default.dim(engine.error)}`);
|
|
35591
|
+
console.log(
|
|
35592
|
+
` ${source_default.dim(
|
|
35593
|
+
"OCR requires Node >= 22.5 (node:sqlite). Upgrade Node, then re-run `ocr doctor`."
|
|
35594
|
+
)}`
|
|
35595
|
+
);
|
|
35596
|
+
return false;
|
|
35597
|
+
}
|
|
35598
|
+
console.log(
|
|
35599
|
+
` ${source_default.green("\u2713")} node:sqlite (SQLite ${engine.version}, WAL)`
|
|
35600
|
+
);
|
|
35601
|
+
if (probeWriteEnabled) {
|
|
35602
|
+
const write = probeWrite();
|
|
35603
|
+
if (!write.ok) {
|
|
35604
|
+
console.log(` ${source_default.red("\u2717")} write probe failed`);
|
|
35605
|
+
console.log(` ${source_default.dim(write.error)}`);
|
|
35606
|
+
return false;
|
|
35607
|
+
}
|
|
35608
|
+
console.log(
|
|
35609
|
+
` ${source_default.green("\u2713")} write probe (on-disk WAL transaction round-trip)`
|
|
35610
|
+
);
|
|
35611
|
+
}
|
|
35612
|
+
return true;
|
|
35613
|
+
}
|
|
35614
|
+
var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").option(
|
|
35615
|
+
"--probe-write",
|
|
35616
|
+
"additionally exercise an on-disk WAL transaction round-trip (used by the release install gate)"
|
|
35617
|
+
).option(
|
|
35618
|
+
"--engine-only",
|
|
35619
|
+
"check ONLY the storage engine and exit on its result \u2014 skips project/tool checks (used by the release install gate, which runs from a non-initialized dir with no AI tools)"
|
|
35620
|
+
).action((options) => {
|
|
34435
35621
|
printHeader();
|
|
35622
|
+
if (options.engineOnly) {
|
|
35623
|
+
const ok = printStorageEngine(options.probeWrite ?? false);
|
|
35624
|
+
console.log();
|
|
35625
|
+
process.exit(ok ? 0 : 1);
|
|
35626
|
+
}
|
|
34436
35627
|
const targetDir = process.cwd();
|
|
34437
35628
|
let hasIssues = false;
|
|
34438
35629
|
const depResult = checkDependencies();
|
|
@@ -34444,10 +35635,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
|
|
|
34444
35635
|
console.log(source_default.bold(" OCR Installation"));
|
|
34445
35636
|
console.log();
|
|
34446
35637
|
const ocrStatus = checkOcrSetup(targetDir);
|
|
34447
|
-
const configPath =
|
|
34448
|
-
const dbPath =
|
|
34449
|
-
const hasConfig =
|
|
34450
|
-
const hasDb =
|
|
35638
|
+
const configPath = join24(targetDir, ".ocr", "config.yaml");
|
|
35639
|
+
const dbPath = join24(targetDir, ".ocr", "data", "ocr.db");
|
|
35640
|
+
const hasConfig = existsSync20(configPath);
|
|
35641
|
+
const hasDb = existsSync20(dbPath);
|
|
34451
35642
|
const ocrChecks = [
|
|
34452
35643
|
{ label: ".ocr/skills/", ok: ocrStatus.hasSkills },
|
|
34453
35644
|
{ label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
|
|
@@ -34469,6 +35660,9 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
|
|
|
34469
35660
|
if (!ocrStatus.valid) {
|
|
34470
35661
|
hasIssues = true;
|
|
34471
35662
|
}
|
|
35663
|
+
if (!printStorageEngine(options.probeWrite ?? false)) {
|
|
35664
|
+
hasIssues = true;
|
|
35665
|
+
}
|
|
34472
35666
|
console.log();
|
|
34473
35667
|
printCapabilities(depResult);
|
|
34474
35668
|
console.log();
|
|
@@ -34521,8 +35715,8 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
|
|
|
34521
35715
|
});
|
|
34522
35716
|
|
|
34523
35717
|
// src/commands/reviewers.ts
|
|
34524
|
-
import { writeFileSync as
|
|
34525
|
-
import { join as
|
|
35718
|
+
import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
|
|
35719
|
+
import { join as join25 } from "node:path";
|
|
34526
35720
|
async function readStdin3() {
|
|
34527
35721
|
const chunks = [];
|
|
34528
35722
|
for await (const chunk of process.stdin) {
|
|
@@ -34585,20 +35779,20 @@ function validateReviewersMeta(data) {
|
|
|
34585
35779
|
var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json from reviewer markdown files or structured JSON").option("--stdin", "Read reviewers JSON from stdin (for AI-invoked sync)").action(async (options) => {
|
|
34586
35780
|
const targetDir = process.cwd();
|
|
34587
35781
|
requireOcrSetup(targetDir);
|
|
34588
|
-
const ocrDir =
|
|
35782
|
+
const ocrDir = join25(targetDir, ".ocr");
|
|
34589
35783
|
if (!options.stdin) {
|
|
34590
35784
|
try {
|
|
34591
|
-
const reviewersDir =
|
|
34592
|
-
const configPath =
|
|
35785
|
+
const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
|
|
35786
|
+
const configPath = join25(ocrDir, "config.yaml");
|
|
34593
35787
|
const meta = generateReviewersMeta(reviewersDir, configPath);
|
|
34594
35788
|
if (!meta || meta.reviewers.length === 0) {
|
|
34595
35789
|
console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
|
|
34596
35790
|
process.exit(1);
|
|
34597
35791
|
}
|
|
34598
|
-
const metaPath =
|
|
35792
|
+
const metaPath = join25(ocrDir, "reviewers-meta.json");
|
|
34599
35793
|
const tmpPath = metaPath + ".tmp";
|
|
34600
|
-
|
|
34601
|
-
|
|
35794
|
+
writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
|
|
35795
|
+
renameSync2(tmpPath, metaPath);
|
|
34602
35796
|
const tierCounts = meta.reviewers.reduce(
|
|
34603
35797
|
(acc, r) => {
|
|
34604
35798
|
acc[r.tier] = (acc[r.tier] ?? 0) + 1;
|
|
@@ -34625,10 +35819,10 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
|
|
|
34625
35819
|
throw new Error("Invalid JSON on stdin");
|
|
34626
35820
|
}
|
|
34627
35821
|
const meta = validateReviewersMeta(parsed);
|
|
34628
|
-
const metaPath =
|
|
35822
|
+
const metaPath = join25(ocrDir, "reviewers-meta.json");
|
|
34629
35823
|
const tmpPath = metaPath + ".tmp";
|
|
34630
|
-
|
|
34631
|
-
|
|
35824
|
+
writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
|
|
35825
|
+
renameSync2(tmpPath, metaPath);
|
|
34632
35826
|
const tierCounts = meta.reviewers.reduce(
|
|
34633
35827
|
(acc, r) => {
|
|
34634
35828
|
acc[r.tier] = (acc[r.tier] ?? 0) + 1;
|
|
@@ -34653,25 +35847,25 @@ var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer
|
|
|
34653
35847
|
|
|
34654
35848
|
// src/lib/update-check.ts
|
|
34655
35849
|
import { homedir } from "node:os";
|
|
34656
|
-
import { join as
|
|
34657
|
-
import { readFileSync as
|
|
35850
|
+
import { join as join26 } from "node:path";
|
|
35851
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
|
|
34658
35852
|
var PACKAGE_NAME = "@open-code-review/cli";
|
|
34659
35853
|
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
34660
|
-
var CACHE_DIR2 =
|
|
34661
|
-
var CACHE_FILE =
|
|
35854
|
+
var CACHE_DIR2 = join26(homedir(), ".ocr");
|
|
35855
|
+
var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
|
|
34662
35856
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
34663
35857
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
34664
35858
|
function readCache(cacheFile) {
|
|
34665
35859
|
try {
|
|
34666
|
-
return JSON.parse(
|
|
35860
|
+
return JSON.parse(readFileSync14(cacheFile, "utf-8"));
|
|
34667
35861
|
} catch {
|
|
34668
35862
|
return null;
|
|
34669
35863
|
}
|
|
34670
35864
|
}
|
|
34671
35865
|
function writeCache(cacheFile, cache) {
|
|
34672
35866
|
try {
|
|
34673
|
-
mkdirSync7(
|
|
34674
|
-
|
|
35867
|
+
mkdirSync7(join26(cacheFile, ".."), { recursive: true });
|
|
35868
|
+
writeFileSync10(cacheFile, JSON.stringify(cache));
|
|
34675
35869
|
} catch {
|
|
34676
35870
|
}
|
|
34677
35871
|
}
|
|
@@ -34691,7 +35885,7 @@ async function checkForUpdate(currentVersion, options) {
|
|
|
34691
35885
|
if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
|
|
34692
35886
|
return null;
|
|
34693
35887
|
}
|
|
34694
|
-
const cacheFile =
|
|
35888
|
+
const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
|
|
34695
35889
|
try {
|
|
34696
35890
|
const cache = readCache(cacheFile);
|
|
34697
35891
|
if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
|