@open-code-review/cli 1.11.0 → 2.0.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 +8 -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 +1113 -657
- package/dist/index.js +1719 -718
- package/dist/lib/db/index.js +638 -101
- package/package.json +4 -4
- 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
|
@@ -23319,7 +23319,102 @@ var init_result_mapper = __esm({
|
|
|
23319
23319
|
}
|
|
23320
23320
|
});
|
|
23321
23321
|
|
|
23322
|
+
// src/lib/db/engine.ts
|
|
23323
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
23324
|
+
function isBusyError(e) {
|
|
23325
|
+
if (e instanceof BetterSqlite3.SqliteError) {
|
|
23326
|
+
return e.code === "SQLITE_BUSY" || e.code === "SQLITE_BUSY_SNAPSHOT";
|
|
23327
|
+
}
|
|
23328
|
+
const code = e?.code;
|
|
23329
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT";
|
|
23330
|
+
}
|
|
23331
|
+
function sleepSync(ms) {
|
|
23332
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
23333
|
+
}
|
|
23334
|
+
function probeEngine() {
|
|
23335
|
+
try {
|
|
23336
|
+
const db = new BetterSqlite3(":memory:");
|
|
23337
|
+
db.pragma("journal_mode = WAL");
|
|
23338
|
+
db.exec("CREATE TABLE _probe(x); INSERT INTO _probe VALUES (1);");
|
|
23339
|
+
const row = db.prepare("SELECT sqlite_version() AS v").get();
|
|
23340
|
+
db.close();
|
|
23341
|
+
return { ok: true, version: row.v };
|
|
23342
|
+
} catch (e) {
|
|
23343
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
23344
|
+
}
|
|
23345
|
+
}
|
|
23346
|
+
function openEngine(dbPath) {
|
|
23347
|
+
const native = new BetterSqlite3(dbPath);
|
|
23348
|
+
native.pragma("journal_mode = WAL");
|
|
23349
|
+
native.pragma("foreign_keys = ON");
|
|
23350
|
+
native.pragma("busy_timeout = 5000");
|
|
23351
|
+
native.pragma("synchronous = NORMAL");
|
|
23352
|
+
return new BetterSqliteAdapter(native);
|
|
23353
|
+
}
|
|
23354
|
+
var BUSY_RETRY_ATTEMPTS, BUSY_RETRY_BACKOFF_MS, BetterSqliteAdapter;
|
|
23355
|
+
var init_engine = __esm({
|
|
23356
|
+
"src/lib/db/engine.ts"() {
|
|
23357
|
+
"use strict";
|
|
23358
|
+
BUSY_RETRY_ATTEMPTS = 5;
|
|
23359
|
+
BUSY_RETRY_BACKOFF_MS = 50;
|
|
23360
|
+
BetterSqliteAdapter = class {
|
|
23361
|
+
raw;
|
|
23362
|
+
constructor(db) {
|
|
23363
|
+
this.raw = db;
|
|
23364
|
+
}
|
|
23365
|
+
exec(sql, params) {
|
|
23366
|
+
const stmt = this.raw.prepare(sql);
|
|
23367
|
+
if (!stmt.reader) {
|
|
23368
|
+
stmt.run(...params ?? []);
|
|
23369
|
+
return [];
|
|
23370
|
+
}
|
|
23371
|
+
const columns = stmt.columns().map((c) => c.name);
|
|
23372
|
+
const values = stmt.raw().all(...params ?? []);
|
|
23373
|
+
return values.length > 0 ? [{ columns, values }] : [];
|
|
23374
|
+
}
|
|
23375
|
+
run(sql, params) {
|
|
23376
|
+
if (params !== void 0) {
|
|
23377
|
+
this.raw.prepare(sql).run(...params);
|
|
23378
|
+
return;
|
|
23379
|
+
}
|
|
23380
|
+
this.raw.exec(sql);
|
|
23381
|
+
}
|
|
23382
|
+
prepare(sql) {
|
|
23383
|
+
return this.raw.prepare(sql);
|
|
23384
|
+
}
|
|
23385
|
+
transaction(fn) {
|
|
23386
|
+
const tx = this.raw.transaction(fn);
|
|
23387
|
+
for (let attempt = 0; ; attempt++) {
|
|
23388
|
+
try {
|
|
23389
|
+
return tx.immediate();
|
|
23390
|
+
} catch (e) {
|
|
23391
|
+
if (!isBusyError(e) || attempt >= BUSY_RETRY_ATTEMPTS - 1) throw e;
|
|
23392
|
+
sleepSync(BUSY_RETRY_BACKOFF_MS);
|
|
23393
|
+
}
|
|
23394
|
+
}
|
|
23395
|
+
}
|
|
23396
|
+
pragma(source) {
|
|
23397
|
+
return this.raw.pragma(source);
|
|
23398
|
+
}
|
|
23399
|
+
close() {
|
|
23400
|
+
try {
|
|
23401
|
+
this.raw.pragma("wal_checkpoint(TRUNCATE)");
|
|
23402
|
+
} catch {
|
|
23403
|
+
}
|
|
23404
|
+
this.raw.close();
|
|
23405
|
+
}
|
|
23406
|
+
};
|
|
23407
|
+
}
|
|
23408
|
+
});
|
|
23409
|
+
|
|
23322
23410
|
// src/lib/db/migrations.ts
|
|
23411
|
+
function columnExists(db, table, column) {
|
|
23412
|
+
const result = db.exec(`PRAGMA table_info(${table})`);
|
|
23413
|
+
const first = result[0];
|
|
23414
|
+
if (!first) return false;
|
|
23415
|
+
const nameIdx = first.columns.indexOf("name");
|
|
23416
|
+
return first.values.some((row) => row[nameIdx] === column);
|
|
23417
|
+
}
|
|
23323
23418
|
function ensureSchemaVersionTable(db) {
|
|
23324
23419
|
db.run(`
|
|
23325
23420
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
@@ -23329,6 +23424,10 @@ function ensureSchemaVersionTable(db) {
|
|
|
23329
23424
|
);
|
|
23330
23425
|
`);
|
|
23331
23426
|
}
|
|
23427
|
+
function getSchemaVersion(db) {
|
|
23428
|
+
ensureSchemaVersionTable(db);
|
|
23429
|
+
return getCurrentVersion(db);
|
|
23430
|
+
}
|
|
23332
23431
|
function getCurrentVersion(db) {
|
|
23333
23432
|
const result = db.exec(
|
|
23334
23433
|
"SELECT MAX(version) as v FROM schema_version"
|
|
@@ -23346,9 +23445,10 @@ function runMigrations(db) {
|
|
|
23346
23445
|
if (migration.version <= currentVersion) {
|
|
23347
23446
|
continue;
|
|
23348
23447
|
}
|
|
23349
|
-
db.run("BEGIN
|
|
23448
|
+
db.run("BEGIN IMMEDIATE;");
|
|
23350
23449
|
try {
|
|
23351
|
-
db.run(migration.sql);
|
|
23450
|
+
if (migration.sql) db.run(migration.sql);
|
|
23451
|
+
migration.run?.(db);
|
|
23352
23452
|
db.run(
|
|
23353
23453
|
"INSERT INTO schema_version (version, description) VALUES (?, ?);",
|
|
23354
23454
|
[migration.version, migration.description]
|
|
@@ -23685,6 +23785,148 @@ var init_migrations = __esm({
|
|
|
23685
23785
|
DROP INDEX IF EXISTS idx_agent_sessions_status_heartbeat;
|
|
23686
23786
|
DROP TABLE IF EXISTS agent_sessions;
|
|
23687
23787
|
`
|
|
23788
|
+
},
|
|
23789
|
+
{
|
|
23790
|
+
version: 12,
|
|
23791
|
+
description: "Event-sourced lifecycle hardening: event_type taxonomy guard, sweep indexes, session_completeness view",
|
|
23792
|
+
sql: `
|
|
23793
|
+
-- \u2500\u2500 Indexes for the now-periodic stale-session sweep + round derivation \u2500\u2500
|
|
23794
|
+
-- The sweep filters sessions by status and rolls up MAX(created_at) per
|
|
23795
|
+
-- session over the event log; deriveNextRound does MAX(round). Index both.
|
|
23796
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
23797
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_created
|
|
23798
|
+
ON orchestration_events(session_id, created_at);
|
|
23799
|
+
|
|
23800
|
+
-- \u2500\u2500 Event-type taxonomy guard \u2500\u2500
|
|
23801
|
+
-- orchestration_events.event_type is the spine of all lifecycle
|
|
23802
|
+
-- derivation. A typo (e.g. 'round_complete' vs 'round_completed') would
|
|
23803
|
+
-- silently break deriveNextRound and the completeness view. SQLite cannot
|
|
23804
|
+
-- add a CHECK to an existing column without a table rebuild, so enforce
|
|
23805
|
+
-- the closed vocabulary with a BEFORE INSERT trigger instead.
|
|
23806
|
+
CREATE TRIGGER IF NOT EXISTS trg_events_known_type
|
|
23807
|
+
BEFORE INSERT ON orchestration_events
|
|
23808
|
+
WHEN NEW.event_type NOT IN (
|
|
23809
|
+
'session_created', 'session_resumed', 'round_started', 'phase_transition',
|
|
23810
|
+
'round_completed', 'map_completed', 'session_closed', 'session_aborted',
|
|
23811
|
+
'session_auto_closed_stale', 'session_synced', 'session_legacy_import'
|
|
23812
|
+
)
|
|
23813
|
+
BEGIN
|
|
23814
|
+
SELECT RAISE(ABORT, 'unknown orchestration_events.event_type');
|
|
23815
|
+
END;
|
|
23816
|
+
|
|
23817
|
+
-- \u2500\u2500 Close-guard (DB backstop for the completion invariant) \u2500\u2500
|
|
23818
|
+
-- A session cannot transition active \u2192 closed unless its current
|
|
23819
|
+
-- round/run has a terminal artifact event, OR an explicit reason event
|
|
23820
|
+
-- (abort / auto-close-stale / sync / legacy-import) is present. Only a
|
|
23821
|
+
-- *silent* premature close is banned \u2014 every legitimate non-artifact
|
|
23822
|
+
-- close carries a reason event and passes. App-level guards in
|
|
23823
|
+
-- stateClose/finish are the primary check; this makes the illegal state
|
|
23824
|
+
-- unrepresentable even via raw SQL.
|
|
23825
|
+
--
|
|
23826
|
+
-- DEFENCE-IN-DEPTH NOTE (intentional, documented gap): the reason-event
|
|
23827
|
+
-- branch below (event_type IN (...)) is NOT round-scoped \u2014 a reason event
|
|
23828
|
+
-- recorded for an earlier round would also satisfy a later close. The
|
|
23829
|
+
-- app-level guards ARE round-scoped (hasCompletionInvariant checks the
|
|
23830
|
+
-- current round/run), so the precise check lives in the application; this
|
|
23831
|
+
-- trigger is a coarse backstop against a *silent* premature close via raw
|
|
23832
|
+
-- SQL. Tightening it to be round-scoped would require a new migration
|
|
23833
|
+
-- (this v12 trigger is append-only and already shipped); the residual
|
|
23834
|
+
-- risk is a non-artifact close carrying a stale reason event, which is
|
|
23835
|
+
-- still an explicit, audited terminal \u2014 not the failure mode this guards.
|
|
23836
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_close_guard
|
|
23837
|
+
BEFORE UPDATE OF status ON sessions
|
|
23838
|
+
WHEN NEW.status = 'closed' AND OLD.status <> 'closed'
|
|
23839
|
+
AND NOT EXISTS (
|
|
23840
|
+
SELECT 1 FROM orchestration_events e
|
|
23841
|
+
WHERE e.session_id = NEW.id
|
|
23842
|
+
AND (
|
|
23843
|
+
(NEW.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = NEW.current_round)
|
|
23844
|
+
OR (NEW.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = NEW.current_map_run)
|
|
23845
|
+
OR e.event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
23846
|
+
)
|
|
23847
|
+
)
|
|
23848
|
+
BEGIN
|
|
23849
|
+
SELECT RAISE(ABORT, 'cannot close session without a completed round/run or an explicit reason event');
|
|
23850
|
+
END;
|
|
23851
|
+
|
|
23852
|
+
-- \u2500\u2500 session_completeness view \u2500\u2500
|
|
23853
|
+
-- The published contract for "is this session actually complete, and if
|
|
23854
|
+
-- not, what's missing". Completion is DERIVED from the event log, never a
|
|
23855
|
+
-- mutable flag: a session is complete iff it is closed AND a terminal
|
|
23856
|
+
-- artifact event exists for its current round/run. The dashboard's
|
|
23857
|
+
-- outcome derivation and the agent 'status' command read this view, so
|
|
23858
|
+
-- they cannot disagree.
|
|
23859
|
+
--
|
|
23860
|
+
-- completeness_state is an INTENTIONAL HYBRID: it combines the mutable
|
|
23861
|
+
-- status column (marked_closed) with append-only event evidence (the
|
|
23862
|
+
-- terminal artifact event). This is sound precisely because the
|
|
23863
|
+
-- close-guard trigger above makes the status column trustworthy \u2014 a row
|
|
23864
|
+
-- can only reach status='closed' with a completed round/run or an
|
|
23865
|
+
-- explicit reason event \u2014 so reading the column is not a regression to
|
|
23866
|
+
-- the old "mutable flag that could lie" model.
|
|
23867
|
+
--
|
|
23868
|
+
-- completeness_state:
|
|
23869
|
+
-- 'complete' \u2014 closed + terminal artifact for current round/run
|
|
23870
|
+
-- 'closed_without_artifact' \u2014 closed but no terminal artifact (the
|
|
23871
|
+
-- "completed too soon" condition)
|
|
23872
|
+
-- 'in_flight' \u2014 open with a dependent process still running
|
|
23873
|
+
-- 'open_no_artifact' \u2014 open, no in-flight dependents
|
|
23874
|
+
CREATE VIEW IF NOT EXISTS session_completeness AS
|
|
23875
|
+
SELECT
|
|
23876
|
+
s.id AS session_id,
|
|
23877
|
+
s.workflow_type AS workflow_type,
|
|
23878
|
+
s.status AS status,
|
|
23879
|
+
s.current_round AS current_round,
|
|
23880
|
+
s.current_map_run AS current_map_run,
|
|
23881
|
+
CASE WHEN EXISTS (
|
|
23882
|
+
SELECT 1 FROM orchestration_events e
|
|
23883
|
+
WHERE e.session_id = s.id
|
|
23884
|
+
AND (
|
|
23885
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
23886
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
23887
|
+
)
|
|
23888
|
+
) THEN 1 ELSE 0 END AS has_terminal_artifact,
|
|
23889
|
+
CASE WHEN s.status = 'closed' THEN 1 ELSE 0 END AS marked_closed,
|
|
23890
|
+
CASE WHEN NOT EXISTS (
|
|
23891
|
+
SELECT 1 FROM command_executions ce
|
|
23892
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
23893
|
+
) THEN 1 ELSE 0 END AS dependents_settled,
|
|
23894
|
+
CASE
|
|
23895
|
+
WHEN s.status = 'closed' AND EXISTS (
|
|
23896
|
+
SELECT 1 FROM orchestration_events e
|
|
23897
|
+
WHERE e.session_id = s.id
|
|
23898
|
+
AND (
|
|
23899
|
+
(s.workflow_type = 'review' AND e.event_type = 'round_completed' AND e.round = s.current_round)
|
|
23900
|
+
OR (s.workflow_type = 'map' AND e.event_type = 'map_completed' AND e.round = s.current_map_run)
|
|
23901
|
+
)
|
|
23902
|
+
) THEN 'complete'
|
|
23903
|
+
WHEN s.status = 'closed' THEN 'closed_without_artifact'
|
|
23904
|
+
WHEN EXISTS (
|
|
23905
|
+
SELECT 1 FROM command_executions ce
|
|
23906
|
+
WHERE ce.workflow_id = s.id AND ce.finished_at IS NULL
|
|
23907
|
+
) THEN 'in_flight'
|
|
23908
|
+
ELSE 'open_no_artifact'
|
|
23909
|
+
END AS completeness_state
|
|
23910
|
+
FROM sessions s;
|
|
23911
|
+
`
|
|
23912
|
+
},
|
|
23913
|
+
{
|
|
23914
|
+
version: 13,
|
|
23915
|
+
description: "Retire dead parent_id column on command_executions (never written; row kind is derived from command)",
|
|
23916
|
+
// parent_id was reserved for an AI-instance → dashboard-spawn lineage link
|
|
23917
|
+
// that was never wired (no writer, no reader). A process's KIND (supervisor
|
|
23918
|
+
// / reviewer-instance / utility) is derived from columns that are always
|
|
23919
|
+
// present (command + last_heartbeat_at), so the dead lineage column and its
|
|
23920
|
+
// all-NULL index are removed. Re-add a wired parent_id alongside a real
|
|
23921
|
+
// consumer (e.g. a parent→child tree view) if lineage is ever needed.
|
|
23922
|
+
//
|
|
23923
|
+
// Imperative + guarded so the DROP COLUMN (which SQLite can't express as
|
|
23924
|
+
// IF EXISTS) is idempotent under re-application.
|
|
23925
|
+
run: (db) => {
|
|
23926
|
+
if (!columnExists(db, "command_executions", "parent_id")) return;
|
|
23927
|
+
db.run("DROP INDEX IF EXISTS idx_command_executions_parent;");
|
|
23928
|
+
db.run("ALTER TABLE command_executions DROP COLUMN parent_id;");
|
|
23929
|
+
}
|
|
23688
23930
|
}
|
|
23689
23931
|
];
|
|
23690
23932
|
}
|
|
@@ -23798,6 +24040,12 @@ function getLatestEventId(db) {
|
|
|
23798
24040
|
const val = result[0]?.values[0]?.[0];
|
|
23799
24041
|
return typeof val === "number" ? val : 0;
|
|
23800
24042
|
}
|
|
24043
|
+
function commitReasonClose(db, sessionId, reasonEvent, projectionUpdates) {
|
|
24044
|
+
db.transaction(() => {
|
|
24045
|
+
insertEvent(db, { session_id: sessionId, ...reasonEvent });
|
|
24046
|
+
updateSession(db, sessionId, projectionUpdates);
|
|
24047
|
+
});
|
|
24048
|
+
}
|
|
23801
24049
|
var init_queries = __esm({
|
|
23802
24050
|
"src/lib/db/queries.ts"() {
|
|
23803
24051
|
"use strict";
|
|
@@ -23805,7 +24053,208 @@ var init_queries = __esm({
|
|
|
23805
24053
|
}
|
|
23806
24054
|
});
|
|
23807
24055
|
|
|
24056
|
+
// src/lib/db/reconcile.ts
|
|
24057
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
24058
|
+
import { isAbsolute as isAbsolute2, join as join12, dirname as dirname4 } from "node:path";
|
|
24059
|
+
function hasTerminalArtifactEvent(db, sessionId, workflowType, currentRound, currentMapRun) {
|
|
24060
|
+
const eventType = workflowType === "map" ? "map_completed" : "round_completed";
|
|
24061
|
+
const round = workflowType === "map" ? currentMapRun : currentRound;
|
|
24062
|
+
const r = db.exec(
|
|
24063
|
+
`SELECT 1 FROM orchestration_events
|
|
24064
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
24065
|
+
[sessionId, eventType, round]
|
|
24066
|
+
);
|
|
24067
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
24068
|
+
}
|
|
24069
|
+
function hasReasonEvent(db, sessionId) {
|
|
24070
|
+
const r = db.exec(
|
|
24071
|
+
`SELECT 1 FROM orchestration_events
|
|
24072
|
+
WHERE session_id = ?
|
|
24073
|
+
AND event_type IN ('session_aborted','session_auto_closed_stale','session_synced','session_legacy_import')
|
|
24074
|
+
LIMIT 1`,
|
|
24075
|
+
[sessionId]
|
|
24076
|
+
);
|
|
24077
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
24078
|
+
}
|
|
24079
|
+
function lastEventAgeSeconds(db, sessionId) {
|
|
24080
|
+
const r = db.exec(
|
|
24081
|
+
`SELECT (julianday('now') - julianday(MAX(created_at))) * 86400
|
|
24082
|
+
FROM orchestration_events WHERE session_id = ?`,
|
|
24083
|
+
[sessionId]
|
|
24084
|
+
);
|
|
24085
|
+
const v = r[0]?.values[0]?.[0];
|
|
24086
|
+
return typeof v === "number" ? v : null;
|
|
24087
|
+
}
|
|
24088
|
+
function hasInFlightDependents(db, sessionId) {
|
|
24089
|
+
const r = db.exec(
|
|
24090
|
+
`SELECT 1 FROM command_executions
|
|
24091
|
+
WHERE workflow_id = ? AND finished_at IS NULL LIMIT 1`,
|
|
24092
|
+
[sessionId]
|
|
24093
|
+
);
|
|
24094
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
24095
|
+
}
|
|
24096
|
+
function resolveSessionDir(ocrDir, sessionDir) {
|
|
24097
|
+
if (!sessionDir) return null;
|
|
24098
|
+
if (isAbsolute2(sessionDir)) return sessionDir;
|
|
24099
|
+
return join12(dirname4(ocrDir), sessionDir);
|
|
24100
|
+
}
|
|
24101
|
+
function reconcileLegacyState(db, ocrDir, opts = {}) {
|
|
24102
|
+
const dryRun = opts.dryRun ?? false;
|
|
24103
|
+
const threshold = opts.staleThresholdSeconds ?? DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
24104
|
+
const actions = [];
|
|
24105
|
+
for (const s of getAllSessions(db)) {
|
|
24106
|
+
const dir = resolveSessionDir(ocrDir, s.session_dir);
|
|
24107
|
+
if (s.status === "closed") {
|
|
24108
|
+
if (hasTerminalArtifactEvent(db, s.id, s.workflow_type, s.current_round, s.current_map_run) || hasReasonEvent(db, s.id)) {
|
|
24109
|
+
continue;
|
|
24110
|
+
}
|
|
24111
|
+
const reviewFinal = s.workflow_type === "review" && dir ? existsSync10(join12(dir, "rounds", `round-${s.current_round}`, "final.md")) : false;
|
|
24112
|
+
const mapFinal = s.workflow_type === "map" && dir ? existsSync10(join12(dir, "map", "runs", `run-${s.current_map_run}`, "map.md")) : false;
|
|
24113
|
+
if (reviewFinal) {
|
|
24114
|
+
actions.push({
|
|
24115
|
+
sessionId: s.id,
|
|
24116
|
+
kind: "synthesize-round-completed",
|
|
24117
|
+
detail: `final.md present for round ${s.current_round}; synthesizing round_completed`
|
|
24118
|
+
});
|
|
24119
|
+
if (!dryRun) {
|
|
24120
|
+
insertEvent(db, {
|
|
24121
|
+
session_id: s.id,
|
|
24122
|
+
event_type: "round_completed",
|
|
24123
|
+
phase: "synthesis",
|
|
24124
|
+
phase_number: 7,
|
|
24125
|
+
round: s.current_round,
|
|
24126
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "final.md" })
|
|
24127
|
+
});
|
|
24128
|
+
}
|
|
24129
|
+
} else if (mapFinal) {
|
|
24130
|
+
actions.push({
|
|
24131
|
+
sessionId: s.id,
|
|
24132
|
+
kind: "synthesize-map-completed",
|
|
24133
|
+
detail: `map.md present for run ${s.current_map_run}; synthesizing map_completed`
|
|
24134
|
+
});
|
|
24135
|
+
if (!dryRun) {
|
|
24136
|
+
insertEvent(db, {
|
|
24137
|
+
session_id: s.id,
|
|
24138
|
+
event_type: "map_completed",
|
|
24139
|
+
phase: "synthesis",
|
|
24140
|
+
phase_number: 5,
|
|
24141
|
+
round: s.current_map_run,
|
|
24142
|
+
metadata: JSON.stringify({ source: "reconciled", synthesized_from: "map.md" })
|
|
24143
|
+
});
|
|
24144
|
+
}
|
|
24145
|
+
} else {
|
|
24146
|
+
actions.push({
|
|
24147
|
+
sessionId: s.id,
|
|
24148
|
+
kind: "grandfather",
|
|
24149
|
+
detail: "no provable artifact; recording session_legacy_import"
|
|
24150
|
+
});
|
|
24151
|
+
if (!dryRun) {
|
|
24152
|
+
insertEvent(db, {
|
|
24153
|
+
session_id: s.id,
|
|
24154
|
+
event_type: "session_legacy_import",
|
|
24155
|
+
phase: "complete",
|
|
24156
|
+
metadata: JSON.stringify({ source: "reconciled" })
|
|
24157
|
+
});
|
|
24158
|
+
}
|
|
24159
|
+
}
|
|
24160
|
+
continue;
|
|
24161
|
+
}
|
|
24162
|
+
const age = lastEventAgeSeconds(db, s.id);
|
|
24163
|
+
const stale = (age === null || age > threshold) && !hasInFlightDependents(db, s.id);
|
|
24164
|
+
if (stale) {
|
|
24165
|
+
actions.push({
|
|
24166
|
+
sessionId: s.id,
|
|
24167
|
+
kind: "stale-close",
|
|
24168
|
+
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`
|
|
24169
|
+
});
|
|
24170
|
+
if (!dryRun) {
|
|
24171
|
+
commitReasonClose(
|
|
24172
|
+
db,
|
|
24173
|
+
s.id,
|
|
24174
|
+
{
|
|
24175
|
+
event_type: "session_auto_closed_stale",
|
|
24176
|
+
phase: "complete",
|
|
24177
|
+
metadata: JSON.stringify({ source: "reconciled", threshold_seconds: threshold })
|
|
24178
|
+
},
|
|
24179
|
+
{ status: "closed", current_phase: "complete" }
|
|
24180
|
+
);
|
|
24181
|
+
}
|
|
24182
|
+
}
|
|
24183
|
+
}
|
|
24184
|
+
return { dryRun, actions };
|
|
24185
|
+
}
|
|
24186
|
+
var DEFAULT_STALE_THRESHOLD_SECONDS;
|
|
24187
|
+
var init_reconcile = __esm({
|
|
24188
|
+
"src/lib/db/reconcile.ts"() {
|
|
24189
|
+
"use strict";
|
|
24190
|
+
init_queries();
|
|
24191
|
+
DEFAULT_STALE_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
|
|
24192
|
+
}
|
|
24193
|
+
});
|
|
24194
|
+
|
|
24195
|
+
// src/lib/db/liveness.ts
|
|
24196
|
+
function defaultIsAlive(pid) {
|
|
24197
|
+
try {
|
|
24198
|
+
process.kill(pid, 0);
|
|
24199
|
+
return true;
|
|
24200
|
+
} catch (err) {
|
|
24201
|
+
return !(err instanceof Error && "code" in err && err.code === "ESRCH");
|
|
24202
|
+
}
|
|
24203
|
+
}
|
|
24204
|
+
function sqliteUtcMs(ts) {
|
|
24205
|
+
const sqliteShape = ts.includes(" ");
|
|
24206
|
+
return new Date(sqliteShape ? ts.replace(" ", "T") + "Z" : ts).getTime();
|
|
24207
|
+
}
|
|
24208
|
+
var PID_REUSE_GUARD_MS;
|
|
24209
|
+
var init_liveness = __esm({
|
|
24210
|
+
"src/lib/db/liveness.ts"() {
|
|
24211
|
+
"use strict";
|
|
24212
|
+
PID_REUSE_GUARD_MS = 24 * 60 * 60 * 1e3;
|
|
24213
|
+
}
|
|
24214
|
+
});
|
|
24215
|
+
|
|
24216
|
+
// src/lib/state/exit-codes.ts
|
|
24217
|
+
var STATE_EXIT, StateError, CANCELLED_EXIT_CODE, ORPHAN_EXIT_CODE, CASCADE_CLOSE_EXIT_CODE;
|
|
24218
|
+
var init_exit_codes = __esm({
|
|
24219
|
+
"src/lib/state/exit-codes.ts"() {
|
|
24220
|
+
"use strict";
|
|
24221
|
+
STATE_EXIT = {
|
|
24222
|
+
OK: 0,
|
|
24223
|
+
USAGE: 2,
|
|
24224
|
+
AMBIGUOUS: 3,
|
|
24225
|
+
NOT_FOUND: 4,
|
|
24226
|
+
ILLEGAL_TRANSITION: 5,
|
|
24227
|
+
INVARIANT_UNMET: 6,
|
|
24228
|
+
SCHEMA_INVALID: 7,
|
|
24229
|
+
/** Database was locked past the bounded retry budget (SQLITE_BUSY). */
|
|
24230
|
+
BUSY: 8
|
|
24231
|
+
};
|
|
24232
|
+
StateError = class extends Error {
|
|
24233
|
+
constructor(code, message) {
|
|
24234
|
+
super(message);
|
|
24235
|
+
this.code = code;
|
|
24236
|
+
this.name = "StateError";
|
|
24237
|
+
}
|
|
24238
|
+
};
|
|
24239
|
+
CANCELLED_EXIT_CODE = -2;
|
|
24240
|
+
ORPHAN_EXIT_CODE = -3;
|
|
24241
|
+
CASCADE_CLOSE_EXIT_CODE = -4;
|
|
24242
|
+
}
|
|
24243
|
+
});
|
|
24244
|
+
|
|
23808
24245
|
// src/lib/db/agent-sessions.ts
|
|
24246
|
+
function cascadeTerminateExecutions(db, workflowId, exitCode, note) {
|
|
24247
|
+
db.run(
|
|
24248
|
+
`UPDATE command_executions
|
|
24249
|
+
SET finished_at = datetime('now'),
|
|
24250
|
+
exit_code = ?,
|
|
24251
|
+
pid = NULL,
|
|
24252
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
24253
|
+
WHERE workflow_id = ?
|
|
24254
|
+
AND finished_at IS NULL`,
|
|
24255
|
+
[exitCode, note, workflowId]
|
|
24256
|
+
);
|
|
24257
|
+
}
|
|
23809
24258
|
function rowToAgentSession(row) {
|
|
23810
24259
|
return {
|
|
23811
24260
|
// The OCR-owned id is the `uid` column. Fall back to the integer
|
|
@@ -23820,6 +24269,7 @@ function rowToAgentSession(row) {
|
|
|
23820
24269
|
resolved_model: row.resolved_model,
|
|
23821
24270
|
phase: null,
|
|
23822
24271
|
status: deriveStatus(row),
|
|
24272
|
+
kind: rowKind(row),
|
|
23823
24273
|
pid: row.pid,
|
|
23824
24274
|
started_at: row.started_at,
|
|
23825
24275
|
last_heartbeat_at: row.last_heartbeat_at ?? row.started_at,
|
|
@@ -23833,7 +24283,9 @@ function deriveStatus(row) {
|
|
|
23833
24283
|
return "running";
|
|
23834
24284
|
}
|
|
23835
24285
|
if (row.exit_code === ORPHAN_EXIT_CODE) return "orphaned";
|
|
23836
|
-
if (row.exit_code === CANCELLED_EXIT_CODE)
|
|
24286
|
+
if (row.exit_code === CANCELLED_EXIT_CODE || row.exit_code === CASCADE_CLOSE_EXIT_CODE) {
|
|
24287
|
+
return "cancelled";
|
|
24288
|
+
}
|
|
23837
24289
|
if (row.exit_code === 0) return "done";
|
|
23838
24290
|
return "crashed";
|
|
23839
24291
|
}
|
|
@@ -23849,7 +24301,7 @@ function insertAgentSession(db, params) {
|
|
|
23849
24301
|
pid = null,
|
|
23850
24302
|
notes = null
|
|
23851
24303
|
} = params;
|
|
23852
|
-
const command = persona && instance_index !== null ?
|
|
24304
|
+
const command = persona && instance_index !== null ? `${INSTANCE_COMMAND}:${persona}-${instance_index}` : INSTANCE_COMMAND;
|
|
23853
24305
|
db.run(
|
|
23854
24306
|
`INSERT INTO command_executions
|
|
23855
24307
|
(uid, command, args, workflow_id, vendor, persona, instance_index, name,
|
|
@@ -24054,63 +24506,139 @@ function updateAgentSession(db, id, params) {
|
|
|
24054
24506
|
values
|
|
24055
24507
|
);
|
|
24056
24508
|
}
|
|
24057
|
-
function sweepStaleAgentSessions(db, thresholdSeconds) {
|
|
24058
|
-
const
|
|
24059
|
-
|
|
24060
|
-
|
|
24061
|
-
|
|
24062
|
-
|
|
24063
|
-
|
|
24064
|
-
|
|
24065
|
-
|
|
24509
|
+
function sweepStaleAgentSessions(db, thresholdSeconds, isAlive = defaultIsAlive) {
|
|
24510
|
+
const candidates = resultToRows(
|
|
24511
|
+
db.exec(
|
|
24512
|
+
`SELECT uid, id, pid, started_at, workflow_id, command, last_heartbeat_at
|
|
24513
|
+
FROM command_executions
|
|
24514
|
+
WHERE finished_at IS NULL
|
|
24515
|
+
AND pid IS NOT NULL
|
|
24516
|
+
AND last_heartbeat_at IS NOT NULL
|
|
24517
|
+
AND (julianday('now') - julianday(last_heartbeat_at)) * 86400 > ?`,
|
|
24518
|
+
[thresholdSeconds]
|
|
24519
|
+
)
|
|
24066
24520
|
);
|
|
24067
|
-
if (
|
|
24068
|
-
return { orphanedIds: [] };
|
|
24521
|
+
if (candidates.length === 0) {
|
|
24522
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
24523
|
+
}
|
|
24524
|
+
const reuseCutoffMs = Date.now() - PID_REUSE_GUARD_MS;
|
|
24525
|
+
const dead = candidates.filter((row) => {
|
|
24526
|
+
if (row.pid === null) return false;
|
|
24527
|
+
if (sqliteUtcMs(row.started_at) < reuseCutoffMs) return false;
|
|
24528
|
+
return !isAlive(row.pid);
|
|
24529
|
+
});
|
|
24530
|
+
if (dead.length === 0) {
|
|
24531
|
+
return { orphanedIds: [], cascadedWorkflowIds: [] };
|
|
24069
24532
|
}
|
|
24070
24533
|
const note = `${NOTE_ORPHAN_PREFIX} (threshold ${thresholdSeconds}s)`;
|
|
24071
|
-
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
|
|
24079
|
-
|
|
24080
|
-
|
|
24534
|
+
const placeholders = dead.map(() => "?").join(", ");
|
|
24535
|
+
const cascadedWorkflowIds = [];
|
|
24536
|
+
db.transaction(() => {
|
|
24537
|
+
db.run(
|
|
24538
|
+
`UPDATE command_executions
|
|
24539
|
+
SET finished_at = datetime('now'),
|
|
24540
|
+
exit_code = ?,
|
|
24541
|
+
pid = NULL,
|
|
24542
|
+
notes = COALESCE(notes || char(10), '') || ?
|
|
24543
|
+
WHERE id IN (${placeholders})
|
|
24544
|
+
AND finished_at IS NULL`,
|
|
24545
|
+
[ORPHAN_EXIT_CODE, note, ...dead.map((r) => r.id)]
|
|
24546
|
+
);
|
|
24547
|
+
for (const row of dead) {
|
|
24548
|
+
if (row.workflow_id && rowKind(row) === "supervisor") {
|
|
24549
|
+
cascadeTerminateExecutions(
|
|
24550
|
+
db,
|
|
24551
|
+
row.workflow_id,
|
|
24552
|
+
CASCADE_CLOSE_EXIT_CODE,
|
|
24553
|
+
"cascade-closed: workflow process orphaned by liveness sweep"
|
|
24554
|
+
);
|
|
24555
|
+
cascadedWorkflowIds.push(row.workflow_id);
|
|
24556
|
+
}
|
|
24557
|
+
}
|
|
24558
|
+
});
|
|
24081
24559
|
return {
|
|
24082
|
-
orphanedIds:
|
|
24560
|
+
orphanedIds: dead.map((r) => r.uid ?? String(r.id)),
|
|
24561
|
+
cascadedWorkflowIds
|
|
24083
24562
|
};
|
|
24084
24563
|
}
|
|
24085
|
-
|
|
24564
|
+
function rowKind(row) {
|
|
24565
|
+
if (row.command === INSTANCE_COMMAND || row.command.startsWith(`${INSTANCE_COMMAND}:`)) {
|
|
24566
|
+
return "instance";
|
|
24567
|
+
}
|
|
24568
|
+
return row.last_heartbeat_at == null ? "utility" : "supervisor";
|
|
24569
|
+
}
|
|
24570
|
+
function sweepStaleSessions(db, thresholdSeconds) {
|
|
24571
|
+
const sql = `
|
|
24572
|
+
SELECT s.id
|
|
24573
|
+
FROM sessions s
|
|
24574
|
+
LEFT JOIN (
|
|
24575
|
+
SELECT session_id, MAX(created_at) AS last_event_at
|
|
24576
|
+
FROM orchestration_events
|
|
24577
|
+
GROUP BY session_id
|
|
24578
|
+
) e ON e.session_id = s.id
|
|
24579
|
+
WHERE s.status = 'active'
|
|
24580
|
+
AND (
|
|
24581
|
+
e.last_event_at IS NULL
|
|
24582
|
+
OR (julianday('now') - julianday(e.last_event_at)) * 86400 > ?
|
|
24583
|
+
)
|
|
24584
|
+
AND NOT EXISTS (
|
|
24585
|
+
SELECT 1 FROM command_executions ce
|
|
24586
|
+
WHERE ce.workflow_id = s.id
|
|
24587
|
+
AND ce.finished_at IS NULL
|
|
24588
|
+
)
|
|
24589
|
+
`;
|
|
24590
|
+
const rows = resultToRows(db.exec(sql, [thresholdSeconds]));
|
|
24591
|
+
if (rows.length === 0) {
|
|
24592
|
+
return { closedSessionIds: [] };
|
|
24593
|
+
}
|
|
24594
|
+
for (const row of rows) {
|
|
24595
|
+
commitReasonClose(
|
|
24596
|
+
db,
|
|
24597
|
+
row.id,
|
|
24598
|
+
{
|
|
24599
|
+
event_type: "session_auto_closed_stale",
|
|
24600
|
+
phase: "complete",
|
|
24601
|
+
metadata: JSON.stringify({
|
|
24602
|
+
reason: "no events past threshold; no in-flight dependents",
|
|
24603
|
+
threshold_seconds: thresholdSeconds
|
|
24604
|
+
})
|
|
24605
|
+
},
|
|
24606
|
+
{ status: "closed", current_phase: "complete" }
|
|
24607
|
+
);
|
|
24608
|
+
}
|
|
24609
|
+
return { closedSessionIds: rows.map((r) => r.id) };
|
|
24610
|
+
}
|
|
24611
|
+
var NOTE_ORPHAN_PREFIX, INSTANCE_COMMAND;
|
|
24086
24612
|
var init_agent_sessions = __esm({
|
|
24087
24613
|
"src/lib/db/agent-sessions.ts"() {
|
|
24088
24614
|
"use strict";
|
|
24089
24615
|
init_result_mapper();
|
|
24090
|
-
|
|
24091
|
-
|
|
24616
|
+
init_queries();
|
|
24617
|
+
init_liveness();
|
|
24618
|
+
init_exit_codes();
|
|
24092
24619
|
NOTE_ORPHAN_PREFIX = "orphaned by liveness sweep";
|
|
24620
|
+
INSTANCE_COMMAND = "session-instance";
|
|
24093
24621
|
}
|
|
24094
24622
|
});
|
|
24095
24623
|
|
|
24096
24624
|
// src/lib/db/command-log.ts
|
|
24097
|
-
import { appendFileSync, existsSync as
|
|
24098
|
-
import { dirname as
|
|
24625
|
+
import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
24626
|
+
import { dirname as dirname5, join as join13 } from "node:path";
|
|
24099
24627
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
24100
24628
|
function generateCommandUid() {
|
|
24101
24629
|
return randomUUID2();
|
|
24102
24630
|
}
|
|
24103
24631
|
function cacheDir(ocrDir) {
|
|
24104
|
-
return
|
|
24632
|
+
return join13(ocrDir, "data", CACHE_DIR);
|
|
24105
24633
|
}
|
|
24106
24634
|
function commandLogPath(ocrDir) {
|
|
24107
|
-
return
|
|
24635
|
+
return join13(cacheDir(ocrDir), FILENAME);
|
|
24108
24636
|
}
|
|
24109
24637
|
function appendCommandLog(ocrDir, entry) {
|
|
24110
24638
|
try {
|
|
24111
24639
|
const filePath = commandLogPath(ocrDir);
|
|
24112
|
-
const dir =
|
|
24113
|
-
if (!
|
|
24640
|
+
const dir = dirname5(filePath);
|
|
24641
|
+
if (!existsSync11(dir)) mkdirSync3(dir, { recursive: true });
|
|
24114
24642
|
const line = JSON.stringify(entry) + "\n";
|
|
24115
24643
|
appendFileSync(filePath, line, { encoding: "utf-8" });
|
|
24116
24644
|
if (approxLineCount >= 0) approxLineCount++;
|
|
@@ -24120,7 +24648,7 @@ function appendCommandLog(ocrDir, entry) {
|
|
|
24120
24648
|
}
|
|
24121
24649
|
function readCommandLog(ocrDir) {
|
|
24122
24650
|
const filePath = commandLogPath(ocrDir);
|
|
24123
|
-
if (!
|
|
24651
|
+
if (!existsSync11(filePath)) return [];
|
|
24124
24652
|
const content = readFileSync9(filePath, "utf-8");
|
|
24125
24653
|
const entries = [];
|
|
24126
24654
|
for (const line of content.split("\n")) {
|
|
@@ -24199,16 +24727,25 @@ var init_command_log = __esm({
|
|
|
24199
24727
|
// src/lib/db/index.ts
|
|
24200
24728
|
var db_exports = {};
|
|
24201
24729
|
__export(db_exports, {
|
|
24730
|
+
CANCELLED_EXIT_CODE: () => CANCELLED_EXIT_CODE,
|
|
24731
|
+
CASCADE_CLOSE_EXIT_CODE: () => CASCADE_CLOSE_EXIT_CODE,
|
|
24202
24732
|
MIGRATIONS: () => MIGRATIONS,
|
|
24733
|
+
ORPHAN_EXIT_CODE: () => ORPHAN_EXIT_CODE,
|
|
24734
|
+
PID_REUSE_GUARD_MS: () => PID_REUSE_GUARD_MS,
|
|
24735
|
+
STATE_EXIT: () => STATE_EXIT,
|
|
24736
|
+
StateError: () => StateError,
|
|
24203
24737
|
appendCommandLog: () => appendCommandLog,
|
|
24204
|
-
applyPragmas: () => applyPragmas,
|
|
24205
24738
|
bindVendorSessionIdOpportunistically: () => bindVendorSessionIdOpportunistically,
|
|
24206
24739
|
bumpAgentSessionHeartbeat: () => bumpAgentSessionHeartbeat,
|
|
24207
24740
|
cacheDir: () => cacheDir,
|
|
24741
|
+
cascadeTerminateExecutions: () => cascadeTerminateExecutions,
|
|
24208
24742
|
closeAllDatabases: () => closeAllDatabases,
|
|
24209
24743
|
closeDatabase: () => closeDatabase,
|
|
24210
24744
|
commandLogPath: () => commandLogPath,
|
|
24745
|
+
commitReasonClose: () => commitReasonClose,
|
|
24746
|
+
defaultIsAlive: () => defaultIsAlive,
|
|
24211
24747
|
ensureDatabase: () => ensureDatabase,
|
|
24748
|
+
formatUpgradeNotice: () => formatUpgradeNotice,
|
|
24212
24749
|
generateCommandUid: () => generateCommandUid,
|
|
24213
24750
|
getAgentSession: () => getAgentSession,
|
|
24214
24751
|
getAllSessions: () => getAllSessions,
|
|
@@ -24217,119 +24754,144 @@ __export(db_exports, {
|
|
|
24217
24754
|
getLatestActiveSession: () => getLatestActiveSession,
|
|
24218
24755
|
getLatestAgentSessionWithVendorId: () => getLatestAgentSessionWithVendorId,
|
|
24219
24756
|
getLatestEventId: () => getLatestEventId,
|
|
24757
|
+
getSchemaVersion: () => getSchemaVersion,
|
|
24220
24758
|
getSession: () => getSession,
|
|
24221
24759
|
insertAgentSession: () => insertAgentSession,
|
|
24222
24760
|
insertEvent: () => insertEvent,
|
|
24223
24761
|
insertSession: () => insertSession,
|
|
24762
|
+
isBusyError: () => isBusyError,
|
|
24224
24763
|
linkDashboardInvocationToWorkflow: () => linkDashboardInvocationToWorkflow,
|
|
24225
24764
|
listAgentSessionsForWorkflow: () => listAgentSessionsForWorkflow,
|
|
24226
|
-
locateWasm: () => locateWasm,
|
|
24227
24765
|
openDatabase: () => openDatabase,
|
|
24766
|
+
probeEngine: () => probeEngine,
|
|
24228
24767
|
readCommandLog: () => readCommandLog,
|
|
24768
|
+
reconcileLegacyState: () => reconcileLegacyState,
|
|
24229
24769
|
recordVendorSessionIdForExecution: () => recordVendorSessionIdForExecution,
|
|
24230
24770
|
replayCommandLog: () => replayCommandLog,
|
|
24231
24771
|
resultToRow: () => resultToRow,
|
|
24232
24772
|
resultToRows: () => resultToRows,
|
|
24773
|
+
rowKind: () => rowKind,
|
|
24233
24774
|
runMigrations: () => runMigrations,
|
|
24234
|
-
saveDatabase: () => saveDatabase,
|
|
24235
24775
|
setAgentSessionStatus: () => setAgentSessionStatus,
|
|
24236
24776
|
setAgentSessionVendorId: () => setAgentSessionVendorId,
|
|
24777
|
+
sqliteUtcMs: () => sqliteUtcMs,
|
|
24237
24778
|
sweepStaleAgentSessions: () => sweepStaleAgentSessions,
|
|
24779
|
+
sweepStaleSessions: () => sweepStaleSessions,
|
|
24238
24780
|
updateAgentSession: () => updateAgentSession,
|
|
24239
24781
|
updateSession: () => updateSession,
|
|
24240
24782
|
walCheckpointTruncate: () => walCheckpointTruncate
|
|
24241
24783
|
});
|
|
24242
|
-
import { existsSync as
|
|
24243
|
-
import { dirname as
|
|
24244
|
-
|
|
24245
|
-
|
|
24246
|
-
|
|
24247
|
-
|
|
24248
|
-
|
|
24249
|
-
|
|
24250
|
-
|
|
24251
|
-
|
|
24252
|
-
|
|
24253
|
-
|
|
24254
|
-
|
|
24255
|
-
|
|
24784
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync4, copyFileSync, statSync } from "node:fs";
|
|
24785
|
+
import { dirname as dirname6, join as join14 } from "node:path";
|
|
24786
|
+
function maybeSnapshotBeforeUpgrade(db, dbPath, fromVersion) {
|
|
24787
|
+
if (fromVersion < 1 || fromVersion >= V2_SCHEMA_VERSION) return null;
|
|
24788
|
+
const bakPath = `${dbPath}.bak.v${fromVersion}`;
|
|
24789
|
+
if (existsSync12(bakPath)) return bakPath;
|
|
24790
|
+
try {
|
|
24791
|
+
if (!existsSync12(dbPath) || statSync(dbPath).size === 0) return null;
|
|
24792
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
24793
|
+
copyFileSync(dbPath, bakPath);
|
|
24794
|
+
return bakPath;
|
|
24795
|
+
} catch {
|
|
24796
|
+
return null;
|
|
24797
|
+
}
|
|
24798
|
+
}
|
|
24799
|
+
function formatUpgradeNotice(bakPath, reconcile) {
|
|
24800
|
+
const lines = [
|
|
24801
|
+
"Storage upgraded to v2.0 \u2014 durable SQLite engine (WAL), event-sourced lifecycle."
|
|
24802
|
+
];
|
|
24803
|
+
if (bakPath) {
|
|
24804
|
+
lines.push(` A backup of your previous database was saved to: ${bakPath}`);
|
|
24805
|
+
}
|
|
24806
|
+
const repairs = (reconcile?.actions ?? []).filter((a) => a.kind !== "ok");
|
|
24807
|
+
if (repairs.length > 0) {
|
|
24808
|
+
const n = (kind) => repairs.filter((a) => a.kind === kind).length;
|
|
24809
|
+
const parts = [];
|
|
24810
|
+
const finalized = n("synthesize-round-completed") + n("synthesize-map-completed");
|
|
24811
|
+
if (finalized > 0) parts.push(`${finalized} finalized from artifacts`);
|
|
24812
|
+
if (n("grandfather") > 0) parts.push(`${n("grandfather")} grandfathered`);
|
|
24813
|
+
if (n("stale-close") > 0) parts.push(`${n("stale-close")} stale closed`);
|
|
24814
|
+
lines.push(
|
|
24815
|
+
` Reconciled ${repairs.length} legacy session(s): ${parts.join(", ")}.`
|
|
24816
|
+
);
|
|
24817
|
+
}
|
|
24818
|
+
lines.push(" Run `ocr doctor` to verify the storage engine.");
|
|
24819
|
+
return lines.map((l) => `[ocr] ${l}`).join("\n");
|
|
24256
24820
|
}
|
|
24257
24821
|
async function openDatabase(dbPath) {
|
|
24258
24822
|
const cached = connections.get(dbPath);
|
|
24259
24823
|
if (cached) {
|
|
24260
24824
|
return cached;
|
|
24261
24825
|
}
|
|
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();
|
|
24826
|
+
const dir = dirname6(dbPath);
|
|
24827
|
+
if (!existsSync12(dir)) {
|
|
24828
|
+
mkdirSync4(dir, { recursive: true });
|
|
24276
24829
|
}
|
|
24277
|
-
|
|
24830
|
+
const db = openEngine(dbPath);
|
|
24278
24831
|
connections.set(dbPath, db);
|
|
24279
24832
|
return db;
|
|
24280
24833
|
}
|
|
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
24834
|
async function getDb(ocrDir) {
|
|
24292
|
-
const dbPath =
|
|
24835
|
+
const dbPath = join14(ocrDir, "data", "ocr.db");
|
|
24293
24836
|
return openDatabase(dbPath);
|
|
24294
24837
|
}
|
|
24295
24838
|
async function ensureDatabase(ocrDir) {
|
|
24296
|
-
const dataDir =
|
|
24297
|
-
if (!
|
|
24839
|
+
const dataDir = join14(ocrDir, "data");
|
|
24840
|
+
if (!existsSync12(dataDir)) {
|
|
24298
24841
|
mkdirSync4(dataDir, { recursive: true });
|
|
24299
24842
|
}
|
|
24300
|
-
const dbPath =
|
|
24843
|
+
const dbPath = join14(dataDir, "ocr.db");
|
|
24301
24844
|
const db = await openDatabase(dbPath);
|
|
24845
|
+
let before = 0;
|
|
24846
|
+
try {
|
|
24847
|
+
before = getSchemaVersion(db);
|
|
24848
|
+
} catch {
|
|
24849
|
+
before = 0;
|
|
24850
|
+
}
|
|
24851
|
+
const isLegacyUpgrade = before >= 1 && before < V2_SCHEMA_VERSION;
|
|
24852
|
+
const bakPath = maybeSnapshotBeforeUpgrade(db, dbPath, before);
|
|
24302
24853
|
runMigrations(db);
|
|
24303
|
-
|
|
24854
|
+
let reconcile;
|
|
24855
|
+
if (before < V2_SCHEMA_VERSION) {
|
|
24856
|
+
try {
|
|
24857
|
+
reconcile = reconcileLegacyState(db, ocrDir);
|
|
24858
|
+
} catch (err) {
|
|
24859
|
+
console.error(
|
|
24860
|
+
`[ocr] legacy reconciliation skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
24861
|
+
);
|
|
24862
|
+
}
|
|
24863
|
+
}
|
|
24864
|
+
if (isLegacyUpgrade) {
|
|
24865
|
+
const notice = formatUpgradeNotice(bakPath, reconcile);
|
|
24866
|
+
if (notice) console.error(notice);
|
|
24867
|
+
}
|
|
24304
24868
|
return db;
|
|
24305
24869
|
}
|
|
24306
24870
|
function walCheckpointTruncate(dbPath) {
|
|
24307
|
-
if (!
|
|
24871
|
+
if (!existsSync12(dbPath)) {
|
|
24308
24872
|
return "skipped";
|
|
24309
24873
|
}
|
|
24310
|
-
|
|
24311
|
-
|
|
24312
|
-
|
|
24313
|
-
|
|
24314
|
-
|
|
24315
|
-
|
|
24316
|
-
return "
|
|
24874
|
+
const cached = connections.get(dbPath);
|
|
24875
|
+
if (cached) {
|
|
24876
|
+
try {
|
|
24877
|
+
cached.pragma("wal_checkpoint(TRUNCATE)");
|
|
24878
|
+
return "checkpointed";
|
|
24879
|
+
} catch {
|
|
24880
|
+
return "failed";
|
|
24317
24881
|
}
|
|
24318
|
-
} catch {
|
|
24319
|
-
return "skipped";
|
|
24320
24882
|
}
|
|
24883
|
+
let transient;
|
|
24321
24884
|
try {
|
|
24322
|
-
|
|
24323
|
-
|
|
24324
|
-
|
|
24325
|
-
{
|
|
24326
|
-
stdio: "ignore",
|
|
24327
|
-
timeout: 5e3
|
|
24328
|
-
}
|
|
24329
|
-
);
|
|
24330
|
-
return result.status === 0 ? "checkpointed" : "failed";
|
|
24885
|
+
transient = openEngine(dbPath);
|
|
24886
|
+
transient.pragma("wal_checkpoint(TRUNCATE)");
|
|
24887
|
+
return "checkpointed";
|
|
24331
24888
|
} catch {
|
|
24332
24889
|
return "failed";
|
|
24890
|
+
} finally {
|
|
24891
|
+
try {
|
|
24892
|
+
transient?.raw.close();
|
|
24893
|
+
} catch {
|
|
24894
|
+
}
|
|
24333
24895
|
}
|
|
24334
24896
|
}
|
|
24335
24897
|
function closeDatabase(dbPath) {
|
|
@@ -24345,16 +24907,24 @@ function closeAllDatabases() {
|
|
|
24345
24907
|
connections.delete(path2);
|
|
24346
24908
|
}
|
|
24347
24909
|
}
|
|
24348
|
-
var connections;
|
|
24910
|
+
var V2_SCHEMA_VERSION, connections;
|
|
24349
24911
|
var init_db = __esm({
|
|
24350
24912
|
"src/lib/db/index.ts"() {
|
|
24351
24913
|
"use strict";
|
|
24914
|
+
init_engine();
|
|
24352
24915
|
init_migrations();
|
|
24916
|
+
init_reconcile();
|
|
24353
24917
|
init_queries();
|
|
24354
24918
|
init_agent_sessions();
|
|
24919
|
+
init_liveness();
|
|
24920
|
+
init_exit_codes();
|
|
24355
24921
|
init_migrations();
|
|
24356
24922
|
init_result_mapper();
|
|
24923
|
+
init_engine();
|
|
24924
|
+
init_reconcile();
|
|
24925
|
+
init_migrations();
|
|
24357
24926
|
init_command_log();
|
|
24927
|
+
V2_SCHEMA_VERSION = 12;
|
|
24358
24928
|
connections = /* @__PURE__ */ new Map();
|
|
24359
24929
|
}
|
|
24360
24930
|
});
|
|
@@ -28763,7 +29333,7 @@ ${hint}
|
|
|
28763
29333
|
}
|
|
28764
29334
|
|
|
28765
29335
|
// src/lib/version.ts
|
|
28766
|
-
var CLI_VERSION = true ? "
|
|
29336
|
+
var CLI_VERSION = true ? "2.0.0" : createRequire(import.meta.url)("../../package.json").version;
|
|
28767
29337
|
|
|
28768
29338
|
// ../shared/platform/src/index.ts
|
|
28769
29339
|
import { pathToFileURL } from "node:url";
|
|
@@ -29822,9 +30392,9 @@ var NodeFsHandler = class {
|
|
|
29822
30392
|
if (this.fsw.closed) {
|
|
29823
30393
|
return;
|
|
29824
30394
|
}
|
|
29825
|
-
const
|
|
30395
|
+
const dirname8 = sysPath.dirname(file);
|
|
29826
30396
|
const basename8 = sysPath.basename(file);
|
|
29827
|
-
const parent = this.fsw._getWatchedDir(
|
|
30397
|
+
const parent = this.fsw._getWatchedDir(dirname8);
|
|
29828
30398
|
let prevStats = stats;
|
|
29829
30399
|
if (parent.has(basename8))
|
|
29830
30400
|
return;
|
|
@@ -29851,7 +30421,7 @@ var NodeFsHandler = class {
|
|
|
29851
30421
|
prevStats = newStats2;
|
|
29852
30422
|
}
|
|
29853
30423
|
} catch (error) {
|
|
29854
|
-
this.fsw._remove(
|
|
30424
|
+
this.fsw._remove(dirname8, basename8);
|
|
29855
30425
|
}
|
|
29856
30426
|
} else if (parent.has(basename8)) {
|
|
29857
30427
|
const at = newStats.atimeMs;
|
|
@@ -30784,8 +31354,8 @@ function watch(paths, options = {}) {
|
|
|
30784
31354
|
}
|
|
30785
31355
|
|
|
30786
31356
|
// src/commands/progress.ts
|
|
30787
|
-
import { existsSync as
|
|
30788
|
-
import { join as
|
|
31357
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
31358
|
+
import { join as join15, basename as basename7 } from "node:path";
|
|
30789
31359
|
|
|
30790
31360
|
// ../../node_modules/.pnpm/log-update@7.0.2/node_modules/log-update/index.js
|
|
30791
31361
|
import process12 from "node:process";
|
|
@@ -32421,15 +32991,15 @@ function debounce(fn, delay) {
|
|
|
32421
32991
|
};
|
|
32422
32992
|
}
|
|
32423
32993
|
function findLatestActiveSession(sessionsDir) {
|
|
32424
|
-
if (!
|
|
32994
|
+
if (!existsSync13(sessionsDir)) {
|
|
32425
32995
|
return null;
|
|
32426
32996
|
}
|
|
32427
32997
|
const sessions = readdirSync5(sessionsDir).filter((name) => {
|
|
32428
|
-
const sessionPath =
|
|
32429
|
-
return
|
|
32998
|
+
const sessionPath = join15(sessionsDir, name);
|
|
32999
|
+
return statSync2(sessionPath).isDirectory();
|
|
32430
33000
|
}).sort().reverse();
|
|
32431
33001
|
for (const session of sessions) {
|
|
32432
|
-
const sessionPath =
|
|
33002
|
+
const sessionPath = join15(sessionsDir, session);
|
|
32433
33003
|
if (isSessionActive(sessionPath)) {
|
|
32434
33004
|
return session;
|
|
32435
33005
|
}
|
|
@@ -32444,8 +33014,8 @@ function getStrategyForSession(sessionPath, explicitWorkflow) {
|
|
|
32444
33014
|
return getStrategy(workflowType) ?? null;
|
|
32445
33015
|
}
|
|
32446
33016
|
async function initProgressDb(ocrDir) {
|
|
32447
|
-
const dbPath =
|
|
32448
|
-
if (!
|
|
33017
|
+
const dbPath = join15(ocrDir, "data", "ocr.db");
|
|
33018
|
+
if (!existsSync13(dbPath)) {
|
|
32449
33019
|
return;
|
|
32450
33020
|
}
|
|
32451
33021
|
try {
|
|
@@ -32470,11 +33040,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32470
33040
|
const targetDir = process.cwd();
|
|
32471
33041
|
requireOcrSetup(targetDir);
|
|
32472
33042
|
const sessionsDir = ensureSessionsDir(targetDir);
|
|
32473
|
-
const ocrDir =
|
|
33043
|
+
const ocrDir = join15(targetDir, ".ocr");
|
|
32474
33044
|
await initProgressDb(ocrDir);
|
|
32475
33045
|
if (options.session) {
|
|
32476
|
-
const sessionPath =
|
|
32477
|
-
if (!
|
|
33046
|
+
const sessionPath = join15(sessionsDir, options.session);
|
|
33047
|
+
if (!existsSync13(sessionPath)) {
|
|
32478
33048
|
console.error(source_default.red(`Session not found: ${options.session}`));
|
|
32479
33049
|
process.exit(1);
|
|
32480
33050
|
}
|
|
@@ -32499,7 +33069,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32499
33069
|
);
|
|
32500
33070
|
console.error(
|
|
32501
33071
|
source_default.dim(
|
|
32502
|
-
`The orchestrating agent must create state via 'ocr state
|
|
33072
|
+
`The orchestrating agent must create state via 'ocr state begin' for progress tracking.`
|
|
32503
33073
|
)
|
|
32504
33074
|
);
|
|
32505
33075
|
process.exit(1);
|
|
@@ -32535,7 +33105,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32535
33105
|
return;
|
|
32536
33106
|
}
|
|
32537
33107
|
let currentSession = findLatestActiveSession(sessionsDir);
|
|
32538
|
-
let currentSessionPath = currentSession ?
|
|
33108
|
+
let currentSessionPath = currentSession ? join15(sessionsDir, currentSession) : null;
|
|
32539
33109
|
let sessionWatcher = null;
|
|
32540
33110
|
const preservedStartTimes = {
|
|
32541
33111
|
review: void 0,
|
|
@@ -32543,11 +33113,11 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32543
33113
|
};
|
|
32544
33114
|
let currentStrategy = null;
|
|
32545
33115
|
const updateDisplayImpl = () => {
|
|
32546
|
-
if (!currentSessionPath || !
|
|
33116
|
+
if (!currentSessionPath || !existsSync13(currentSessionPath) || !isSessionActive(currentSessionPath)) {
|
|
32547
33117
|
const latestActive = findLatestActiveSession(sessionsDir);
|
|
32548
33118
|
if (latestActive && latestActive !== currentSession) {
|
|
32549
33119
|
currentSession = latestActive;
|
|
32550
|
-
currentSessionPath =
|
|
33120
|
+
currentSessionPath = join15(sessionsDir, latestActive);
|
|
32551
33121
|
preservedStartTimes.review = void 0;
|
|
32552
33122
|
preservedStartTimes.map = void 0;
|
|
32553
33123
|
currentStrategy = null;
|
|
@@ -32560,7 +33130,7 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32560
33130
|
currentStrategy = null;
|
|
32561
33131
|
}
|
|
32562
33132
|
}
|
|
32563
|
-
if (currentSessionPath &&
|
|
33133
|
+
if (currentSessionPath && existsSync13(currentSessionPath)) {
|
|
32564
33134
|
if (!options.workflow) {
|
|
32565
33135
|
const activeWorkflows = detectActiveWorkflows(currentSessionPath);
|
|
32566
33136
|
if (activeWorkflows.length > 1) {
|
|
@@ -32614,15 +33184,15 @@ var progressCommand = new Command("progress").description("Watch real-time progr
|
|
|
32614
33184
|
watchSession(currentSessionPath);
|
|
32615
33185
|
}
|
|
32616
33186
|
const timerInterval = setInterval(updateDisplay, 1e3);
|
|
32617
|
-
const watchDir =
|
|
33187
|
+
const watchDir = existsSync13(ocrDir) ? ocrDir : targetDir;
|
|
32618
33188
|
const dirWatcher = watch(watchDir, {
|
|
32619
33189
|
persistent: true,
|
|
32620
33190
|
ignoreInitial: true,
|
|
32621
33191
|
depth: 3
|
|
32622
33192
|
});
|
|
32623
33193
|
dirWatcher.on("addDir", (dirPath) => {
|
|
32624
|
-
const parentDir =
|
|
32625
|
-
const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(
|
|
33194
|
+
const parentDir = join15(dirPath, "..");
|
|
33195
|
+
const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join15(".ocr", "sessions"));
|
|
32626
33196
|
if (isDirectChild && !dirPath.endsWith("sessions")) {
|
|
32627
33197
|
const newSession = basename7(dirPath);
|
|
32628
33198
|
currentSession = newSession;
|
|
@@ -32724,226 +33294,115 @@ function renderCombinedProgress(sessionPath, preservedStartTimes, ocrDir) {
|
|
|
32724
33294
|
}
|
|
32725
33295
|
|
|
32726
33296
|
// src/commands/state.ts
|
|
32727
|
-
import { existsSync as
|
|
32728
|
-
import { join as
|
|
33297
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
33298
|
+
import { join as join17 } from "node:path";
|
|
32729
33299
|
|
|
32730
33300
|
// src/lib/state/index.ts
|
|
32731
33301
|
init_db();
|
|
33302
|
+
init_exit_codes();
|
|
32732
33303
|
import {
|
|
32733
|
-
existsSync as
|
|
33304
|
+
existsSync as existsSync14,
|
|
32734
33305
|
mkdirSync as mkdirSync5,
|
|
32735
33306
|
readdirSync as readdirSync6,
|
|
32736
|
-
readFileSync as
|
|
32737
|
-
statSync as
|
|
32738
|
-
writeFileSync as
|
|
33307
|
+
readFileSync as readFileSync10,
|
|
33308
|
+
statSync as statSync3,
|
|
33309
|
+
writeFileSync as writeFileSync7
|
|
32739
33310
|
} from "node:fs";
|
|
32740
|
-
import { join as
|
|
32741
|
-
|
|
32742
|
-
|
|
32743
|
-
|
|
32744
|
-
|
|
32745
|
-
|
|
32746
|
-
|
|
32747
|
-
|
|
32748
|
-
|
|
32749
|
-
|
|
32750
|
-
|
|
33311
|
+
import { join as join16 } from "node:path";
|
|
33312
|
+
|
|
33313
|
+
// src/lib/state/phase-graph.ts
|
|
33314
|
+
init_exit_codes();
|
|
33315
|
+
var REVIEW_PHASE_NUMBERS = {
|
|
33316
|
+
context: 1,
|
|
33317
|
+
"change-context": 2,
|
|
33318
|
+
analysis: 3,
|
|
33319
|
+
reviews: 4,
|
|
33320
|
+
aggregation: 5,
|
|
33321
|
+
discourse: 6,
|
|
33322
|
+
synthesis: 7,
|
|
33323
|
+
complete: 8
|
|
33324
|
+
};
|
|
33325
|
+
var MAP_PHASE_NUMBERS = {
|
|
33326
|
+
"map-context": 1,
|
|
33327
|
+
topology: 2,
|
|
33328
|
+
"flow-analysis": 3,
|
|
33329
|
+
"requirements-mapping": 4,
|
|
33330
|
+
synthesis: 5,
|
|
33331
|
+
complete: 6
|
|
33332
|
+
};
|
|
33333
|
+
function phaseNumberFor(workflowType, phase) {
|
|
33334
|
+
const map = workflowType === "map" ? MAP_PHASE_NUMBERS : REVIEW_PHASE_NUMBERS;
|
|
33335
|
+
const n = map[phase];
|
|
33336
|
+
if (n === void 0) {
|
|
33337
|
+
throw new StateError(
|
|
33338
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33339
|
+
`Invalid phase "${phase}" for workflow_type "${workflowType}". Valid: ${Object.keys(map).join(", ")}`
|
|
33340
|
+
);
|
|
32751
33341
|
}
|
|
32752
|
-
return
|
|
33342
|
+
return n;
|
|
32753
33343
|
}
|
|
32754
|
-
|
|
32755
|
-
|
|
32756
|
-
|
|
32757
|
-
|
|
32758
|
-
|
|
32759
|
-
|
|
32760
|
-
|
|
32761
|
-
|
|
32762
|
-
|
|
32763
|
-
|
|
32764
|
-
|
|
32765
|
-
|
|
32766
|
-
|
|
32767
|
-
|
|
32768
|
-
|
|
32769
|
-
|
|
32770
|
-
|
|
32771
|
-
|
|
32772
|
-
|
|
32773
|
-
|
|
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;
|
|
33344
|
+
var REVIEW_PHASE_GRAPH = {
|
|
33345
|
+
context: ["change-context"],
|
|
33346
|
+
"change-context": ["analysis"],
|
|
33347
|
+
analysis: ["reviews"],
|
|
33348
|
+
reviews: ["aggregation"],
|
|
33349
|
+
aggregation: ["discourse"],
|
|
33350
|
+
discourse: ["synthesis"],
|
|
33351
|
+
synthesis: ["complete"],
|
|
33352
|
+
complete: ["context"]
|
|
33353
|
+
};
|
|
33354
|
+
var MAP_PHASE_GRAPH = {
|
|
33355
|
+
"map-context": ["topology"],
|
|
33356
|
+
topology: ["flow-analysis"],
|
|
33357
|
+
"flow-analysis": ["requirements-mapping"],
|
|
33358
|
+
"requirements-mapping": ["synthesis"],
|
|
33359
|
+
synthesis: ["complete"],
|
|
33360
|
+
complete: ["map-context"]
|
|
33361
|
+
};
|
|
33362
|
+
function graphFor(workflowType) {
|
|
33363
|
+
return workflowType === "review" ? REVIEW_PHASE_GRAPH : MAP_PHASE_GRAPH;
|
|
32807
33364
|
}
|
|
32808
|
-
|
|
32809
|
-
|
|
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
|
-
});
|
|
32838
|
-
}
|
|
32839
|
-
saveDatabase(db, dbPath);
|
|
33365
|
+
function initialPhaseFor(workflowType) {
|
|
33366
|
+
return workflowType === "map" ? "map-context" : "context";
|
|
32840
33367
|
}
|
|
32841
|
-
|
|
32842
|
-
const
|
|
32843
|
-
|
|
32844
|
-
|
|
32845
|
-
|
|
32846
|
-
|
|
32847
|
-
|
|
33368
|
+
function validatePhaseTransition(workflowType, source, target, isRoundBoundary) {
|
|
33369
|
+
const graph = graphFor(workflowType);
|
|
33370
|
+
if (!(target in graph)) {
|
|
33371
|
+
const validPhases = Object.keys(graph).join(", ");
|
|
33372
|
+
throw new StateError(
|
|
33373
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33374
|
+
`Invalid phase "${target}" for workflow_type "${workflowType}". Valid phases: ${validPhases}`
|
|
33375
|
+
);
|
|
32848
33376
|
}
|
|
32849
|
-
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
|
|
32855
|
-
|
|
32856
|
-
|
|
32857
|
-
phase_number: existing.phase_number,
|
|
32858
|
-
round: existing.current_round
|
|
32859
|
-
});
|
|
32860
|
-
saveDatabase(db, dbPath);
|
|
32861
|
-
}
|
|
32862
|
-
async function stateShow(ocrDir, sessionId) {
|
|
32863
|
-
let db;
|
|
32864
|
-
try {
|
|
32865
|
-
db = await ensureDatabase(ocrDir);
|
|
32866
|
-
} catch {
|
|
32867
|
-
return null;
|
|
33377
|
+
if (source === target) return;
|
|
33378
|
+
if (isRoundBoundary) {
|
|
33379
|
+
const initial = initialPhaseFor(workflowType);
|
|
33380
|
+
if (target === initial) return;
|
|
33381
|
+
throw new StateError(
|
|
33382
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33383
|
+
`Illegal round-boundary transition: a new round/run must reset to "${initial}", not "${target}".`
|
|
33384
|
+
);
|
|
32868
33385
|
}
|
|
32869
|
-
const
|
|
32870
|
-
if (!
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
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
|
-
};
|
|
32897
|
-
}
|
|
32898
|
-
async function resolveActiveSession(ocrDir) {
|
|
32899
|
-
const db = await ensureDatabase(ocrDir);
|
|
32900
|
-
const session = getLatestActiveSession(db);
|
|
32901
|
-
if (!session) {
|
|
32902
|
-
throw new Error("No active session found");
|
|
32903
|
-
}
|
|
32904
|
-
return {
|
|
32905
|
-
id: session.id,
|
|
32906
|
-
sessionDir: session.session_dir
|
|
32907
|
-
};
|
|
32908
|
-
}
|
|
32909
|
-
function readJsonFromSource(params) {
|
|
32910
|
-
if (params.source === "file") {
|
|
32911
|
-
if (!existsSync13(params.filePath)) {
|
|
32912
|
-
throw new Error(`File not found: ${params.filePath}`);
|
|
32913
|
-
}
|
|
32914
|
-
return readFileSync11(params.filePath, "utf-8");
|
|
32915
|
-
}
|
|
32916
|
-
return params.data;
|
|
32917
|
-
}
|
|
32918
|
-
function parseRawJson(raw, label) {
|
|
32919
|
-
try {
|
|
32920
|
-
return JSON.parse(raw);
|
|
32921
|
-
} catch (err) {
|
|
32922
|
-
throw new Error(
|
|
32923
|
-
`Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
|
|
32924
|
-
);
|
|
33386
|
+
const allowed = graph[source];
|
|
33387
|
+
if (!allowed || !allowed.includes(target)) {
|
|
33388
|
+
throw new StateError(
|
|
33389
|
+
STATE_EXIT.ILLEGAL_TRANSITION,
|
|
33390
|
+
`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.`
|
|
33391
|
+
);
|
|
32925
33392
|
}
|
|
32926
33393
|
}
|
|
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
|
-
};
|
|
33394
|
+
|
|
33395
|
+
// src/lib/state/meta-util.ts
|
|
33396
|
+
var DEFAULT_METADATA_MAX_LEN = 4096;
|
|
33397
|
+
function sanitizeMetadataString(s, opts = {}) {
|
|
33398
|
+
const maxLen = opts.maxLen ?? DEFAULT_METADATA_MAX_LEN;
|
|
33399
|
+
let out = s.replace(/[\x00-\x08\x0b-\x1f]/g, "");
|
|
33400
|
+
out = out.replace(/^\s*\[ocr\]\s*/i, "");
|
|
33401
|
+
if (out.length > maxLen) out = out.slice(0, maxLen);
|
|
33402
|
+
return out;
|
|
32946
33403
|
}
|
|
33404
|
+
|
|
33405
|
+
// src/lib/state/round-meta.ts
|
|
32947
33406
|
var VALID_CATEGORIES = /* @__PURE__ */ new Set(["blocker", "should_fix", "suggestion", "style"]);
|
|
32948
33407
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
|
|
32949
33408
|
function validateRoundMeta(meta) {
|
|
@@ -32959,6 +33418,7 @@ function validateRoundMeta(meta) {
|
|
|
32959
33418
|
if (typeof obj.verdict !== "string" || obj.verdict.trim().length === 0) {
|
|
32960
33419
|
throw new Error("round-meta.json must contain a non-empty verdict string");
|
|
32961
33420
|
}
|
|
33421
|
+
obj.verdict = sanitizeMetadataString(obj.verdict);
|
|
32962
33422
|
if (!Array.isArray(obj.reviewers)) {
|
|
32963
33423
|
throw new Error("round-meta.json must contain a reviewers array");
|
|
32964
33424
|
}
|
|
@@ -32984,6 +33444,7 @@ function validateRoundMeta(meta) {
|
|
|
32984
33444
|
if (typeof f.title !== "string" || f.title.trim().length === 0) {
|
|
32985
33445
|
throw new Error("Each finding must have a non-empty title");
|
|
32986
33446
|
}
|
|
33447
|
+
f.title = sanitizeMetadataString(f.title);
|
|
32987
33448
|
if (typeof f.category !== "string" || !VALID_CATEGORIES.has(f.category)) {
|
|
32988
33449
|
throw new Error(
|
|
32989
33450
|
`Finding "${f.title}" has invalid category: "${String(f.category)}". Must be one of: ${[...VALID_CATEGORIES].join(", ")}`
|
|
@@ -32997,6 +33458,7 @@ function validateRoundMeta(meta) {
|
|
|
32997
33458
|
if (typeof f.summary !== "string") {
|
|
32998
33459
|
throw new Error(`Finding "${f.title}" must have a summary string`);
|
|
32999
33460
|
}
|
|
33461
|
+
f.summary = sanitizeMetadataString(f.summary);
|
|
33000
33462
|
if (f.file_path !== void 0 && typeof f.file_path !== "string") {
|
|
33001
33463
|
throw new Error(`Finding "${f.title}" has invalid file_path: expected string`);
|
|
33002
33464
|
}
|
|
@@ -33042,43 +33504,8 @@ function computeRoundCounts(meta) {
|
|
|
33042
33504
|
totalFindingCount: allFindings.length
|
|
33043
33505
|
};
|
|
33044
33506
|
}
|
|
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
|
-
}
|
|
33507
|
+
|
|
33508
|
+
// src/lib/state/map-meta.ts
|
|
33082
33509
|
function validateMapMeta(meta) {
|
|
33083
33510
|
if (!meta || typeof meta !== "object") {
|
|
33084
33511
|
throw new Error("map-meta.json must be a JSON object");
|
|
@@ -33103,6 +33530,13 @@ function validateMapMeta(meta) {
|
|
|
33103
33530
|
if (typeof s.title !== "string" || s.title.trim().length === 0) {
|
|
33104
33531
|
throw new Error("Each section must have a non-empty title");
|
|
33105
33532
|
}
|
|
33533
|
+
s.title = sanitizeMetadataString(s.title);
|
|
33534
|
+
if (s.description !== void 0) {
|
|
33535
|
+
if (typeof s.description !== "string") {
|
|
33536
|
+
throw new Error(`Section "${s.title}" description must be a string if provided`);
|
|
33537
|
+
}
|
|
33538
|
+
s.description = sanitizeMetadataString(s.description);
|
|
33539
|
+
}
|
|
33106
33540
|
if (!Array.isArray(s.files)) {
|
|
33107
33541
|
throw new Error(`Section "${s.title}" must have a files array`);
|
|
33108
33542
|
}
|
|
@@ -33117,6 +33551,7 @@ function validateMapMeta(meta) {
|
|
|
33117
33551
|
if (typeof f.role !== "string") {
|
|
33118
33552
|
throw new Error(`File "${f.file_path}" must have a role string`);
|
|
33119
33553
|
}
|
|
33554
|
+
f.role = sanitizeMetadataString(f.role);
|
|
33120
33555
|
if (typeof f.lines_added !== "number") {
|
|
33121
33556
|
throw new Error(`File "${f.file_path}" must have a lines_added number`);
|
|
33122
33557
|
}
|
|
@@ -33136,53 +33571,597 @@ function computeMapCounts(meta) {
|
|
|
33136
33571
|
fileCount: meta.sections.reduce((sum, s) => sum + s.files.length, 0)
|
|
33137
33572
|
};
|
|
33138
33573
|
}
|
|
33139
|
-
|
|
33140
|
-
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
33144
|
-
|
|
33145
|
-
|
|
33146
|
-
|
|
33147
|
-
|
|
33148
|
-
|
|
33149
|
-
|
|
33150
|
-
|
|
33151
|
-
|
|
33152
|
-
|
|
33153
|
-
|
|
33154
|
-
|
|
33155
|
-
|
|
33156
|
-
|
|
33157
|
-
|
|
33158
|
-
|
|
33159
|
-
|
|
33160
|
-
|
|
33161
|
-
|
|
33162
|
-
|
|
33163
|
-
|
|
33164
|
-
|
|
33165
|
-
|
|
33166
|
-
|
|
33167
|
-
|
|
33168
|
-
|
|
33169
|
-
|
|
33170
|
-
|
|
33171
|
-
|
|
33574
|
+
|
|
33575
|
+
// src/lib/state/projection.ts
|
|
33576
|
+
init_db();
|
|
33577
|
+
var REASON_EVENT_TYPES = [
|
|
33578
|
+
"session_aborted",
|
|
33579
|
+
"session_auto_closed_stale",
|
|
33580
|
+
"session_synced",
|
|
33581
|
+
"session_legacy_import"
|
|
33582
|
+
];
|
|
33583
|
+
var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
33584
|
+
"session_closed",
|
|
33585
|
+
...REASON_EVENT_TYPES
|
|
33586
|
+
]);
|
|
33587
|
+
function hasCompletionInvariant(db, session) {
|
|
33588
|
+
const eventType = session.workflow_type === "map" ? "map_completed" : "round_completed";
|
|
33589
|
+
const round = session.workflow_type === "map" ? session.current_map_run : session.current_round;
|
|
33590
|
+
const r = db.exec(
|
|
33591
|
+
`SELECT 1 FROM orchestration_events
|
|
33592
|
+
WHERE session_id = ? AND event_type = ? AND round = ? LIMIT 1`,
|
|
33593
|
+
[session.id, eventType, round]
|
|
33594
|
+
);
|
|
33595
|
+
return (r[0]?.values.length ?? 0) > 0;
|
|
33596
|
+
}
|
|
33597
|
+
function getCompletenessState(db, sessionId) {
|
|
33598
|
+
const r = db.exec(
|
|
33599
|
+
"SELECT completeness_state FROM session_completeness WHERE session_id = ?",
|
|
33600
|
+
[sessionId]
|
|
33601
|
+
);
|
|
33602
|
+
return r[0]?.values[0]?.[0] ?? null;
|
|
33603
|
+
}
|
|
33604
|
+
|
|
33605
|
+
// src/lib/state/index.ts
|
|
33606
|
+
init_exit_codes();
|
|
33607
|
+
function deriveNextRound(db, sessionId, fallbackRound) {
|
|
33608
|
+
const result = db.exec(
|
|
33609
|
+
`SELECT MAX(round) FROM orchestration_events
|
|
33610
|
+
WHERE session_id = ? AND event_type = 'round_completed'`,
|
|
33611
|
+
[sessionId]
|
|
33612
|
+
);
|
|
33613
|
+
const max = result[0]?.values[0]?.[0];
|
|
33614
|
+
if (typeof max === "number") return max + 1;
|
|
33615
|
+
return fallbackRound;
|
|
33616
|
+
}
|
|
33617
|
+
function hasArtifacts(dir) {
|
|
33618
|
+
try {
|
|
33619
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
33620
|
+
if (entry.isDirectory()) {
|
|
33621
|
+
if (hasArtifacts(join16(dir, entry.name))) return true;
|
|
33622
|
+
} else if (/\.(md|json)$/.test(entry.name)) {
|
|
33623
|
+
return true;
|
|
33624
|
+
}
|
|
33625
|
+
}
|
|
33626
|
+
} catch {
|
|
33627
|
+
}
|
|
33628
|
+
return false;
|
|
33629
|
+
}
|
|
33630
|
+
function readJsonFromSource(params) {
|
|
33631
|
+
if (params.source === "file") {
|
|
33632
|
+
if (!existsSync14(params.filePath)) {
|
|
33633
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `File not found: ${params.filePath}`);
|
|
33634
|
+
}
|
|
33635
|
+
return readFileSync10(params.filePath, "utf-8");
|
|
33636
|
+
}
|
|
33637
|
+
return params.data;
|
|
33638
|
+
}
|
|
33639
|
+
function parseRawJson(raw, label) {
|
|
33640
|
+
try {
|
|
33641
|
+
return JSON.parse(raw);
|
|
33642
|
+
} catch (err) {
|
|
33643
|
+
throw new StateError(
|
|
33644
|
+
STATE_EXIT.SCHEMA_INVALID,
|
|
33645
|
+
`Failed to parse ${label}: ${err instanceof Error ? err.message : "invalid JSON"}`
|
|
33646
|
+
);
|
|
33647
|
+
}
|
|
33648
|
+
}
|
|
33649
|
+
async function stateInit(params) {
|
|
33650
|
+
const { sessionId, branch, workflowType, sessionDir, ocrDir } = params;
|
|
33651
|
+
const db = await ensureDatabase(ocrDir);
|
|
33652
|
+
const existing = getSession(db, sessionId);
|
|
33653
|
+
if (existing) {
|
|
33654
|
+
if (existing.workflow_type !== workflowType) {
|
|
33655
|
+
throw new StateError(
|
|
33656
|
+
STATE_EXIT.USAGE,
|
|
33657
|
+
`Cannot re-open session ${sessionId} as workflow_type "${workflowType}": existing workflow_type is "${existing.workflow_type}". Maps and reviews have disjoint phase graphs.`
|
|
33658
|
+
);
|
|
33659
|
+
}
|
|
33660
|
+
const nextRound = deriveNextRound(db, sessionId, existing.current_round);
|
|
33661
|
+
const initialPhase2 = workflowType === "map" ? "map-context" : "context";
|
|
33662
|
+
db.transaction(() => {
|
|
33663
|
+
updateSession(db, sessionId, {
|
|
33664
|
+
status: "active",
|
|
33665
|
+
current_phase: initialPhase2,
|
|
33666
|
+
phase_number: 1,
|
|
33667
|
+
current_round: nextRound
|
|
33668
|
+
});
|
|
33669
|
+
insertEvent(db, {
|
|
33670
|
+
session_id: sessionId,
|
|
33671
|
+
event_type: nextRound > (existing.current_round ?? 1) ? "round_started" : "session_resumed",
|
|
33672
|
+
phase: initialPhase2,
|
|
33673
|
+
phase_number: 1,
|
|
33674
|
+
round: nextRound
|
|
33675
|
+
});
|
|
33676
|
+
});
|
|
33677
|
+
return sessionId;
|
|
33678
|
+
}
|
|
33679
|
+
const initialPhase = workflowType === "map" ? "map-context" : "context";
|
|
33680
|
+
db.transaction(() => {
|
|
33681
|
+
insertSession(db, {
|
|
33682
|
+
id: sessionId,
|
|
33683
|
+
branch,
|
|
33684
|
+
workflow_type: workflowType,
|
|
33685
|
+
current_phase: initialPhase,
|
|
33686
|
+
phase_number: 1,
|
|
33687
|
+
current_round: 1,
|
|
33688
|
+
current_map_run: 1,
|
|
33689
|
+
session_dir: sessionDir
|
|
33690
|
+
});
|
|
33691
|
+
insertEvent(db, {
|
|
33692
|
+
session_id: sessionId,
|
|
33693
|
+
event_type: "session_created",
|
|
33694
|
+
phase: initialPhase,
|
|
33695
|
+
phase_number: 1,
|
|
33696
|
+
round: 1
|
|
33697
|
+
});
|
|
33698
|
+
});
|
|
33699
|
+
return sessionId;
|
|
33700
|
+
}
|
|
33701
|
+
async function stateTransition(params, db) {
|
|
33702
|
+
const { sessionId, phase, phaseNumber, round, mapRun, ocrDir } = params;
|
|
33703
|
+
db ??= await ensureDatabase(ocrDir);
|
|
33704
|
+
const existing = getSession(db, sessionId);
|
|
33705
|
+
if (!existing) {
|
|
33706
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
|
|
33707
|
+
}
|
|
33708
|
+
const previousRound = existing.current_round;
|
|
33709
|
+
const previousMapRun = existing.current_map_run;
|
|
33710
|
+
const isRoundBoundary = round !== void 0 && round !== previousRound || mapRun !== void 0 && mapRun !== previousMapRun;
|
|
33711
|
+
validatePhaseTransition(
|
|
33712
|
+
existing.workflow_type,
|
|
33713
|
+
existing.current_phase,
|
|
33714
|
+
phase,
|
|
33715
|
+
isRoundBoundary
|
|
33716
|
+
);
|
|
33717
|
+
db.transaction(() => {
|
|
33718
|
+
updateSession(db, sessionId, {
|
|
33719
|
+
current_phase: phase,
|
|
33720
|
+
phase_number: phaseNumber,
|
|
33721
|
+
...round !== void 0 ? { current_round: round } : {},
|
|
33722
|
+
...mapRun !== void 0 ? { current_map_run: mapRun } : {}
|
|
33723
|
+
});
|
|
33724
|
+
insertEvent(db, {
|
|
33725
|
+
session_id: sessionId,
|
|
33726
|
+
event_type: "phase_transition",
|
|
33727
|
+
phase,
|
|
33728
|
+
phase_number: phaseNumber,
|
|
33729
|
+
round: round ?? existing.current_round
|
|
33730
|
+
});
|
|
33731
|
+
if (round !== void 0 && round !== previousRound) {
|
|
33732
|
+
insertEvent(db, {
|
|
33733
|
+
session_id: sessionId,
|
|
33734
|
+
event_type: "round_started",
|
|
33735
|
+
phase,
|
|
33736
|
+
phase_number: phaseNumber,
|
|
33737
|
+
round
|
|
33738
|
+
});
|
|
33739
|
+
}
|
|
33740
|
+
});
|
|
33741
|
+
}
|
|
33742
|
+
async function stateClose(params) {
|
|
33743
|
+
const { sessionId, ocrDir, abort } = params;
|
|
33744
|
+
const db = await ensureDatabase(ocrDir);
|
|
33745
|
+
const existing = getSession(db, sessionId);
|
|
33746
|
+
if (!existing) {
|
|
33747
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${sessionId}`);
|
|
33748
|
+
}
|
|
33749
|
+
if (existing.status === "closed") {
|
|
33750
|
+
console.error(`[ocr] Session already closed: ${sessionId}`);
|
|
33751
|
+
return;
|
|
33752
|
+
}
|
|
33753
|
+
if (!abort && !hasCompletionInvariant(db, existing)) {
|
|
33754
|
+
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`;
|
|
33755
|
+
throw new StateError(
|
|
33756
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
33757
|
+
`Cannot close session ${sessionId}: ${what}. Run 'ocr state complete-round' to finalize it, or pass --abort to record an abandoned session.`
|
|
33758
|
+
);
|
|
33759
|
+
}
|
|
33760
|
+
const note = "closed by parent workflow close";
|
|
33761
|
+
db.transaction(() => {
|
|
33762
|
+
if (abort) {
|
|
33763
|
+
insertEvent(db, {
|
|
33764
|
+
session_id: sessionId,
|
|
33765
|
+
event_type: "session_aborted",
|
|
33766
|
+
phase: existing.current_phase,
|
|
33767
|
+
phase_number: existing.phase_number,
|
|
33768
|
+
round: existing.current_round
|
|
33769
|
+
});
|
|
33770
|
+
}
|
|
33771
|
+
updateSession(db, sessionId, {
|
|
33772
|
+
status: "closed",
|
|
33773
|
+
current_phase: "complete"
|
|
33774
|
+
});
|
|
33775
|
+
if (!abort) {
|
|
33776
|
+
insertEvent(db, {
|
|
33777
|
+
session_id: sessionId,
|
|
33778
|
+
event_type: "session_closed",
|
|
33779
|
+
phase: "complete",
|
|
33780
|
+
phase_number: existing.phase_number,
|
|
33781
|
+
round: existing.current_round
|
|
33782
|
+
});
|
|
33783
|
+
}
|
|
33784
|
+
cascadeTerminateExecutions(db, sessionId, CASCADE_CLOSE_EXIT_CODE, note);
|
|
33785
|
+
});
|
|
33786
|
+
}
|
|
33787
|
+
async function stateShow(ocrDir, sessionId) {
|
|
33788
|
+
let db;
|
|
33789
|
+
try {
|
|
33790
|
+
db = await ensureDatabase(ocrDir);
|
|
33791
|
+
} catch {
|
|
33792
|
+
return null;
|
|
33793
|
+
}
|
|
33794
|
+
const session = sessionId ? getSession(db, sessionId) : getLatestActiveSession(db);
|
|
33795
|
+
if (!session) {
|
|
33796
|
+
return null;
|
|
33797
|
+
}
|
|
33798
|
+
const events = getEventsForSession(db, session.id);
|
|
33799
|
+
return {
|
|
33800
|
+
session: {
|
|
33801
|
+
id: session.id,
|
|
33802
|
+
branch: session.branch,
|
|
33803
|
+
status: session.status,
|
|
33804
|
+
workflow_type: session.workflow_type,
|
|
33805
|
+
current_phase: session.current_phase,
|
|
33806
|
+
phase_number: session.phase_number,
|
|
33807
|
+
current_round: session.current_round,
|
|
33808
|
+
current_map_run: session.current_map_run,
|
|
33809
|
+
started_at: session.started_at,
|
|
33810
|
+
updated_at: session.updated_at
|
|
33811
|
+
},
|
|
33812
|
+
events: events.map((e) => ({
|
|
33813
|
+
id: e.id,
|
|
33814
|
+
event_type: e.event_type,
|
|
33815
|
+
phase: e.phase,
|
|
33816
|
+
phase_number: e.phase_number,
|
|
33817
|
+
round: e.round,
|
|
33818
|
+
metadata: e.metadata,
|
|
33819
|
+
created_at: e.created_at
|
|
33820
|
+
}))
|
|
33821
|
+
};
|
|
33822
|
+
}
|
|
33823
|
+
function resolveSession(db, explicitId) {
|
|
33824
|
+
if (explicitId) {
|
|
33825
|
+
const s = getSession(db, explicitId);
|
|
33826
|
+
if (!s) throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${explicitId}`);
|
|
33827
|
+
return {
|
|
33828
|
+
id: s.id,
|
|
33829
|
+
session_dir: s.session_dir,
|
|
33830
|
+
current_round: s.current_round,
|
|
33831
|
+
current_map_run: s.current_map_run,
|
|
33832
|
+
workflow_type: s.workflow_type,
|
|
33833
|
+
status: s.status,
|
|
33834
|
+
current_phase: s.current_phase,
|
|
33835
|
+
phase_number: s.phase_number,
|
|
33836
|
+
branch: s.branch,
|
|
33837
|
+
decision: "explicit"
|
|
33838
|
+
};
|
|
33839
|
+
}
|
|
33840
|
+
const uid = process.env["OCR_DASHBOARD_EXECUTION_UID"];
|
|
33841
|
+
if (uid) {
|
|
33842
|
+
const result = db.exec(
|
|
33843
|
+
"SELECT workflow_id FROM command_executions WHERE uid = ?",
|
|
33844
|
+
[uid]
|
|
33845
|
+
);
|
|
33846
|
+
const workflowId = result[0]?.values[0]?.[0];
|
|
33847
|
+
if (workflowId) {
|
|
33848
|
+
const s = getSession(db, workflowId);
|
|
33849
|
+
if (s) {
|
|
33850
|
+
return {
|
|
33851
|
+
id: s.id,
|
|
33852
|
+
session_dir: s.session_dir,
|
|
33853
|
+
current_round: s.current_round,
|
|
33854
|
+
current_map_run: s.current_map_run,
|
|
33855
|
+
workflow_type: s.workflow_type,
|
|
33856
|
+
status: s.status,
|
|
33857
|
+
current_phase: s.current_phase,
|
|
33858
|
+
phase_number: s.phase_number,
|
|
33859
|
+
branch: s.branch,
|
|
33860
|
+
decision: "dashboard-uid"
|
|
33861
|
+
};
|
|
33862
|
+
}
|
|
33863
|
+
}
|
|
33864
|
+
}
|
|
33865
|
+
const activeRows = db.exec(
|
|
33866
|
+
`SELECT id, session_dir, current_round, current_map_run, workflow_type,
|
|
33867
|
+
status, current_phase, phase_number, branch
|
|
33868
|
+
FROM sessions
|
|
33869
|
+
WHERE status = 'active'
|
|
33870
|
+
ORDER BY started_at DESC`
|
|
33871
|
+
);
|
|
33872
|
+
const rows = activeRows[0]?.values ?? [];
|
|
33873
|
+
if (rows.length === 0) throw new StateError(STATE_EXIT.NOT_FOUND, "No active session found");
|
|
33874
|
+
if (rows.length > 1) {
|
|
33875
|
+
const ids = rows.map((r) => r[0]);
|
|
33876
|
+
throw new StateError(
|
|
33877
|
+
STATE_EXIT.AMBIGUOUS,
|
|
33878
|
+
`Ambiguous auto-detect: ${rows.length} active sessions exist. Pass --session-id explicitly. Candidates: ${ids.join(", ")}`
|
|
33879
|
+
);
|
|
33880
|
+
}
|
|
33881
|
+
const row = rows[0];
|
|
33882
|
+
return {
|
|
33883
|
+
id: row[0],
|
|
33884
|
+
session_dir: row[1],
|
|
33885
|
+
current_round: row[2],
|
|
33886
|
+
current_map_run: row[3],
|
|
33887
|
+
workflow_type: row[4],
|
|
33888
|
+
status: row[5],
|
|
33889
|
+
current_phase: row[6],
|
|
33890
|
+
phase_number: row[7],
|
|
33891
|
+
branch: row[8],
|
|
33892
|
+
decision: "latest-active"
|
|
33893
|
+
};
|
|
33894
|
+
}
|
|
33895
|
+
function announceResolveDecision(r) {
|
|
33896
|
+
if (r.decision === "explicit") return;
|
|
33897
|
+
const path2 = r.decision === "dashboard-uid" ? "via OCR_DASHBOARD_EXECUTION_UID" : "via latest-active";
|
|
33898
|
+
console.error(`[ocr] Auto-detected session: ${r.id} (${path2})`);
|
|
33899
|
+
}
|
|
33900
|
+
async function resolveActiveSession(ocrDir, explicitId) {
|
|
33901
|
+
const db = await ensureDatabase(ocrDir);
|
|
33902
|
+
const result = resolveSession(db, explicitId);
|
|
33903
|
+
announceResolveDecision(result);
|
|
33904
|
+
return {
|
|
33905
|
+
id: result.id,
|
|
33906
|
+
sessionDir: result.session_dir,
|
|
33907
|
+
decision: result.decision
|
|
33908
|
+
};
|
|
33909
|
+
}
|
|
33910
|
+
async function stateBegin(params) {
|
|
33911
|
+
const id = await stateInit(params);
|
|
33912
|
+
const db = await ensureDatabase(params.ocrDir);
|
|
33913
|
+
const s = getSession(db, id);
|
|
33914
|
+
return {
|
|
33915
|
+
schema_version: 1,
|
|
33916
|
+
session_id: id,
|
|
33917
|
+
round: s?.current_round ?? 1,
|
|
33918
|
+
phase: s?.current_phase ?? "context",
|
|
33919
|
+
completeness: getCompletenessState(db, id)
|
|
33920
|
+
};
|
|
33921
|
+
}
|
|
33922
|
+
async function stateAdvance(params) {
|
|
33923
|
+
const db = await ensureDatabase(params.ocrDir);
|
|
33924
|
+
const existing = getSession(db, params.sessionId);
|
|
33925
|
+
if (!existing) {
|
|
33926
|
+
throw new StateError(STATE_EXIT.NOT_FOUND, `Session not found: ${params.sessionId}`);
|
|
33927
|
+
}
|
|
33928
|
+
const phaseNumber = phaseNumberFor(existing.workflow_type, params.phase);
|
|
33929
|
+
await stateTransition(
|
|
33930
|
+
{
|
|
33931
|
+
sessionId: params.sessionId,
|
|
33932
|
+
phase: params.phase,
|
|
33933
|
+
phaseNumber,
|
|
33934
|
+
round: params.round,
|
|
33935
|
+
mapRun: params.mapRun,
|
|
33936
|
+
ocrDir: params.ocrDir
|
|
33937
|
+
},
|
|
33938
|
+
db
|
|
33939
|
+
);
|
|
33940
|
+
}
|
|
33941
|
+
async function stateCompleteRound(params) {
|
|
33942
|
+
const { ocrDir } = params;
|
|
33943
|
+
const db = await ensureDatabase(ocrDir);
|
|
33944
|
+
let meta;
|
|
33945
|
+
let counts;
|
|
33946
|
+
try {
|
|
33947
|
+
const rawJsonString = readJsonFromSource(params);
|
|
33948
|
+
const label = params.source === "file" ? params.filePath : "stdin";
|
|
33949
|
+
meta = validateRoundMeta(parseRawJson(rawJsonString, label));
|
|
33950
|
+
counts = computeRoundCounts(meta);
|
|
33951
|
+
} catch (e) {
|
|
33952
|
+
throw new StateError(
|
|
33953
|
+
STATE_EXIT.SCHEMA_INVALID,
|
|
33954
|
+
e instanceof Error ? e.message : "invalid round metadata"
|
|
33955
|
+
);
|
|
33956
|
+
}
|
|
33957
|
+
const resolved = resolveSession(db, params.sessionId);
|
|
33958
|
+
const roundNumber = params.round ?? resolved.current_round;
|
|
33959
|
+
const roundMetaPath = join16(
|
|
33960
|
+
resolved.session_dir,
|
|
33961
|
+
"rounds",
|
|
33962
|
+
`round-${roundNumber}`,
|
|
33963
|
+
"round-meta.json"
|
|
33964
|
+
);
|
|
33965
|
+
const already = db.exec(
|
|
33966
|
+
`SELECT 1 FROM orchestration_events
|
|
33967
|
+
WHERE session_id = ? AND event_type = 'round_completed' AND round = ? LIMIT 1`,
|
|
33968
|
+
[resolved.id, roundNumber]
|
|
33969
|
+
);
|
|
33970
|
+
if ((already[0]?.values.length ?? 0) > 0) {
|
|
33971
|
+
return { sessionId: resolved.id, round: roundNumber, metaPath: roundMetaPath, schema_version: 1 };
|
|
33972
|
+
}
|
|
33973
|
+
if (resolved.current_phase !== "synthesis") {
|
|
33974
|
+
throw new StateError(
|
|
33975
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
33976
|
+
`Cannot complete round: workflow is at "${resolved.current_phase}", not "synthesis". Advance through the phases first.`
|
|
33977
|
+
);
|
|
33978
|
+
}
|
|
33979
|
+
if (params.requireFinal) {
|
|
33980
|
+
const finalPath = join16(resolved.session_dir, "rounds", `round-${roundNumber}`, "final.md");
|
|
33981
|
+
if (!existsSync14(finalPath)) {
|
|
33982
|
+
throw new StateError(
|
|
33983
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
33984
|
+
`Cannot complete round: --require-final set but ${finalPath} is missing.`
|
|
33985
|
+
);
|
|
33986
|
+
}
|
|
33987
|
+
}
|
|
33988
|
+
let metaPath;
|
|
33989
|
+
if (params.source === "stdin") {
|
|
33990
|
+
const roundDir = join16(resolved.session_dir, "rounds", `round-${roundNumber}`);
|
|
33991
|
+
mkdirSync5(roundDir, { recursive: true });
|
|
33992
|
+
metaPath = roundMetaPath;
|
|
33993
|
+
writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
|
|
33994
|
+
}
|
|
33995
|
+
db.transaction(() => {
|
|
33996
|
+
insertEvent(db, {
|
|
33997
|
+
session_id: resolved.id,
|
|
33998
|
+
event_type: "round_completed",
|
|
33999
|
+
phase: "synthesis",
|
|
34000
|
+
phase_number: 7,
|
|
34001
|
+
round: roundNumber,
|
|
34002
|
+
metadata: JSON.stringify({
|
|
34003
|
+
verdict: meta.verdict,
|
|
34004
|
+
blocker_count: counts.blockerCount,
|
|
34005
|
+
should_fix_count: counts.shouldFixCount,
|
|
34006
|
+
suggestion_count: counts.suggestionCount,
|
|
34007
|
+
reviewer_count: counts.reviewerCount,
|
|
34008
|
+
total_finding_count: counts.totalFindingCount,
|
|
34009
|
+
source: "orchestrator"
|
|
34010
|
+
})
|
|
34011
|
+
});
|
|
34012
|
+
if (roundNumber >= resolved.current_round) {
|
|
34013
|
+
updateSession(db, resolved.id, { current_round: roundNumber });
|
|
34014
|
+
}
|
|
34015
|
+
validatePhaseTransition("review", resolved.current_phase, "complete", false);
|
|
34016
|
+
updateSession(db, resolved.id, { current_phase: "complete", phase_number: 8 });
|
|
34017
|
+
insertEvent(db, {
|
|
34018
|
+
session_id: resolved.id,
|
|
34019
|
+
event_type: "phase_transition",
|
|
34020
|
+
phase: "complete",
|
|
34021
|
+
phase_number: 8,
|
|
34022
|
+
round: roundNumber
|
|
34023
|
+
});
|
|
34024
|
+
});
|
|
34025
|
+
return { sessionId: resolved.id, round: roundNumber, metaPath, schema_version: 1 };
|
|
34026
|
+
}
|
|
34027
|
+
async function stateCompleteMap(params) {
|
|
34028
|
+
const { ocrDir } = params;
|
|
34029
|
+
const db = await ensureDatabase(ocrDir);
|
|
34030
|
+
let meta;
|
|
34031
|
+
let counts;
|
|
34032
|
+
try {
|
|
34033
|
+
const rawJsonString = readJsonFromSource(params);
|
|
34034
|
+
const label = params.source === "file" ? params.filePath : "stdin";
|
|
34035
|
+
meta = validateMapMeta(parseRawJson(rawJsonString, label));
|
|
34036
|
+
counts = computeMapCounts(meta);
|
|
34037
|
+
} catch (e) {
|
|
34038
|
+
throw new StateError(
|
|
34039
|
+
STATE_EXIT.SCHEMA_INVALID,
|
|
34040
|
+
e instanceof Error ? e.message : "invalid map metadata"
|
|
34041
|
+
);
|
|
34042
|
+
}
|
|
34043
|
+
const resolved = resolveSession(db, params.sessionId);
|
|
34044
|
+
const mapRunNumber = params.mapRun ?? resolved.current_map_run;
|
|
34045
|
+
const mapMetaPath = join16(
|
|
34046
|
+
resolved.session_dir,
|
|
34047
|
+
"map",
|
|
34048
|
+
"runs",
|
|
34049
|
+
`run-${mapRunNumber}`,
|
|
34050
|
+
"map-meta.json"
|
|
34051
|
+
);
|
|
34052
|
+
const already = db.exec(
|
|
34053
|
+
`SELECT 1 FROM orchestration_events
|
|
34054
|
+
WHERE session_id = ? AND event_type = 'map_completed' AND round = ? LIMIT 1`,
|
|
34055
|
+
[resolved.id, mapRunNumber]
|
|
34056
|
+
);
|
|
34057
|
+
if ((already[0]?.values.length ?? 0) > 0) {
|
|
34058
|
+
return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath: mapMetaPath, schema_version: 1 };
|
|
34059
|
+
}
|
|
34060
|
+
if (resolved.current_phase !== "synthesis") {
|
|
34061
|
+
throw new StateError(
|
|
34062
|
+
STATE_EXIT.INVARIANT_UNMET,
|
|
34063
|
+
`Cannot complete map: workflow is at "${resolved.current_phase}", not "synthesis". Advance first.`
|
|
34064
|
+
);
|
|
34065
|
+
}
|
|
34066
|
+
let metaPath;
|
|
34067
|
+
if (params.source === "stdin") {
|
|
34068
|
+
const runDir = join16(resolved.session_dir, "map", "runs", `run-${mapRunNumber}`);
|
|
34069
|
+
mkdirSync5(runDir, { recursive: true });
|
|
34070
|
+
metaPath = mapMetaPath;
|
|
34071
|
+
writeFileSync7(metaPath, JSON.stringify(meta, null, 2));
|
|
34072
|
+
}
|
|
34073
|
+
db.transaction(() => {
|
|
34074
|
+
insertEvent(db, {
|
|
34075
|
+
session_id: resolved.id,
|
|
34076
|
+
event_type: "map_completed",
|
|
34077
|
+
phase: "synthesis",
|
|
34078
|
+
phase_number: 5,
|
|
34079
|
+
round: mapRunNumber,
|
|
34080
|
+
metadata: JSON.stringify({
|
|
34081
|
+
section_count: counts.sectionCount,
|
|
34082
|
+
file_count: counts.fileCount,
|
|
34083
|
+
source: "orchestrator"
|
|
34084
|
+
})
|
|
34085
|
+
});
|
|
34086
|
+
validatePhaseTransition("map", resolved.current_phase, "complete", false);
|
|
34087
|
+
updateSession(db, resolved.id, { current_phase: "complete", phase_number: 6 });
|
|
34088
|
+
insertEvent(db, {
|
|
34089
|
+
session_id: resolved.id,
|
|
34090
|
+
event_type: "phase_transition",
|
|
34091
|
+
phase: "complete",
|
|
34092
|
+
phase_number: 6,
|
|
34093
|
+
round: mapRunNumber
|
|
34094
|
+
});
|
|
34095
|
+
});
|
|
34096
|
+
return { sessionId: resolved.id, mapRun: mapRunNumber, metaPath, schema_version: 1 };
|
|
34097
|
+
}
|
|
34098
|
+
async function stateStatus(ocrDir, sessionId) {
|
|
34099
|
+
const db = await ensureDatabase(ocrDir);
|
|
34100
|
+
const resolved = resolveSession(db, sessionId);
|
|
34101
|
+
const view = db.exec(
|
|
34102
|
+
`SELECT completeness_state, has_terminal_artifact, marked_closed, dependents_settled
|
|
34103
|
+
FROM session_completeness WHERE session_id = ?`,
|
|
34104
|
+
[resolved.id]
|
|
34105
|
+
);
|
|
34106
|
+
const row = view[0]?.values[0];
|
|
34107
|
+
const completenessState = row?.[0] ?? null;
|
|
34108
|
+
const hasTerminalArtifact = row?.[1] === 1;
|
|
34109
|
+
let nextAction;
|
|
34110
|
+
let nextActionKind;
|
|
34111
|
+
switch (completenessState) {
|
|
34112
|
+
case "complete":
|
|
34113
|
+
nextAction = "none \u2014 session is complete";
|
|
34114
|
+
nextActionKind = "none";
|
|
34115
|
+
break;
|
|
34116
|
+
case "closed_without_artifact":
|
|
34117
|
+
nextAction = "re-open and finalize: this session was closed without a completed round/run";
|
|
34118
|
+
nextActionKind = "reopen";
|
|
34119
|
+
break;
|
|
34120
|
+
case "in_flight":
|
|
34121
|
+
nextAction = "wait for in-flight agent processes to finish";
|
|
34122
|
+
nextActionKind = "wait";
|
|
34123
|
+
break;
|
|
34124
|
+
default:
|
|
34125
|
+
if (hasTerminalArtifact) {
|
|
34126
|
+
nextAction = "run 'ocr state finish' to close the workflow";
|
|
34127
|
+
nextActionKind = "finish";
|
|
34128
|
+
} else if (resolved.current_phase === "synthesis") {
|
|
34129
|
+
nextAction = "pipe round metadata to 'ocr state complete-round --stdin'";
|
|
34130
|
+
nextActionKind = "complete_round";
|
|
34131
|
+
} else {
|
|
34132
|
+
nextAction = "advance through the phases, then 'ocr state complete-round'";
|
|
34133
|
+
nextActionKind = "advance";
|
|
34134
|
+
}
|
|
34135
|
+
}
|
|
34136
|
+
return {
|
|
34137
|
+
schema_version: 1,
|
|
34138
|
+
session_id: resolved.id,
|
|
34139
|
+
workflow_type: resolved.workflow_type,
|
|
34140
|
+
status: resolved.status,
|
|
34141
|
+
current_phase: resolved.current_phase,
|
|
34142
|
+
current_round: resolved.current_round,
|
|
34143
|
+
current_map_run: resolved.current_map_run,
|
|
34144
|
+
completeness_state: completenessState,
|
|
34145
|
+
has_terminal_artifact: hasTerminalArtifact,
|
|
34146
|
+
marked_closed: row?.[2] === 1,
|
|
34147
|
+
dependents_settled: row?.[3] === 1,
|
|
34148
|
+
next_action: nextAction,
|
|
34149
|
+
next_action_kind: nextActionKind
|
|
34150
|
+
};
|
|
34151
|
+
}
|
|
33172
34152
|
async function stateSync(ocrDir) {
|
|
33173
34153
|
const db = await ensureDatabase(ocrDir);
|
|
33174
|
-
const
|
|
33175
|
-
|
|
33176
|
-
if (!existsSync13(sessionsRoot)) {
|
|
34154
|
+
const sessionsRoot = join16(ocrDir, "sessions");
|
|
34155
|
+
if (!existsSync14(sessionsRoot)) {
|
|
33177
34156
|
return 0;
|
|
33178
34157
|
}
|
|
33179
34158
|
const entries = readdirSync6(sessionsRoot).filter((name) => {
|
|
33180
|
-
const fullPath =
|
|
33181
|
-
return
|
|
34159
|
+
const fullPath = join16(sessionsRoot, name);
|
|
34160
|
+
return statSync3(fullPath).isDirectory();
|
|
33182
34161
|
});
|
|
33183
34162
|
let synced = 0;
|
|
33184
34163
|
for (const dirName of entries) {
|
|
33185
|
-
const dirPath =
|
|
34164
|
+
const dirPath = join16(sessionsRoot, dirName);
|
|
33186
34165
|
const existing = getSession(db, dirName);
|
|
33187
34166
|
if (existing) {
|
|
33188
34167
|
continue;
|
|
@@ -33190,28 +34169,41 @@ async function stateSync(ocrDir) {
|
|
|
33190
34169
|
if (!hasArtifacts(dirPath)) {
|
|
33191
34170
|
continue;
|
|
33192
34171
|
}
|
|
33193
|
-
const hasRoundsDir =
|
|
33194
|
-
const hasMapDir =
|
|
34172
|
+
const hasRoundsDir = existsSync14(join16(dirPath, "rounds"));
|
|
34173
|
+
const hasMapDir = existsSync14(join16(dirPath, "map"));
|
|
33195
34174
|
const workflowType = hasMapDir && !hasRoundsDir ? "map" : "review";
|
|
33196
34175
|
const branchMatch = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
33197
34176
|
const branch = branchMatch?.[1] ?? dirName;
|
|
33198
34177
|
let inferredPhase = "context";
|
|
34178
|
+
let inferredPhaseNumber = 1;
|
|
34179
|
+
let inferredRound = 1;
|
|
34180
|
+
let inferredMapRun = 1;
|
|
33199
34181
|
if (workflowType === "review") {
|
|
33200
|
-
const roundsDir =
|
|
33201
|
-
if (
|
|
33202
|
-
const roundDirs = readdirSync6(roundsDir).filter((d) => /^round-\d+$/.test(d)).
|
|
33203
|
-
const
|
|
33204
|
-
if (
|
|
33205
|
-
|
|
34182
|
+
const roundsDir = join16(dirPath, "rounds");
|
|
34183
|
+
if (existsSync14(roundsDir)) {
|
|
34184
|
+
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);
|
|
34185
|
+
const latestRoundNum = roundDirs[roundDirs.length - 1];
|
|
34186
|
+
if (latestRoundNum !== void 0) {
|
|
34187
|
+
inferredRound = latestRoundNum;
|
|
34188
|
+
if (existsSync14(
|
|
34189
|
+
join16(roundsDir, `round-${latestRoundNum}`, "final.md")
|
|
34190
|
+
)) {
|
|
34191
|
+
inferredPhase = "complete";
|
|
34192
|
+
inferredPhaseNumber = 8;
|
|
34193
|
+
}
|
|
33206
34194
|
}
|
|
33207
34195
|
}
|
|
33208
34196
|
} 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
|
-
|
|
34197
|
+
const runsDir = join16(dirPath, "map", "runs");
|
|
34198
|
+
if (existsSync14(runsDir)) {
|
|
34199
|
+
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);
|
|
34200
|
+
const latestRunNum = runDirs[runDirs.length - 1];
|
|
34201
|
+
if (latestRunNum !== void 0) {
|
|
34202
|
+
inferredMapRun = latestRunNum;
|
|
34203
|
+
if (existsSync14(join16(runsDir, `run-${latestRunNum}`, "map.md"))) {
|
|
34204
|
+
inferredPhase = "complete";
|
|
34205
|
+
inferredPhaseNumber = 6;
|
|
34206
|
+
}
|
|
33215
34207
|
}
|
|
33216
34208
|
}
|
|
33217
34209
|
}
|
|
@@ -33220,35 +34212,36 @@ async function stateSync(ocrDir) {
|
|
|
33220
34212
|
branch,
|
|
33221
34213
|
workflow_type: workflowType,
|
|
33222
34214
|
current_phase: inferredPhase,
|
|
33223
|
-
phase_number:
|
|
33224
|
-
current_round:
|
|
33225
|
-
current_map_run:
|
|
34215
|
+
phase_number: inferredPhaseNumber,
|
|
34216
|
+
current_round: inferredRound,
|
|
34217
|
+
current_map_run: inferredMapRun,
|
|
33226
34218
|
session_dir: dirPath
|
|
33227
34219
|
});
|
|
33228
|
-
|
|
33229
|
-
|
|
33230
|
-
|
|
33231
|
-
|
|
33232
|
-
|
|
33233
|
-
|
|
33234
|
-
|
|
33235
|
-
|
|
34220
|
+
commitReasonClose(
|
|
34221
|
+
db,
|
|
34222
|
+
dirName,
|
|
34223
|
+
{
|
|
34224
|
+
event_type: "session_synced",
|
|
34225
|
+
phase: inferredPhase,
|
|
34226
|
+
phase_number: 1,
|
|
34227
|
+
metadata: JSON.stringify({ source: "filesystem_backfill" })
|
|
34228
|
+
},
|
|
34229
|
+
{ status: "closed" }
|
|
34230
|
+
);
|
|
33236
34231
|
synced++;
|
|
33237
34232
|
}
|
|
33238
|
-
if (synced > 0) {
|
|
33239
|
-
saveDatabase(db, dbPath);
|
|
33240
|
-
}
|
|
33241
34233
|
return synced;
|
|
33242
34234
|
}
|
|
33243
34235
|
|
|
33244
34236
|
// src/commands/state.ts
|
|
33245
34237
|
init_command_log();
|
|
33246
34238
|
init_db();
|
|
34239
|
+
init_db();
|
|
33247
34240
|
function readDashboardSpawnMarker(ocrDir) {
|
|
33248
|
-
const path2 =
|
|
34241
|
+
const path2 = join17(ocrDir, "data", "dashboard-active-spawn.json");
|
|
33249
34242
|
let raw;
|
|
33250
34243
|
try {
|
|
33251
|
-
raw =
|
|
34244
|
+
raw = readFileSync11(path2, "utf-8");
|
|
33252
34245
|
} catch {
|
|
33253
34246
|
return null;
|
|
33254
34247
|
}
|
|
@@ -33280,143 +34273,37 @@ async function readStdin() {
|
|
|
33280
34273
|
}
|
|
33281
34274
|
return data;
|
|
33282
34275
|
}
|
|
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
|
-
}
|
|
34276
|
+
async function linkDashboardInvocation(ocrDir, sessionId, explicitUid, label) {
|
|
34277
|
+
const markerUid = readDashboardSpawnMarker(ocrDir)?.execution_uid;
|
|
34278
|
+
const dashboardUid = explicitUid ?? process.env["OCR_DASHBOARD_EXECUTION_UID"] ?? markerUid;
|
|
34279
|
+
if (!dashboardUid) {
|
|
34280
|
+
console.error(
|
|
34281
|
+
source_default.gray(
|
|
34282
|
+
`[state ${label}] no dashboard linkage available (flag, env var, and marker file all absent \u2014 CLI-only invocation)`
|
|
34283
|
+
)
|
|
34284
|
+
);
|
|
34285
|
+
return;
|
|
33394
34286
|
}
|
|
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
34287
|
try {
|
|
33401
|
-
const
|
|
33402
|
-
|
|
33403
|
-
sessionId,
|
|
33404
|
-
ocrDir
|
|
33405
|
-
});
|
|
33406
|
-
console.log(`${sessionId}: closed`);
|
|
33407
|
-
} catch (error) {
|
|
34288
|
+
const db = await getDb(ocrDir);
|
|
34289
|
+
linkDashboardInvocationToWorkflow(db, dashboardUid, sessionId);
|
|
33408
34290
|
console.error(
|
|
33409
|
-
source_default.
|
|
33410
|
-
`
|
|
34291
|
+
source_default.gray(
|
|
34292
|
+
`[state ${label}] linked workflow_id=${sessionId} \u2192 dashboard uid=${dashboardUid}`
|
|
34293
|
+
)
|
|
34294
|
+
);
|
|
34295
|
+
} catch (linkErr) {
|
|
34296
|
+
console.error(
|
|
34297
|
+
source_default.yellow(
|
|
34298
|
+
`Warning: failed to link dashboard command_execution to session: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`
|
|
33411
34299
|
)
|
|
33412
34300
|
);
|
|
33413
|
-
process.exit(1);
|
|
33414
34301
|
}
|
|
33415
|
-
}
|
|
34302
|
+
}
|
|
33416
34303
|
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
34304
|
const targetDir = process.cwd();
|
|
33418
34305
|
requireOcrSetup(targetDir);
|
|
33419
|
-
const ocrDir =
|
|
34306
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
33420
34307
|
try {
|
|
33421
34308
|
const result = await stateShow(ocrDir, options.sessionId);
|
|
33422
34309
|
if (!result) {
|
|
@@ -33485,7 +34372,7 @@ var showSubcommand = new Command("show").description("Show current session state
|
|
|
33485
34372
|
var syncSubcommand = new Command("sync").description("Rebuild session state from filesystem artifacts").action(async () => {
|
|
33486
34373
|
const targetDir = process.cwd();
|
|
33487
34374
|
requireOcrSetup(targetDir);
|
|
33488
|
-
const ocrDir =
|
|
34375
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
33489
34376
|
try {
|
|
33490
34377
|
const synced = await stateSync(ocrDir);
|
|
33491
34378
|
console.log(`Synced ${synced} session${synced !== 1 ? "s" : ""} from filesystem.`);
|
|
@@ -33495,7 +34382,6 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
|
|
|
33495
34382
|
if (totalCmds === 0) {
|
|
33496
34383
|
const recovered = replayCommandLog(db, ocrDir);
|
|
33497
34384
|
if (recovered > 0) {
|
|
33498
|
-
saveDatabase(db, join16(ocrDir, "data", "ocr.db"));
|
|
33499
34385
|
console.log(`Recovered ${recovered} command${recovered !== 1 ? "s" : ""} from backup log.`);
|
|
33500
34386
|
}
|
|
33501
34387
|
}
|
|
@@ -33508,123 +34394,216 @@ var syncSubcommand = new Command("sync").description("Rebuild session state from
|
|
|
33508
34394
|
process.exit(1);
|
|
33509
34395
|
}
|
|
33510
34396
|
});
|
|
33511
|
-
var
|
|
34397
|
+
var reconcileSubcommand = new Command("reconcile").description(
|
|
34398
|
+
"Heal legacy/drifted session state by deriving truth from events + artifacts"
|
|
34399
|
+
).option("--dry-run", "Print the repair plan without writing anything").option("--json", "Output the result as JSON").action(async (options) => {
|
|
34400
|
+
const targetDir = process.cwd();
|
|
34401
|
+
requireOcrSetup(targetDir);
|
|
34402
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34403
|
+
try {
|
|
34404
|
+
const db = await ensureDatabase(ocrDir);
|
|
34405
|
+
const result = reconcileLegacyState(db, ocrDir, { dryRun: options.dryRun });
|
|
34406
|
+
if (options.json) {
|
|
34407
|
+
console.log(JSON.stringify(result, null, 2));
|
|
34408
|
+
return;
|
|
34409
|
+
}
|
|
34410
|
+
const repairs = result.actions.filter((a) => a.kind !== "ok");
|
|
34411
|
+
if (repairs.length === 0) {
|
|
34412
|
+
console.log(source_default.dim("Nothing to reconcile \u2014 all sessions consistent."));
|
|
34413
|
+
return;
|
|
34414
|
+
}
|
|
34415
|
+
console.log(
|
|
34416
|
+
result.dryRun ? source_default.bold(`Reconciliation plan (${repairs.length} change(s), dry run):`) : source_default.bold(`Reconciled ${repairs.length} session(s):`)
|
|
34417
|
+
);
|
|
34418
|
+
for (const a of repairs) {
|
|
34419
|
+
console.log(` ${source_default.cyan(a.kind)} ${a.sessionId}`);
|
|
34420
|
+
console.log(` ${source_default.dim(a.detail)}`);
|
|
34421
|
+
}
|
|
34422
|
+
} catch (error) {
|
|
34423
|
+
console.error(
|
|
34424
|
+
source_default.red(
|
|
34425
|
+
`Error: ${error instanceof Error ? error.message : "Failed to reconcile"}`
|
|
34426
|
+
)
|
|
34427
|
+
);
|
|
34428
|
+
process.exit(1);
|
|
34429
|
+
}
|
|
34430
|
+
});
|
|
34431
|
+
function exitFromStateError(error, fallback2) {
|
|
34432
|
+
if (error instanceof StateError) {
|
|
34433
|
+
console.error(source_default.red(`Error: ${error.message}`));
|
|
34434
|
+
process.exit(error.code);
|
|
34435
|
+
}
|
|
34436
|
+
if (isBusyError(error)) {
|
|
34437
|
+
console.error(
|
|
34438
|
+
source_default.red(
|
|
34439
|
+
`Error: database is busy (locked past retry budget): ${error instanceof Error ? error.message : String(error)}`
|
|
34440
|
+
)
|
|
34441
|
+
);
|
|
34442
|
+
process.exit(STATE_EXIT.BUSY);
|
|
34443
|
+
}
|
|
34444
|
+
console.error(
|
|
34445
|
+
source_default.red(`Error: ${error instanceof Error ? error.message : fallback2}`)
|
|
34446
|
+
);
|
|
34447
|
+
process.exit(1);
|
|
34448
|
+
}
|
|
34449
|
+
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) => {
|
|
34450
|
+
if (v !== "review" && v !== "map") {
|
|
34451
|
+
throw new Error(`Invalid workflow type: "${v}". Must be "review" or "map".`);
|
|
34452
|
+
}
|
|
34453
|
+
return v;
|
|
34454
|
+
}).option("--session-dir <dir>", "Session directory path (auto-resolved if omitted)").option(
|
|
34455
|
+
"--dashboard-uid <uid>",
|
|
34456
|
+
"Dashboard command_executions uid to link this workflow to (takes precedence over OCR_DASHBOARD_EXECUTION_UID)"
|
|
34457
|
+
).option("--json", "Output the result as JSON").action(
|
|
33512
34458
|
async (options) => {
|
|
33513
34459
|
const targetDir = process.cwd();
|
|
33514
34460
|
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
|
-
}
|
|
34461
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34462
|
+
const sessionDir = options.sessionDir ?? join17(ocrDir, "sessions", options.sessionId);
|
|
34463
|
+
if (!existsSync15(sessionDir)) mkdirSync6(sessionDir, { recursive: true });
|
|
33524
34464
|
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
|
-
)
|
|
34465
|
+
const result = await stateBegin({
|
|
34466
|
+
sessionId: options.sessionId,
|
|
34467
|
+
branch: options.branch,
|
|
34468
|
+
workflowType: options.workflowType,
|
|
34469
|
+
sessionDir,
|
|
34470
|
+
ocrDir
|
|
34471
|
+
});
|
|
34472
|
+
await linkDashboardInvocation(ocrDir, result.session_id, options.dashboardUid, "begin");
|
|
34473
|
+
console.log(
|
|
34474
|
+
options.json ? JSON.stringify(result, null, 2) : `${result.session_id}: round ${result.round}, phase ${result.phase} (${result.completeness ?? "unknown"})`
|
|
33555
34475
|
);
|
|
33556
|
-
|
|
34476
|
+
} catch (error) {
|
|
34477
|
+
exitFromStateError(error, "Failed to begin session");
|
|
33557
34478
|
}
|
|
33558
34479
|
}
|
|
33559
34480
|
);
|
|
33560
|
-
var
|
|
34481
|
+
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
34482
|
async (options) => {
|
|
33562
34483
|
const targetDir = process.cwd();
|
|
33563
34484
|
requireOcrSetup(targetDir);
|
|
33564
|
-
const ocrDir =
|
|
33565
|
-
|
|
33566
|
-
|
|
33567
|
-
|
|
33568
|
-
|
|
33569
|
-
|
|
33570
|
-
|
|
33571
|
-
|
|
34485
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34486
|
+
try {
|
|
34487
|
+
const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
|
|
34488
|
+
await stateAdvance({
|
|
34489
|
+
sessionId,
|
|
34490
|
+
phase: options.phase,
|
|
34491
|
+
round: options.currentRound,
|
|
34492
|
+
mapRun: options.currentMapRun,
|
|
34493
|
+
ocrDir
|
|
34494
|
+
});
|
|
34495
|
+
console.log(`${sessionId}: ${options.phase}`);
|
|
34496
|
+
} catch (error) {
|
|
34497
|
+
exitFromStateError(error, "Failed to advance");
|
|
33572
34498
|
}
|
|
34499
|
+
}
|
|
34500
|
+
);
|
|
34501
|
+
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(
|
|
34502
|
+
async (options) => {
|
|
34503
|
+
const targetDir = process.cwd();
|
|
34504
|
+
requireOcrSetup(targetDir);
|
|
34505
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
33573
34506
|
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
|
-
}
|
|
34507
|
+
const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
|
|
34508
|
+
throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with round metadata");
|
|
34509
|
+
})();
|
|
34510
|
+
const result = await stateCompleteRound({
|
|
34511
|
+
...base,
|
|
34512
|
+
ocrDir,
|
|
34513
|
+
sessionId: options.sessionId,
|
|
34514
|
+
round: options.round,
|
|
34515
|
+
requireFinal: options.requireFinal
|
|
34516
|
+
});
|
|
34517
|
+
console.log(
|
|
34518
|
+
options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: round ${result.round} complete`
|
|
34519
|
+
);
|
|
33599
34520
|
} catch (error) {
|
|
33600
|
-
|
|
33601
|
-
|
|
33602
|
-
|
|
33603
|
-
|
|
34521
|
+
exitFromStateError(error, "Failed to complete round");
|
|
34522
|
+
}
|
|
34523
|
+
}
|
|
34524
|
+
);
|
|
34525
|
+
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(
|
|
34526
|
+
async (options) => {
|
|
34527
|
+
const targetDir = process.cwd();
|
|
34528
|
+
requireOcrSetup(targetDir);
|
|
34529
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34530
|
+
try {
|
|
34531
|
+
const base = options.stdin ? { source: "stdin", data: await readStdin() } : options.file ? { source: "file", filePath: options.file } : (() => {
|
|
34532
|
+
throw new StateError(STATE_EXIT.USAGE, "Provide --stdin or --file with map metadata");
|
|
34533
|
+
})();
|
|
34534
|
+
const result = await stateCompleteMap({
|
|
34535
|
+
...base,
|
|
34536
|
+
ocrDir,
|
|
34537
|
+
sessionId: options.sessionId,
|
|
34538
|
+
mapRun: options.mapRun
|
|
34539
|
+
});
|
|
34540
|
+
console.log(
|
|
34541
|
+
options.json ? JSON.stringify(result, null, 2) : `${result.sessionId}: map run ${result.mapRun} complete`
|
|
33604
34542
|
);
|
|
33605
|
-
|
|
34543
|
+
} catch (error) {
|
|
34544
|
+
exitFromStateError(error, "Failed to complete map");
|
|
33606
34545
|
}
|
|
33607
34546
|
}
|
|
33608
34547
|
);
|
|
33609
|
-
var
|
|
34548
|
+
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) => {
|
|
34549
|
+
const targetDir = process.cwd();
|
|
34550
|
+
requireOcrSetup(targetDir);
|
|
34551
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34552
|
+
try {
|
|
34553
|
+
const { id: sessionId } = await resolveActiveSession(ocrDir, options.sessionId);
|
|
34554
|
+
await stateClose({ sessionId, ocrDir, abort: options.abort });
|
|
34555
|
+
console.log(`${sessionId}: ${options.abort ? "aborted" : "finished"}`);
|
|
34556
|
+
} catch (error) {
|
|
34557
|
+
exitFromStateError(error, "Failed to finish");
|
|
34558
|
+
}
|
|
34559
|
+
});
|
|
34560
|
+
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) => {
|
|
34561
|
+
const targetDir = process.cwd();
|
|
34562
|
+
requireOcrSetup(targetDir);
|
|
34563
|
+
const ocrDir = join17(targetDir, ".ocr");
|
|
34564
|
+
try {
|
|
34565
|
+
const result = await stateStatus(ocrDir, options.sessionId);
|
|
34566
|
+
if (options.json) {
|
|
34567
|
+
console.log(JSON.stringify(result, null, 2));
|
|
34568
|
+
} else {
|
|
34569
|
+
console.log(`${result.session_id}: ${result.completeness_state}`);
|
|
34570
|
+
console.log(source_default.dim(` next: ${result.next_action}`));
|
|
34571
|
+
}
|
|
34572
|
+
} catch (error) {
|
|
34573
|
+
exitFromStateError(error, "Failed to read status");
|
|
34574
|
+
}
|
|
34575
|
+
});
|
|
34576
|
+
var RETIRED_STATE_VERBS = {
|
|
34577
|
+
init: "begin",
|
|
34578
|
+
transition: "advance",
|
|
34579
|
+
"round-complete": "complete-round",
|
|
34580
|
+
"map-complete": "complete-map",
|
|
34581
|
+
close: "finish"
|
|
34582
|
+
};
|
|
34583
|
+
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) => {
|
|
34584
|
+
const verb = operands[0] ?? "";
|
|
34585
|
+
const replacement = RETIRED_STATE_VERBS[verb];
|
|
34586
|
+
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'.`;
|
|
34587
|
+
exitFromStateError(new StateError(STATE_EXIT.USAGE, msg), msg);
|
|
34588
|
+
});
|
|
33610
34589
|
|
|
33611
34590
|
// src/commands/session.ts
|
|
33612
34591
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
33613
|
-
import { join as
|
|
34592
|
+
import { join as join19 } from "node:path";
|
|
33614
34593
|
init_db();
|
|
33615
34594
|
|
|
33616
34595
|
// src/lib/runtime-config.ts
|
|
33617
|
-
import { existsSync as
|
|
33618
|
-
import { join as
|
|
34596
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
34597
|
+
import { join as join18 } from "node:path";
|
|
33619
34598
|
var DEFAULT_AGENT_HEARTBEAT_SECONDS = 60;
|
|
33620
34599
|
function getAgentHeartbeatSeconds(ocrDir) {
|
|
33621
|
-
const configPath =
|
|
33622
|
-
if (!
|
|
34600
|
+
const configPath = join18(ocrDir, "config.yaml");
|
|
34601
|
+
if (!existsSync16(configPath)) {
|
|
33623
34602
|
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
33624
34603
|
}
|
|
33625
34604
|
let content;
|
|
33626
34605
|
try {
|
|
33627
|
-
content =
|
|
34606
|
+
content = readFileSync12(configPath, "utf-8");
|
|
33628
34607
|
} catch {
|
|
33629
34608
|
return DEFAULT_AGENT_HEARTBEAT_SECONDS;
|
|
33630
34609
|
}
|
|
@@ -33663,16 +34642,18 @@ function fail(message) {
|
|
|
33663
34642
|
async function setup() {
|
|
33664
34643
|
const targetDir = process.cwd();
|
|
33665
34644
|
requireOcrSetup(targetDir);
|
|
33666
|
-
const ocrDir =
|
|
33667
|
-
|
|
33668
|
-
return { ocrDir, dbPath };
|
|
34645
|
+
const ocrDir = join19(targetDir, ".ocr");
|
|
34646
|
+
return { ocrDir };
|
|
33669
34647
|
}
|
|
33670
34648
|
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
34649
|
async (options) => {
|
|
33672
|
-
const { ocrDir
|
|
34650
|
+
const { ocrDir } = await setup();
|
|
33673
34651
|
const db = await ensureDatabase(ocrDir);
|
|
33674
34652
|
try {
|
|
33675
|
-
const workflowId =
|
|
34653
|
+
const { id: workflowId } = await resolveActiveSession(
|
|
34654
|
+
ocrDir,
|
|
34655
|
+
options.workflow
|
|
34656
|
+
);
|
|
33676
34657
|
const id = randomUUID3();
|
|
33677
34658
|
const persona = options.persona ?? null;
|
|
33678
34659
|
const instanceIndex = options.instance ?? null;
|
|
@@ -33691,7 +34672,6 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
|
|
|
33691
34672
|
pid: options.pid ?? null,
|
|
33692
34673
|
notes: options.note ?? null
|
|
33693
34674
|
});
|
|
33694
|
-
saveDatabase(db, dbPath);
|
|
33695
34675
|
console.log(id);
|
|
33696
34676
|
} catch (error) {
|
|
33697
34677
|
fail(error instanceof Error ? error.message : "Failed to start agent session");
|
|
@@ -33699,18 +34679,17 @@ var startInstanceSubcommand = new Command("start-instance").description("Journal
|
|
|
33699
34679
|
}
|
|
33700
34680
|
);
|
|
33701
34681
|
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
|
|
34682
|
+
const { ocrDir } = await setup();
|
|
33703
34683
|
const db = await ensureDatabase(ocrDir);
|
|
33704
34684
|
try {
|
|
33705
34685
|
setAgentSessionVendorId(db, agentId, vendorId);
|
|
33706
|
-
saveDatabase(db, dbPath);
|
|
33707
34686
|
console.log(`${agentId}: vendor_session_id=${vendorId}`);
|
|
33708
34687
|
} catch (error) {
|
|
33709
34688
|
fail(error instanceof Error ? error.message : "Failed to bind vendor session id");
|
|
33710
34689
|
}
|
|
33711
34690
|
});
|
|
33712
34691
|
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
|
|
34692
|
+
const { ocrDir } = await setup();
|
|
33714
34693
|
const db = await ensureDatabase(ocrDir);
|
|
33715
34694
|
try {
|
|
33716
34695
|
const existing = getAgentSession(db, agentId);
|
|
@@ -33718,7 +34697,6 @@ var beatSubcommand = new Command("beat").description("Bump last_heartbeat_at on
|
|
|
33718
34697
|
fail(`Agent session not found: ${agentId}`);
|
|
33719
34698
|
}
|
|
33720
34699
|
bumpAgentSessionHeartbeat(db, agentId);
|
|
33721
|
-
saveDatabase(db, dbPath);
|
|
33722
34700
|
console.log(`${agentId}: heartbeat`);
|
|
33723
34701
|
} catch (error) {
|
|
33724
34702
|
fail(error instanceof Error ? error.message : "Failed to bump heartbeat");
|
|
@@ -33729,7 +34707,7 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
|
|
|
33729
34707
|
"Terminal status (done | crashed | cancelled). Default inferred from --exit-code (0 \u2192 done, non-zero \u2192 crashed)"
|
|
33730
34708
|
).option("--exit-code <code>", "Process exit code", parseInt).option("--note <text>", "Free-form note to append").action(
|
|
33731
34709
|
async (agentId, options) => {
|
|
33732
|
-
const { ocrDir
|
|
34710
|
+
const { ocrDir } = await setup();
|
|
33733
34711
|
const db = await ensureDatabase(ocrDir);
|
|
33734
34712
|
try {
|
|
33735
34713
|
const existing = getAgentSession(db, agentId);
|
|
@@ -33760,7 +34738,6 @@ var endInstanceSubcommand = new Command("end-instance").description("Transition
|
|
|
33760
34738
|
exitCode: options.exitCode ?? null,
|
|
33761
34739
|
note: options.note
|
|
33762
34740
|
});
|
|
33763
|
-
saveDatabase(db, dbPath);
|
|
33764
34741
|
console.log(`${agentId}: ${status}`);
|
|
33765
34742
|
} catch (error) {
|
|
33766
34743
|
fail(error instanceof Error ? error.message : "Failed to end agent session");
|
|
@@ -33771,7 +34748,10 @@ var listSubcommand = new Command("list").description("List agent sessions for a
|
|
|
33771
34748
|
const { ocrDir } = await setup();
|
|
33772
34749
|
const db = await ensureDatabase(ocrDir);
|
|
33773
34750
|
try {
|
|
33774
|
-
const workflowId =
|
|
34751
|
+
const { id: workflowId } = await resolveActiveSession(
|
|
34752
|
+
ocrDir,
|
|
34753
|
+
options.workflow
|
|
34754
|
+
);
|
|
33775
34755
|
const rows = listAgentSessionsForWorkflow(db, workflowId);
|
|
33776
34756
|
if (options.json) {
|
|
33777
34757
|
console.log(JSON.stringify(rows, null, 2));
|
|
@@ -33917,8 +34897,8 @@ var modelsCommand = new Command("models").description("Inspect models available
|
|
|
33917
34897
|
|
|
33918
34898
|
// src/commands/team.ts
|
|
33919
34899
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
33920
|
-
import { existsSync as
|
|
33921
|
-
import { join as
|
|
34900
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
|
|
34901
|
+
import { join as join20 } from "node:path";
|
|
33922
34902
|
async function readStdin2() {
|
|
33923
34903
|
const chunks = [];
|
|
33924
34904
|
for await (const chunk of process.stdin) {
|
|
@@ -33977,7 +34957,7 @@ var resolveSubcommand = new Command("resolve").description("Resolve and print th
|
|
|
33977
34957
|
async (options) => {
|
|
33978
34958
|
const targetDir = process.cwd();
|
|
33979
34959
|
requireOcrSetup(targetDir);
|
|
33980
|
-
const ocrDir =
|
|
34960
|
+
const ocrDir = join20(targetDir, ".ocr");
|
|
33981
34961
|
try {
|
|
33982
34962
|
const { team } = loadTeamConfig(ocrDir);
|
|
33983
34963
|
let override;
|
|
@@ -34016,8 +34996,8 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
|
|
|
34016
34996
|
}
|
|
34017
34997
|
const targetDir = process.cwd();
|
|
34018
34998
|
requireOcrSetup(targetDir);
|
|
34019
|
-
const ocrDir =
|
|
34020
|
-
const configPath =
|
|
34999
|
+
const ocrDir = join20(targetDir, ".ocr");
|
|
35000
|
+
const configPath = join20(ocrDir, "config.yaml");
|
|
34021
35001
|
try {
|
|
34022
35002
|
const raw = await readStdin2();
|
|
34023
35003
|
const team = parseSessionOverride(raw);
|
|
@@ -34027,17 +35007,17 @@ var setSubcommand = new Command("set").description("Persist a new default_team c
|
|
|
34027
35007
|
list.push(inst);
|
|
34028
35008
|
byPersona.set(inst.persona, list);
|
|
34029
35009
|
}
|
|
34030
|
-
const doc =
|
|
35010
|
+
const doc = existsSync17(configPath) ? (0, import_yaml2.parseDocument)(readFileSync13(configPath, "utf-8")) : new import_yaml2.Document({});
|
|
34031
35011
|
applyDefaultTeamSurgically(doc, byPersona);
|
|
34032
35012
|
const yamlOutput = doc.toString({ lineWidth: 0 });
|
|
34033
|
-
|
|
34034
|
-
const reviewersDir =
|
|
34035
|
-
const metaPath =
|
|
35013
|
+
writeFileSync8(configPath, yamlOutput, "utf-8");
|
|
35014
|
+
const reviewersDir = join20(ocrDir, "skills", "references", "reviewers");
|
|
35015
|
+
const metaPath = join20(ocrDir, "reviewers-meta.json");
|
|
34036
35016
|
let metaWritten = false;
|
|
34037
35017
|
try {
|
|
34038
35018
|
const meta = generateReviewersMeta(reviewersDir, configPath);
|
|
34039
35019
|
if (meta) {
|
|
34040
|
-
|
|
35020
|
+
writeFileSync8(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
34041
35021
|
metaWritten = true;
|
|
34042
35022
|
}
|
|
34043
35023
|
} catch (err) {
|
|
@@ -34128,7 +35108,7 @@ var teamCommand = new Command("team").description("Resolve and persist team comp
|
|
|
34128
35108
|
|
|
34129
35109
|
// src/commands/review.ts
|
|
34130
35110
|
import { spawn as spawn3 } from "node:child_process";
|
|
34131
|
-
import { join as
|
|
35111
|
+
import { join as join21 } from "node:path";
|
|
34132
35112
|
init_db();
|
|
34133
35113
|
|
|
34134
35114
|
// src/lib/vendor-resume.ts
|
|
@@ -34167,7 +35147,7 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
|
|
|
34167
35147
|
}
|
|
34168
35148
|
const targetDir = process.cwd();
|
|
34169
35149
|
requireOcrSetup(targetDir);
|
|
34170
|
-
const ocrDir =
|
|
35150
|
+
const ocrDir = join21(targetDir, ".ocr");
|
|
34171
35151
|
const db = await ensureDatabase(ocrDir);
|
|
34172
35152
|
const session = getSession(db, options.resume);
|
|
34173
35153
|
if (!session) {
|
|
@@ -34209,16 +35189,16 @@ var reviewCommand = new Command("review").description("Run or resume an OCR revi
|
|
|
34209
35189
|
});
|
|
34210
35190
|
|
|
34211
35191
|
// src/commands/update.ts
|
|
34212
|
-
import { existsSync as
|
|
34213
|
-
import { join as
|
|
35192
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
35193
|
+
import { join as join22 } from "node:path";
|
|
34214
35194
|
function detectConfiguredTools(targetDir) {
|
|
34215
35195
|
return AI_TOOLS.filter((tool) => {
|
|
34216
35196
|
if (tool.commandStrategy === "subdirectory") {
|
|
34217
|
-
const ocrDir =
|
|
34218
|
-
return
|
|
35197
|
+
const ocrDir = join22(targetDir, tool.commandsDir, "ocr");
|
|
35198
|
+
return existsSync18(ocrDir);
|
|
34219
35199
|
} else {
|
|
34220
|
-
const reviewCmd =
|
|
34221
|
-
return
|
|
35200
|
+
const reviewCmd = join22(targetDir, tool.commandsDir, "ocr-review.md");
|
|
35201
|
+
return existsSync18(reviewCmd);
|
|
34222
35202
|
}
|
|
34223
35203
|
});
|
|
34224
35204
|
}
|
|
@@ -34292,7 +35272,7 @@ var updateCommand = new Command("update").description("Update OCR assets after p
|
|
|
34292
35272
|
const result = installForTool(tool, targetDir);
|
|
34293
35273
|
results.push(result);
|
|
34294
35274
|
}
|
|
34295
|
-
ensureGitignore(
|
|
35275
|
+
ensureGitignore(join22(targetDir, ".ocr"));
|
|
34296
35276
|
spinner.stop();
|
|
34297
35277
|
const successful = results.filter((r) => r.success);
|
|
34298
35278
|
const failed = results.filter((r) => !r.success);
|
|
@@ -34328,10 +35308,10 @@ var updateCommand = new Command("update").description("Update OCR assets after p
|
|
|
34328
35308
|
if (updateInject) {
|
|
34329
35309
|
if (options.dryRun) {
|
|
34330
35310
|
console.log(source_default.dim(" Would update:"));
|
|
34331
|
-
if (
|
|
35311
|
+
if (existsSync18(join22(targetDir, "AGENTS.md"))) {
|
|
34332
35312
|
console.log(source_default.dim(" \u2022 AGENTS.md (OCR managed block)"));
|
|
34333
35313
|
}
|
|
34334
|
-
if (
|
|
35314
|
+
if (existsSync18(join22(targetDir, "CLAUDE.md"))) {
|
|
34335
35315
|
console.log(source_default.dim(" \u2022 CLAUDE.md (OCR managed block)"));
|
|
34336
35316
|
}
|
|
34337
35317
|
console.log();
|
|
@@ -34363,14 +35343,14 @@ var updateCommand = new Command("update").description("Update OCR assets after p
|
|
|
34363
35343
|
});
|
|
34364
35344
|
|
|
34365
35345
|
// src/commands/dashboard.ts
|
|
34366
|
-
import { existsSync as
|
|
34367
|
-
import { join as
|
|
35346
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
35347
|
+
import { join as join23, dirname as dirname7 } from "node:path";
|
|
34368
35348
|
import { fileURLToPath } from "node:url";
|
|
34369
35349
|
init_db();
|
|
34370
35350
|
var __filename = fileURLToPath(import.meta.url);
|
|
34371
|
-
var __dirname =
|
|
35351
|
+
var __dirname = dirname7(__filename);
|
|
34372
35352
|
function resolveServerPath() {
|
|
34373
|
-
return
|
|
35353
|
+
return join23(__dirname, "dashboard", "server.js");
|
|
34374
35354
|
}
|
|
34375
35355
|
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
35356
|
async (options) => {
|
|
@@ -34381,7 +35361,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
|
|
|
34381
35361
|
console.error(source_default.red(`Error: Invalid port "${options.port}". Must be 1-65535.`));
|
|
34382
35362
|
process.exit(1);
|
|
34383
35363
|
}
|
|
34384
|
-
const ocrDir =
|
|
35364
|
+
const ocrDir = join23(targetDir, ".ocr");
|
|
34385
35365
|
try {
|
|
34386
35366
|
await ensureDatabase(ocrDir);
|
|
34387
35367
|
closeAllDatabases();
|
|
@@ -34395,7 +35375,7 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
|
|
|
34395
35375
|
process.exit(1);
|
|
34396
35376
|
}
|
|
34397
35377
|
const serverPath = resolveServerPath();
|
|
34398
|
-
if (!
|
|
35378
|
+
if (!existsSync19(serverPath)) {
|
|
34399
35379
|
console.error(source_default.red("Error: Dashboard server bundle not found."));
|
|
34400
35380
|
console.error(
|
|
34401
35381
|
source_default.dim(` Expected at: ${serverPath}`)
|
|
@@ -34429,8 +35409,9 @@ var dashboardCommand = new Command("dashboard").description("Start the OCR dashb
|
|
|
34429
35409
|
);
|
|
34430
35410
|
|
|
34431
35411
|
// src/commands/doctor.ts
|
|
34432
|
-
import { existsSync as
|
|
34433
|
-
import { join as
|
|
35412
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
35413
|
+
import { join as join24 } from "node:path";
|
|
35414
|
+
init_db();
|
|
34434
35415
|
var doctorCommand = new Command("doctor").description("Check OCR installation and verify all dependencies").action(() => {
|
|
34435
35416
|
printHeader();
|
|
34436
35417
|
const targetDir = process.cwd();
|
|
@@ -34444,10 +35425,10 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
|
|
|
34444
35425
|
console.log(source_default.bold(" OCR Installation"));
|
|
34445
35426
|
console.log();
|
|
34446
35427
|
const ocrStatus = checkOcrSetup(targetDir);
|
|
34447
|
-
const configPath =
|
|
34448
|
-
const dbPath =
|
|
34449
|
-
const hasConfig =
|
|
34450
|
-
const hasDb =
|
|
35428
|
+
const configPath = join24(targetDir, ".ocr", "config.yaml");
|
|
35429
|
+
const dbPath = join24(targetDir, ".ocr", "data", "ocr.db");
|
|
35430
|
+
const hasConfig = existsSync20(configPath);
|
|
35431
|
+
const hasDb = existsSync20(dbPath);
|
|
34451
35432
|
const ocrChecks = [
|
|
34452
35433
|
{ label: ".ocr/skills/", ok: ocrStatus.hasSkills },
|
|
34453
35434
|
{ label: ".ocr/sessions/", ok: ocrStatus.hasSessions },
|
|
@@ -34470,6 +35451,26 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
|
|
|
34470
35451
|
hasIssues = true;
|
|
34471
35452
|
}
|
|
34472
35453
|
console.log();
|
|
35454
|
+
console.log(source_default.bold(" Storage Engine"));
|
|
35455
|
+
console.log();
|
|
35456
|
+
const engine = probeEngine();
|
|
35457
|
+
if (engine.ok) {
|
|
35458
|
+
console.log(
|
|
35459
|
+
` ${source_default.green("\u2713")} better-sqlite3 (SQLite ${engine.version}, WAL)`
|
|
35460
|
+
);
|
|
35461
|
+
} else {
|
|
35462
|
+
hasIssues = true;
|
|
35463
|
+
console.log(
|
|
35464
|
+
` ${source_default.red("\u2717")} better-sqlite3 failed to load`
|
|
35465
|
+
);
|
|
35466
|
+
console.log(` ${source_default.dim(engine.error)}`);
|
|
35467
|
+
console.log(
|
|
35468
|
+
` ${source_default.dim(
|
|
35469
|
+
"Reinstall the CLI; if it persists your platform may need build tools (python3 + a C++ toolchain) or lacks a prebuilt binary."
|
|
35470
|
+
)}`
|
|
35471
|
+
);
|
|
35472
|
+
}
|
|
35473
|
+
console.log();
|
|
34473
35474
|
printCapabilities(depResult);
|
|
34474
35475
|
console.log();
|
|
34475
35476
|
if (hasIssues) {
|
|
@@ -34521,8 +35522,8 @@ var doctorCommand = new Command("doctor").description("Check OCR installation an
|
|
|
34521
35522
|
});
|
|
34522
35523
|
|
|
34523
35524
|
// src/commands/reviewers.ts
|
|
34524
|
-
import { writeFileSync as
|
|
34525
|
-
import { join as
|
|
35525
|
+
import { writeFileSync as writeFileSync9, renameSync as renameSync2 } from "node:fs";
|
|
35526
|
+
import { join as join25 } from "node:path";
|
|
34526
35527
|
async function readStdin3() {
|
|
34527
35528
|
const chunks = [];
|
|
34528
35529
|
for await (const chunk of process.stdin) {
|
|
@@ -34585,20 +35586,20 @@ function validateReviewersMeta(data) {
|
|
|
34585
35586
|
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
35587
|
const targetDir = process.cwd();
|
|
34587
35588
|
requireOcrSetup(targetDir);
|
|
34588
|
-
const ocrDir =
|
|
35589
|
+
const ocrDir = join25(targetDir, ".ocr");
|
|
34589
35590
|
if (!options.stdin) {
|
|
34590
35591
|
try {
|
|
34591
|
-
const reviewersDir =
|
|
34592
|
-
const configPath =
|
|
35592
|
+
const reviewersDir = join25(ocrDir, "skills", "references", "reviewers");
|
|
35593
|
+
const configPath = join25(ocrDir, "config.yaml");
|
|
34593
35594
|
const meta = generateReviewersMeta(reviewersDir, configPath);
|
|
34594
35595
|
if (!meta || meta.reviewers.length === 0) {
|
|
34595
35596
|
console.error(source_default.yellow("No reviewer files found in .ocr/skills/references/reviewers/"));
|
|
34596
35597
|
process.exit(1);
|
|
34597
35598
|
}
|
|
34598
|
-
const metaPath =
|
|
35599
|
+
const metaPath = join25(ocrDir, "reviewers-meta.json");
|
|
34599
35600
|
const tmpPath = metaPath + ".tmp";
|
|
34600
|
-
|
|
34601
|
-
|
|
35601
|
+
writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
|
|
35602
|
+
renameSync2(tmpPath, metaPath);
|
|
34602
35603
|
const tierCounts = meta.reviewers.reduce(
|
|
34603
35604
|
(acc, r) => {
|
|
34604
35605
|
acc[r.tier] = (acc[r.tier] ?? 0) + 1;
|
|
@@ -34625,10 +35626,10 @@ var syncSubcommand2 = new Command("sync").description("Sync reviewers-meta.json
|
|
|
34625
35626
|
throw new Error("Invalid JSON on stdin");
|
|
34626
35627
|
}
|
|
34627
35628
|
const meta = validateReviewersMeta(parsed);
|
|
34628
|
-
const metaPath =
|
|
35629
|
+
const metaPath = join25(ocrDir, "reviewers-meta.json");
|
|
34629
35630
|
const tmpPath = metaPath + ".tmp";
|
|
34630
|
-
|
|
34631
|
-
|
|
35631
|
+
writeFileSync9(tmpPath, JSON.stringify(meta, null, 2) + "\n");
|
|
35632
|
+
renameSync2(tmpPath, metaPath);
|
|
34632
35633
|
const tierCounts = meta.reviewers.reduce(
|
|
34633
35634
|
(acc, r) => {
|
|
34634
35635
|
acc[r.tier] = (acc[r.tier] ?? 0) + 1;
|
|
@@ -34653,25 +35654,25 @@ var reviewersCommand = new Command("reviewers").description("Manage OCR reviewer
|
|
|
34653
35654
|
|
|
34654
35655
|
// src/lib/update-check.ts
|
|
34655
35656
|
import { homedir } from "node:os";
|
|
34656
|
-
import { join as
|
|
34657
|
-
import { readFileSync as
|
|
35657
|
+
import { join as join26 } from "node:path";
|
|
35658
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7 } from "node:fs";
|
|
34658
35659
|
var PACKAGE_NAME = "@open-code-review/cli";
|
|
34659
35660
|
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
34660
|
-
var CACHE_DIR2 =
|
|
34661
|
-
var CACHE_FILE =
|
|
35661
|
+
var CACHE_DIR2 = join26(homedir(), ".ocr");
|
|
35662
|
+
var CACHE_FILE = join26(CACHE_DIR2, "update-check.json");
|
|
34662
35663
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
34663
35664
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
34664
35665
|
function readCache(cacheFile) {
|
|
34665
35666
|
try {
|
|
34666
|
-
return JSON.parse(
|
|
35667
|
+
return JSON.parse(readFileSync14(cacheFile, "utf-8"));
|
|
34667
35668
|
} catch {
|
|
34668
35669
|
return null;
|
|
34669
35670
|
}
|
|
34670
35671
|
}
|
|
34671
35672
|
function writeCache(cacheFile, cache) {
|
|
34672
35673
|
try {
|
|
34673
|
-
mkdirSync7(
|
|
34674
|
-
|
|
35674
|
+
mkdirSync7(join26(cacheFile, ".."), { recursive: true });
|
|
35675
|
+
writeFileSync10(cacheFile, JSON.stringify(cache));
|
|
34675
35676
|
} catch {
|
|
34676
35677
|
}
|
|
34677
35678
|
}
|
|
@@ -34691,7 +35692,7 @@ async function checkForUpdate(currentVersion, options) {
|
|
|
34691
35692
|
if (process.env.CI || process.env.OCR_NO_UPDATE_CHECK) {
|
|
34692
35693
|
return null;
|
|
34693
35694
|
}
|
|
34694
|
-
const cacheFile =
|
|
35695
|
+
const cacheFile = join26(options?.cacheDir ?? CACHE_DIR2, "update-check.json");
|
|
34695
35696
|
try {
|
|
34696
35697
|
const cache = readCache(cacheFile);
|
|
34697
35698
|
if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
|