@basou/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +398 -130
- package/dist/index.js +1128 -667
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -767,7 +767,7 @@ function isLazyExpired(approval, now) {
|
|
|
767
767
|
|
|
768
768
|
// src/decisions/decisions-renderer.ts
|
|
769
769
|
import { lstat } from "fs/promises";
|
|
770
|
-
import { dirname, join as
|
|
770
|
+
import { dirname as dirname2, join as join6, resolve } from "path";
|
|
771
771
|
|
|
772
772
|
// src/events/event-replay.ts
|
|
773
773
|
import { createReadStream } from "fs";
|
|
@@ -1013,7 +1013,208 @@ async function readAllEvents(sessionDir, options = {}) {
|
|
|
1013
1013
|
|
|
1014
1014
|
// src/storage/sessions.ts
|
|
1015
1015
|
import { readdir as readdir2 } from "fs/promises";
|
|
1016
|
-
import { join as
|
|
1016
|
+
import { join as join5 } from "path";
|
|
1017
|
+
|
|
1018
|
+
// src/events/chained-append.ts
|
|
1019
|
+
import { appendFile, readFile as readFile3 } from "fs/promises";
|
|
1020
|
+
import { join as join4 } from "path";
|
|
1021
|
+
|
|
1022
|
+
// src/storage/lockfile.ts
|
|
1023
|
+
import { mkdir, readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
1024
|
+
import { dirname, join as join3 } from "path";
|
|
1025
|
+
var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
1026
|
+
async function acquireLock(paths, scope, resourceId) {
|
|
1027
|
+
const lockPath = lockfilePath(paths, scope, resourceId);
|
|
1028
|
+
const body = {
|
|
1029
|
+
pid: process.pid,
|
|
1030
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1031
|
+
};
|
|
1032
|
+
const serialised = JSON.stringify(body);
|
|
1033
|
+
try {
|
|
1034
|
+
await atomicCreate(lockPath, serialised);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (findErrorCode(error, "ENOENT")) {
|
|
1037
|
+
try {
|
|
1038
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
1039
|
+
await atomicCreate(lockPath, serialised);
|
|
1040
|
+
return {
|
|
1041
|
+
release: async () => {
|
|
1042
|
+
await unlink2(lockPath).catch(() => void 0);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
} catch (retryError) {
|
|
1046
|
+
throw new Error("Failed to acquire lock", { cause: retryError });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (!findErrorCode(error, "EEXIST")) {
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
const stale = await isStaleLock(lockPath);
|
|
1053
|
+
if (!stale) {
|
|
1054
|
+
throw new Error("Lock is held by another process", { cause: error });
|
|
1055
|
+
}
|
|
1056
|
+
await unlink2(lockPath).catch(() => void 0);
|
|
1057
|
+
try {
|
|
1058
|
+
await atomicCreate(lockPath, serialised);
|
|
1059
|
+
} catch (retryError) {
|
|
1060
|
+
throw new Error("Lock is held by another process", { cause: retryError });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
release: async () => {
|
|
1065
|
+
await unlink2(lockPath).catch(() => void 0);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async function isStaleLock(lockPath) {
|
|
1070
|
+
let body;
|
|
1071
|
+
try {
|
|
1072
|
+
const raw = await readFile2(lockPath, "utf8");
|
|
1073
|
+
const parsed = JSON.parse(raw);
|
|
1074
|
+
if (typeof parsed !== "object" || parsed === null) return true;
|
|
1075
|
+
const candidate = parsed;
|
|
1076
|
+
if (typeof candidate.pid !== "number" || typeof candidate.acquired_at !== "string") {
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
body = { pid: candidate.pid, acquired_at: candidate.acquired_at };
|
|
1080
|
+
} catch {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
const ageMs = Date.now() - Date.parse(body.acquired_at);
|
|
1084
|
+
if (!Number.isFinite(ageMs) || ageMs > STALE_LOCK_MAX_AGE_MS) {
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
process.kill(body.pid, 0);
|
|
1089
|
+
return false;
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (findErrorCode(error, "ESRCH")) return true;
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function lockfilePath(paths, scope, resourceId) {
|
|
1096
|
+
const sep = resourceId.indexOf("_");
|
|
1097
|
+
const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
|
|
1098
|
+
return join3(paths.locks, `${scope}_${ulid2}.lock`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/events/chain.ts
|
|
1102
|
+
import { createHash } from "crypto";
|
|
1103
|
+
var GENESIS_PREFIX = "basou:event-chain:v1:";
|
|
1104
|
+
function genesisHash(sessionId) {
|
|
1105
|
+
return createHash("sha256").update(`${GENESIS_PREFIX}${sessionId}`, "utf8").digest("hex");
|
|
1106
|
+
}
|
|
1107
|
+
function lineHash(rawLine) {
|
|
1108
|
+
const hash = createHash("sha256");
|
|
1109
|
+
if (typeof rawLine === "string") {
|
|
1110
|
+
hash.update(rawLine, "utf8");
|
|
1111
|
+
} else {
|
|
1112
|
+
hash.update(rawLine);
|
|
1113
|
+
}
|
|
1114
|
+
return hash.digest("hex");
|
|
1115
|
+
}
|
|
1116
|
+
function serializeEventLine(event) {
|
|
1117
|
+
return JSON.stringify(event);
|
|
1118
|
+
}
|
|
1119
|
+
function chainEvents(events, sessionId) {
|
|
1120
|
+
let prev = genesisHash(sessionId);
|
|
1121
|
+
const lines = [];
|
|
1122
|
+
for (const event of events) {
|
|
1123
|
+
const chained = { ...event, prev_hash: prev };
|
|
1124
|
+
const line = serializeEventLine(chained);
|
|
1125
|
+
lines.push(line);
|
|
1126
|
+
prev = lineHash(line);
|
|
1127
|
+
}
|
|
1128
|
+
return { lines, headHash: prev, count: lines.length };
|
|
1129
|
+
}
|
|
1130
|
+
function chainRawJsonLines(rawLines, sessionId) {
|
|
1131
|
+
let prev = genesisHash(sessionId);
|
|
1132
|
+
const lines = [];
|
|
1133
|
+
for (const rawLine of rawLines) {
|
|
1134
|
+
const parsed = JSON.parse(rawLine);
|
|
1135
|
+
const line = JSON.stringify({ ...parsed, prev_hash: prev });
|
|
1136
|
+
lines.push(line);
|
|
1137
|
+
prev = lineHash(line);
|
|
1138
|
+
}
|
|
1139
|
+
return { lines, headHash: prev, count: lines.length };
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/events/chained-append.ts
|
|
1143
|
+
function splitLinesBytes(buf) {
|
|
1144
|
+
const out = [];
|
|
1145
|
+
let start = 0;
|
|
1146
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1147
|
+
if (buf[i] === 10) {
|
|
1148
|
+
out.push(buf.subarray(start, i));
|
|
1149
|
+
start = i + 1;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (start < buf.length) out.push(buf.subarray(start));
|
|
1153
|
+
return out;
|
|
1154
|
+
}
|
|
1155
|
+
function carriesPrevHash(line) {
|
|
1156
|
+
try {
|
|
1157
|
+
const obj = JSON.parse(line.toString("utf8"));
|
|
1158
|
+
return typeof obj === "object" && obj !== null && "prev_hash" in obj;
|
|
1159
|
+
} catch {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
async function inspectChainTail(paths, sessionId) {
|
|
1164
|
+
const filePath = join4(paths.sessions, sessionId, "events.jsonl");
|
|
1165
|
+
let raw;
|
|
1166
|
+
try {
|
|
1167
|
+
raw = await readFile3(filePath);
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
if (findErrorCode(error, "ENOENT")) {
|
|
1170
|
+
return { chained: true, head: genesisHash(sessionId), count: 0 };
|
|
1171
|
+
}
|
|
1172
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
1173
|
+
}
|
|
1174
|
+
if (raw.length === 0) {
|
|
1175
|
+
return { chained: true, head: genesisHash(sessionId), count: 0 };
|
|
1176
|
+
}
|
|
1177
|
+
if (raw[raw.length - 1] !== 10) {
|
|
1178
|
+
throw new Error("Unterminated final line in events.jsonl");
|
|
1179
|
+
}
|
|
1180
|
+
const lines = splitLinesBytes(raw);
|
|
1181
|
+
const first = lines[0];
|
|
1182
|
+
const last = lines[lines.length - 1];
|
|
1183
|
+
const firstChained = carriesPrevHash(first);
|
|
1184
|
+
if (firstChained !== carriesPrevHash(last)) {
|
|
1185
|
+
throw new Error("events.jsonl is partially chained");
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
chained: firstChained,
|
|
1189
|
+
head: firstChained ? lineHash(last) : genesisHash(sessionId),
|
|
1190
|
+
count: lines.length
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
async function appendChainedEventLocked(paths, sessionId, event) {
|
|
1194
|
+
let validated;
|
|
1195
|
+
try {
|
|
1196
|
+
validated = EventSchema.parse(event);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
1199
|
+
}
|
|
1200
|
+
const tail = await inspectChainTail(paths, sessionId);
|
|
1201
|
+
const line = tail.chained ? serializeEventLine({ ...validated, prev_hash: tail.head }) : serializeEventLine(validated);
|
|
1202
|
+
try {
|
|
1203
|
+
await appendFile(join4(paths.sessions, sessionId, "events.jsonl"), `${line}
|
|
1204
|
+
`, "utf8");
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
1207
|
+
}
|
|
1208
|
+
return { chained: tail.chained };
|
|
1209
|
+
}
|
|
1210
|
+
async function appendChainedEvent(paths, sessionId, event) {
|
|
1211
|
+
const lock = await acquireLock(paths, "session", sessionId);
|
|
1212
|
+
try {
|
|
1213
|
+
return await appendChainedEventLocked(paths, sessionId, event);
|
|
1214
|
+
} finally {
|
|
1215
|
+
await lock.release();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1017
1218
|
|
|
1018
1219
|
// src/schemas/session.schema.ts
|
|
1019
1220
|
import { z as z4 } from "zod";
|
|
@@ -1108,7 +1309,7 @@ async function enumerateSessionDirs(paths) {
|
|
|
1108
1309
|
}
|
|
1109
1310
|
}
|
|
1110
1311
|
async function readSessionYaml(paths, sessionId) {
|
|
1111
|
-
const filePath =
|
|
1312
|
+
const filePath = join5(paths.sessions, sessionId, "session.yaml");
|
|
1112
1313
|
let raw;
|
|
1113
1314
|
try {
|
|
1114
1315
|
raw = await readYamlFile(filePath);
|
|
@@ -1122,11 +1323,26 @@ async function readSessionYaml(paths, sessionId) {
|
|
|
1122
1323
|
}
|
|
1123
1324
|
return result.data;
|
|
1124
1325
|
}
|
|
1326
|
+
async function finalizeSessionYaml(paths, sessionId, mutate) {
|
|
1327
|
+
const lock = await acquireLock(paths, "session", sessionId);
|
|
1328
|
+
try {
|
|
1329
|
+
const session = await readSessionYaml(paths, sessionId);
|
|
1330
|
+
mutate(session);
|
|
1331
|
+
const tail = await inspectChainTail(paths, sessionId);
|
|
1332
|
+
if (tail.chained && tail.count > 0) {
|
|
1333
|
+
session.session.integrity = { head_hash: tail.head, event_count: tail.count };
|
|
1334
|
+
}
|
|
1335
|
+
const validated = SessionSchema.parse(session);
|
|
1336
|
+
await overwriteYamlFile(join5(paths.sessions, sessionId, "session.yaml"), validated);
|
|
1337
|
+
} finally {
|
|
1338
|
+
await lock.release();
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1125
1341
|
async function classifySuspect(paths, sessionId, session, now, onWarning) {
|
|
1126
1342
|
if (session.session.status !== "running") {
|
|
1127
1343
|
return { suspect: false, suspectReason: null };
|
|
1128
1344
|
}
|
|
1129
|
-
const sessionDir =
|
|
1345
|
+
const sessionDir = join5(paths.sessions, sessionId);
|
|
1130
1346
|
let endedFound = false;
|
|
1131
1347
|
let lastEventOccurredAt = null;
|
|
1132
1348
|
const replayOpts = onWarning !== void 0 ? { onWarning } : {};
|
|
@@ -1194,7 +1410,7 @@ async function renderDecisions(input) {
|
|
|
1194
1410
|
const decisions = [];
|
|
1195
1411
|
const knownEventIds = /* @__PURE__ */ new Set();
|
|
1196
1412
|
for (const entry of entries) {
|
|
1197
|
-
const sessionDir =
|
|
1413
|
+
const sessionDir = join6(input.paths.sessions, entry.sessionId);
|
|
1198
1414
|
try {
|
|
1199
1415
|
for await (const ev of replayEvents(sessionDir, {
|
|
1200
1416
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -1224,7 +1440,7 @@ async function renderDecisions(input) {
|
|
|
1224
1440
|
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
1225
1441
|
return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
|
|
1226
1442
|
});
|
|
1227
|
-
const repoRoot =
|
|
1443
|
+
const repoRoot = dirname2(input.paths.root);
|
|
1228
1444
|
const fileExistenceCache = /* @__PURE__ */ new Map();
|
|
1229
1445
|
async function fileExists(relPath) {
|
|
1230
1446
|
const cached = fileExistenceCache.get(relPath);
|
|
@@ -1298,50 +1514,9 @@ function shortDecisionSessionId(sessionId) {
|
|
|
1298
1514
|
return sessionId.slice(0, 10);
|
|
1299
1515
|
}
|
|
1300
1516
|
|
|
1301
|
-
// src/events/chain.ts
|
|
1302
|
-
import { createHash } from "crypto";
|
|
1303
|
-
var GENESIS_PREFIX = "basou:event-chain:v1:";
|
|
1304
|
-
function genesisHash(sessionId) {
|
|
1305
|
-
return createHash("sha256").update(`${GENESIS_PREFIX}${sessionId}`, "utf8").digest("hex");
|
|
1306
|
-
}
|
|
1307
|
-
function lineHash(rawLine) {
|
|
1308
|
-
const hash = createHash("sha256");
|
|
1309
|
-
if (typeof rawLine === "string") {
|
|
1310
|
-
hash.update(rawLine, "utf8");
|
|
1311
|
-
} else {
|
|
1312
|
-
hash.update(rawLine);
|
|
1313
|
-
}
|
|
1314
|
-
return hash.digest("hex");
|
|
1315
|
-
}
|
|
1316
|
-
function serializeEventLine(event) {
|
|
1317
|
-
return JSON.stringify(event);
|
|
1318
|
-
}
|
|
1319
|
-
function chainEvents(events, sessionId) {
|
|
1320
|
-
let prev = genesisHash(sessionId);
|
|
1321
|
-
const lines = [];
|
|
1322
|
-
for (const event of events) {
|
|
1323
|
-
const chained = { ...event, prev_hash: prev };
|
|
1324
|
-
const line = serializeEventLine(chained);
|
|
1325
|
-
lines.push(line);
|
|
1326
|
-
prev = lineHash(line);
|
|
1327
|
-
}
|
|
1328
|
-
return { lines, headHash: prev, count: lines.length };
|
|
1329
|
-
}
|
|
1330
|
-
function chainRawJsonLines(rawLines, sessionId) {
|
|
1331
|
-
let prev = genesisHash(sessionId);
|
|
1332
|
-
const lines = [];
|
|
1333
|
-
for (const rawLine of rawLines) {
|
|
1334
|
-
const parsed = JSON.parse(rawLine);
|
|
1335
|
-
const line = JSON.stringify({ ...parsed, prev_hash: prev });
|
|
1336
|
-
lines.push(line);
|
|
1337
|
-
prev = lineHash(line);
|
|
1338
|
-
}
|
|
1339
|
-
return { lines, headHash: prev, count: lines.length };
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
1517
|
// src/events/event-writer.ts
|
|
1343
|
-
import { appendFile } from "fs/promises";
|
|
1344
|
-
import { basename, join as
|
|
1518
|
+
import { appendFile as appendFile2 } from "fs/promises";
|
|
1519
|
+
import { basename, join as join7 } from "path";
|
|
1345
1520
|
async function appendEvent(sessionDir, event) {
|
|
1346
1521
|
let validated;
|
|
1347
1522
|
try {
|
|
@@ -1352,7 +1527,7 @@ async function appendEvent(sessionDir, event) {
|
|
|
1352
1527
|
const line = `${serializeEventLine(validated)}
|
|
1353
1528
|
`;
|
|
1354
1529
|
try {
|
|
1355
|
-
await
|
|
1530
|
+
await appendFile2(join7(sessionDir, "events.jsonl"), line, "utf8");
|
|
1356
1531
|
} catch (error) {
|
|
1357
1532
|
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
1358
1533
|
}
|
|
@@ -1366,7 +1541,7 @@ async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
|
1366
1541
|
} catch (error) {
|
|
1367
1542
|
throw new Error("Invalid Basou event payload", { cause: error });
|
|
1368
1543
|
}
|
|
1369
|
-
const filePath =
|
|
1544
|
+
const filePath = join7(sessionDir, "events.jsonl");
|
|
1370
1545
|
let body;
|
|
1371
1546
|
let result = null;
|
|
1372
1547
|
if (options.chain === true) {
|
|
@@ -1387,13 +1562,30 @@ async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
|
1387
1562
|
}
|
|
1388
1563
|
|
|
1389
1564
|
// src/events/verify.ts
|
|
1390
|
-
import { readFile as
|
|
1391
|
-
import { join as
|
|
1565
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1566
|
+
import { join as join8 } from "path";
|
|
1567
|
+
var STRICT_STATUSES = /* @__PURE__ */ new Set([
|
|
1568
|
+
"completed",
|
|
1569
|
+
"failed",
|
|
1570
|
+
"interrupted",
|
|
1571
|
+
"imported",
|
|
1572
|
+
"archived"
|
|
1573
|
+
]);
|
|
1574
|
+
function isLiveStatus(status) {
|
|
1575
|
+
return !STRICT_STATUSES.has(status);
|
|
1576
|
+
}
|
|
1392
1577
|
async function verifyEventsChain(paths, sessionId) {
|
|
1393
|
-
const
|
|
1578
|
+
const first = await verifyOnce(paths, sessionId);
|
|
1579
|
+
if (first.status === "tampered" && first.reason === "anchor_mismatch") {
|
|
1580
|
+
return await verifyOnce(paths, sessionId);
|
|
1581
|
+
}
|
|
1582
|
+
return first;
|
|
1583
|
+
}
|
|
1584
|
+
async function verifyOnce(paths, sessionId) {
|
|
1585
|
+
const sessionDir = join8(paths.sessions, sessionId);
|
|
1394
1586
|
let raw = null;
|
|
1395
1587
|
try {
|
|
1396
|
-
raw = await
|
|
1588
|
+
raw = await readFile4(join8(sessionDir, "events.jsonl"));
|
|
1397
1589
|
} catch (error) {
|
|
1398
1590
|
if (!findErrorCode(error, "ENOENT")) {
|
|
1399
1591
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
@@ -1402,7 +1594,11 @@ async function verifyEventsChain(paths, sessionId) {
|
|
|
1402
1594
|
let anchor;
|
|
1403
1595
|
try {
|
|
1404
1596
|
const session = await readSessionYaml(paths, sessionId);
|
|
1405
|
-
anchor = {
|
|
1597
|
+
anchor = {
|
|
1598
|
+
kind: "present",
|
|
1599
|
+
integrity: session.session.integrity,
|
|
1600
|
+
status: session.session.status
|
|
1601
|
+
};
|
|
1406
1602
|
} catch (error) {
|
|
1407
1603
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
1408
1604
|
anchor = { kind: "absent" };
|
|
@@ -1411,10 +1607,10 @@ async function verifyEventsChain(paths, sessionId) {
|
|
|
1411
1607
|
}
|
|
1412
1608
|
}
|
|
1413
1609
|
const terminated = raw === null || raw.length === 0 || raw[raw.length - 1] === 10;
|
|
1414
|
-
const segments = raw === null ? [] :
|
|
1610
|
+
const segments = raw === null ? [] : splitLinesBytes2(raw);
|
|
1415
1611
|
const tailFragment = !terminated && segments.length > 0 ? segments.pop() : null;
|
|
1416
1612
|
const lines = segments;
|
|
1417
|
-
const
|
|
1613
|
+
const carriesPrevHash2 = (s) => {
|
|
1418
1614
|
try {
|
|
1419
1615
|
const obj = JSON.parse(s.toString("utf8"));
|
|
1420
1616
|
return typeof obj === "object" && obj !== null && "prev_hash" in obj;
|
|
@@ -1422,7 +1618,7 @@ async function verifyEventsChain(paths, sessionId) {
|
|
|
1422
1618
|
return false;
|
|
1423
1619
|
}
|
|
1424
1620
|
};
|
|
1425
|
-
const chained = lines.some((l) => l.length > 0 &&
|
|
1621
|
+
const chained = lines.some((l) => l.length > 0 && carriesPrevHash2(l)) || tailFragment !== null && carriesPrevHash2(tailFragment);
|
|
1426
1622
|
if (!chained) {
|
|
1427
1623
|
if (anchor.kind === "present" && anchor.integrity !== void 0) {
|
|
1428
1624
|
return {
|
|
@@ -1481,7 +1677,11 @@ async function verifyEventsChain(paths, sessionId) {
|
|
|
1481
1677
|
}
|
|
1482
1678
|
expected = lineHash(line);
|
|
1483
1679
|
}
|
|
1680
|
+
const live = anchor.kind === "present" && isLiveStatus(anchor.status);
|
|
1484
1681
|
if (tailFragment !== null || !terminated) {
|
|
1682
|
+
if (live) {
|
|
1683
|
+
return { status: "in_progress", eventCount: lines.length };
|
|
1684
|
+
}
|
|
1485
1685
|
return {
|
|
1486
1686
|
status: "tampered",
|
|
1487
1687
|
eventCount: lines.length,
|
|
@@ -1495,6 +1695,9 @@ async function verifyEventsChain(paths, sessionId) {
|
|
|
1495
1695
|
if (anchor.kind === "unreadable") {
|
|
1496
1696
|
return { status: "tampered", eventCount: lines.length, reason: "yaml_unreadable" };
|
|
1497
1697
|
}
|
|
1698
|
+
if (live) {
|
|
1699
|
+
return { status: "in_progress", eventCount: lines.length };
|
|
1700
|
+
}
|
|
1498
1701
|
if (anchor.integrity === void 0) {
|
|
1499
1702
|
return { status: "tampered", eventCount: lines.length, reason: "anchor_missing" };
|
|
1500
1703
|
}
|
|
@@ -1503,7 +1706,7 @@ async function verifyEventsChain(paths, sessionId) {
|
|
|
1503
1706
|
}
|
|
1504
1707
|
return { status: "verified", eventCount: lines.length };
|
|
1505
1708
|
}
|
|
1506
|
-
function
|
|
1709
|
+
function splitLinesBytes2(buf) {
|
|
1507
1710
|
const out = [];
|
|
1508
1711
|
let start = 0;
|
|
1509
1712
|
for (let i = 0; i < buf.length; i++) {
|
|
@@ -1810,12 +2013,12 @@ function parseDiffNameStatus(raw) {
|
|
|
1810
2013
|
}
|
|
1811
2014
|
|
|
1812
2015
|
// src/handoff/handoff-renderer.ts
|
|
1813
|
-
import { join as
|
|
2016
|
+
import { join as join12 } from "path";
|
|
1814
2017
|
|
|
1815
2018
|
// src/storage/tasks.ts
|
|
1816
2019
|
import { createHash as createHash2 } from "crypto";
|
|
1817
|
-
import { mkdir as mkdir3, readdir as readdir3, readFile as
|
|
1818
|
-
import { join as
|
|
2020
|
+
import { mkdir as mkdir3, readdir as readdir3, readFile as readFile7, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
|
|
2021
|
+
import { join as join11 } from "path";
|
|
1819
2022
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1820
2023
|
import { z as z8 } from "zod";
|
|
1821
2024
|
|
|
@@ -1857,9 +2060,9 @@ var TaskSchema = z6.object({
|
|
|
1857
2060
|
});
|
|
1858
2061
|
|
|
1859
2062
|
// src/storage/ad-hoc-session.ts
|
|
1860
|
-
import { mkdir, rm } from "fs/promises";
|
|
2063
|
+
import { mkdir as mkdir2, rm } from "fs/promises";
|
|
1861
2064
|
import { homedir } from "os";
|
|
1862
|
-
import { join as
|
|
2065
|
+
import { join as join9 } from "path";
|
|
1863
2066
|
|
|
1864
2067
|
// src/lib/path-sanitizer.ts
|
|
1865
2068
|
import { posix as path } from "path";
|
|
@@ -1939,87 +2142,94 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
1939
2142
|
taskId: input.taskId ?? null
|
|
1940
2143
|
})
|
|
1941
2144
|
);
|
|
1942
|
-
const sessionDir =
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
throw new Error("Failed to create session directory", { cause: error });
|
|
1947
|
-
}
|
|
1948
|
-
const sessionYamlPath = join7(sessionDir, "session.yaml");
|
|
2145
|
+
const sessionDir = join9(input.paths.sessions, sessionId);
|
|
2146
|
+
const sessionYamlPath = join9(sessionDir, "session.yaml");
|
|
2147
|
+
const lock = await acquireLock(input.paths, "session", sessionId);
|
|
2148
|
+
let bulkResult = null;
|
|
1949
2149
|
try {
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
throw new Error("Session directory collision (retry the command)", {
|
|
1955
|
-
cause: error
|
|
1956
|
-
});
|
|
2150
|
+
try {
|
|
2151
|
+
await mkdir2(sessionDir, { recursive: true });
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
throw new Error("Failed to create session directory", { cause: error });
|
|
1957
2154
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
{
|
|
1967
|
-
schema_version: "0.1.0",
|
|
1968
|
-
id: startedEventId,
|
|
1969
|
-
session_id: sessionId,
|
|
1970
|
-
occurred_at: input.occurredAt,
|
|
1971
|
-
source: "local-cli",
|
|
1972
|
-
type: "session_started"
|
|
1973
|
-
},
|
|
1974
|
-
{
|
|
1975
|
-
schema_version: "0.1.0",
|
|
1976
|
-
id: statusToRunningEventId,
|
|
1977
|
-
session_id: sessionId,
|
|
1978
|
-
occurred_at: input.occurredAt,
|
|
1979
|
-
source: "local-cli",
|
|
1980
|
-
type: "session_status_changed",
|
|
1981
|
-
from: "initialized",
|
|
1982
|
-
to: "running"
|
|
1983
|
-
},
|
|
1984
|
-
...targetEvents,
|
|
1985
|
-
{
|
|
1986
|
-
schema_version: "0.1.0",
|
|
1987
|
-
id: statusToCompletedEventId,
|
|
1988
|
-
session_id: sessionId,
|
|
1989
|
-
occurred_at: input.occurredAt,
|
|
1990
|
-
source: "local-cli",
|
|
1991
|
-
type: "session_status_changed",
|
|
1992
|
-
from: "running",
|
|
1993
|
-
to: "completed"
|
|
1994
|
-
},
|
|
1995
|
-
{
|
|
1996
|
-
schema_version: "0.1.0",
|
|
1997
|
-
id: endedEventId,
|
|
1998
|
-
session_id: sessionId,
|
|
1999
|
-
occurred_at: input.occurredAt,
|
|
2000
|
-
source: "local-cli",
|
|
2001
|
-
type: "session_ended",
|
|
2002
|
-
exit_code: 0
|
|
2003
|
-
}
|
|
2004
|
-
];
|
|
2005
|
-
await writeEventsBulk(sessionDir, events);
|
|
2006
|
-
} catch (error) {
|
|
2007
|
-
await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
2008
|
-
throw error;
|
|
2009
|
-
}
|
|
2010
|
-
try {
|
|
2011
|
-
const finalSession = SessionSchema.parse({
|
|
2012
|
-
...initialSession,
|
|
2013
|
-
session: {
|
|
2014
|
-
...initialSession.session,
|
|
2015
|
-
status: "completed",
|
|
2016
|
-
ended_at: input.occurredAt,
|
|
2017
|
-
invocation: { ...initialSession.session.invocation, exit_code: 0 }
|
|
2155
|
+
try {
|
|
2156
|
+
await linkYamlFile(sessionYamlPath, initialSession);
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
2159
|
+
if (findErrorCode(error, "EEXIST")) {
|
|
2160
|
+
throw new Error("Session directory collision (retry the command)", {
|
|
2161
|
+
cause: error
|
|
2162
|
+
});
|
|
2018
2163
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2164
|
+
throw error;
|
|
2165
|
+
}
|
|
2166
|
+
try {
|
|
2167
|
+
const targetEvents = input.targetEventBuilders.map((build, index) => {
|
|
2168
|
+
const targetEventId = targetEventIds[index];
|
|
2169
|
+
return assertTargetEventIdentity(build(sessionId, targetEventId), sessionId, targetEventId);
|
|
2170
|
+
});
|
|
2171
|
+
const events = [
|
|
2172
|
+
{
|
|
2173
|
+
schema_version: "0.1.0",
|
|
2174
|
+
id: startedEventId,
|
|
2175
|
+
session_id: sessionId,
|
|
2176
|
+
occurred_at: input.occurredAt,
|
|
2177
|
+
source: "local-cli",
|
|
2178
|
+
type: "session_started"
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
schema_version: "0.1.0",
|
|
2182
|
+
id: statusToRunningEventId,
|
|
2183
|
+
session_id: sessionId,
|
|
2184
|
+
occurred_at: input.occurredAt,
|
|
2185
|
+
source: "local-cli",
|
|
2186
|
+
type: "session_status_changed",
|
|
2187
|
+
from: "initialized",
|
|
2188
|
+
to: "running"
|
|
2189
|
+
},
|
|
2190
|
+
...targetEvents,
|
|
2191
|
+
{
|
|
2192
|
+
schema_version: "0.1.0",
|
|
2193
|
+
id: statusToCompletedEventId,
|
|
2194
|
+
session_id: sessionId,
|
|
2195
|
+
occurred_at: input.occurredAt,
|
|
2196
|
+
source: "local-cli",
|
|
2197
|
+
type: "session_status_changed",
|
|
2198
|
+
from: "running",
|
|
2199
|
+
to: "completed"
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
schema_version: "0.1.0",
|
|
2203
|
+
id: endedEventId,
|
|
2204
|
+
session_id: sessionId,
|
|
2205
|
+
occurred_at: input.occurredAt,
|
|
2206
|
+
source: "local-cli",
|
|
2207
|
+
type: "session_ended",
|
|
2208
|
+
exit_code: 0
|
|
2209
|
+
}
|
|
2210
|
+
];
|
|
2211
|
+
bulkResult = await writeEventsBulk(sessionDir, events, { chain: true });
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
2214
|
+
throw error;
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
const finalSession = SessionSchema.parse({
|
|
2218
|
+
...initialSession,
|
|
2219
|
+
session: {
|
|
2220
|
+
...initialSession.session,
|
|
2221
|
+
status: "completed",
|
|
2222
|
+
ended_at: input.occurredAt,
|
|
2223
|
+
invocation: { ...initialSession.session.invocation, exit_code: 0 },
|
|
2224
|
+
...bulkResult !== null ? { integrity: { head_hash: bulkResult.headHash, event_count: bulkResult.count } } : {}
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
await overwriteYamlFile(sessionYamlPath, finalSession);
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
throw new FailedToFinalizeError(sessionId, targetEventIds, error);
|
|
2230
|
+
}
|
|
2231
|
+
} finally {
|
|
2232
|
+
await lock.release();
|
|
2023
2233
|
}
|
|
2024
2234
|
return {
|
|
2025
2235
|
sessionId,
|
|
@@ -2068,8 +2278,7 @@ async function appendEventToExistingSession(input) {
|
|
|
2068
2278
|
}
|
|
2069
2279
|
const eventId = prefixedUlid("evt");
|
|
2070
2280
|
const event = assertTargetEventIdentity(input.eventBuilder(eventId), input.sessionId, eventId);
|
|
2071
|
-
|
|
2072
|
-
await appendEvent(sessionDir, event);
|
|
2281
|
+
await appendChainedEventLocked(input.paths, input.sessionId, event);
|
|
2073
2282
|
return { eventId, sessionStatus: status };
|
|
2074
2283
|
}
|
|
2075
2284
|
function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
|
|
@@ -2082,88 +2291,9 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
|
|
|
2082
2291
|
return event;
|
|
2083
2292
|
}
|
|
2084
2293
|
|
|
2085
|
-
// src/storage/lockfile.ts
|
|
2086
|
-
import { mkdir as mkdir2, readFile as readFile4, unlink as unlink2 } from "fs/promises";
|
|
2087
|
-
import { dirname as dirname2, join as join8 } from "path";
|
|
2088
|
-
var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
2089
|
-
async function acquireLock(paths, scope, resourceId) {
|
|
2090
|
-
const lockPath = lockfilePath(paths, scope, resourceId);
|
|
2091
|
-
const body = {
|
|
2092
|
-
pid: process.pid,
|
|
2093
|
-
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2094
|
-
};
|
|
2095
|
-
const serialised = JSON.stringify(body);
|
|
2096
|
-
try {
|
|
2097
|
-
await atomicCreate(lockPath, serialised);
|
|
2098
|
-
} catch (error) {
|
|
2099
|
-
if (findErrorCode(error, "ENOENT")) {
|
|
2100
|
-
try {
|
|
2101
|
-
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
2102
|
-
await atomicCreate(lockPath, serialised);
|
|
2103
|
-
return {
|
|
2104
|
-
release: async () => {
|
|
2105
|
-
await unlink2(lockPath).catch(() => void 0);
|
|
2106
|
-
}
|
|
2107
|
-
};
|
|
2108
|
-
} catch (retryError) {
|
|
2109
|
-
throw new Error("Failed to acquire lock", { cause: retryError });
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
if (!findErrorCode(error, "EEXIST")) {
|
|
2113
|
-
throw error;
|
|
2114
|
-
}
|
|
2115
|
-
const stale = await isStaleLock(lockPath);
|
|
2116
|
-
if (!stale) {
|
|
2117
|
-
throw new Error("Lock is held by another process", { cause: error });
|
|
2118
|
-
}
|
|
2119
|
-
await unlink2(lockPath).catch(() => void 0);
|
|
2120
|
-
try {
|
|
2121
|
-
await atomicCreate(lockPath, serialised);
|
|
2122
|
-
} catch (retryError) {
|
|
2123
|
-
throw new Error("Lock is held by another process", { cause: retryError });
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
return {
|
|
2127
|
-
release: async () => {
|
|
2128
|
-
await unlink2(lockPath).catch(() => void 0);
|
|
2129
|
-
}
|
|
2130
|
-
};
|
|
2131
|
-
}
|
|
2132
|
-
async function isStaleLock(lockPath) {
|
|
2133
|
-
let body;
|
|
2134
|
-
try {
|
|
2135
|
-
const raw = await readFile4(lockPath, "utf8");
|
|
2136
|
-
const parsed = JSON.parse(raw);
|
|
2137
|
-
if (typeof parsed !== "object" || parsed === null) return true;
|
|
2138
|
-
const candidate = parsed;
|
|
2139
|
-
if (typeof candidate.pid !== "number" || typeof candidate.acquired_at !== "string") {
|
|
2140
|
-
return true;
|
|
2141
|
-
}
|
|
2142
|
-
body = { pid: candidate.pid, acquired_at: candidate.acquired_at };
|
|
2143
|
-
} catch {
|
|
2144
|
-
return true;
|
|
2145
|
-
}
|
|
2146
|
-
const ageMs = Date.now() - Date.parse(body.acquired_at);
|
|
2147
|
-
if (!Number.isFinite(ageMs) || ageMs > STALE_LOCK_MAX_AGE_MS) {
|
|
2148
|
-
return true;
|
|
2149
|
-
}
|
|
2150
|
-
try {
|
|
2151
|
-
process.kill(body.pid, 0);
|
|
2152
|
-
return false;
|
|
2153
|
-
} catch (error) {
|
|
2154
|
-
if (findErrorCode(error, "ESRCH")) return true;
|
|
2155
|
-
return false;
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
function lockfilePath(paths, scope, resourceId) {
|
|
2159
|
-
const sep = resourceId.indexOf("_");
|
|
2160
|
-
const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
|
|
2161
|
-
return join8(paths.locks, `${scope}_${ulid2}.lock`);
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
2294
|
// src/storage/task-index.ts
|
|
2165
|
-
import { readFile as
|
|
2166
|
-
import { join as
|
|
2295
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2296
|
+
import { join as join10 } from "path";
|
|
2167
2297
|
|
|
2168
2298
|
// src/schemas/task-index.schema.ts
|
|
2169
2299
|
import { z as z7 } from "zod";
|
|
@@ -2182,13 +2312,13 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
|
|
|
2182
2312
|
|
|
2183
2313
|
// src/storage/task-index.ts
|
|
2184
2314
|
function taskIndexPath(paths) {
|
|
2185
|
-
return
|
|
2315
|
+
return join10(paths.tasks, "index.json");
|
|
2186
2316
|
}
|
|
2187
2317
|
async function readTaskIndex(paths) {
|
|
2188
2318
|
const filePath = taskIndexPath(paths);
|
|
2189
2319
|
let raw;
|
|
2190
2320
|
try {
|
|
2191
|
-
raw = await
|
|
2321
|
+
raw = await readFile6(filePath, "utf8");
|
|
2192
2322
|
} catch (error) {
|
|
2193
2323
|
if (findErrorCode(error, "ENOENT")) {
|
|
2194
2324
|
throw new Error("Task index not found", { cause: error });
|
|
@@ -2296,10 +2426,10 @@ function splitFrontMatter(raw) {
|
|
|
2296
2426
|
return { yamlText, body };
|
|
2297
2427
|
}
|
|
2298
2428
|
async function readTaskFile(paths, taskId) {
|
|
2299
|
-
const filePath =
|
|
2429
|
+
const filePath = join11(paths.tasks, `${taskId}.md`);
|
|
2300
2430
|
let raw;
|
|
2301
2431
|
try {
|
|
2302
|
-
raw = await
|
|
2432
|
+
raw = await readFile7(filePath, "utf8");
|
|
2303
2433
|
} catch (error) {
|
|
2304
2434
|
if (findErrorCode(error, "ENOENT")) {
|
|
2305
2435
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2329,7 +2459,7 @@ async function readTaskFile(paths, taskId) {
|
|
|
2329
2459
|
}
|
|
2330
2460
|
async function writeTaskFile(paths, taskId, doc, options) {
|
|
2331
2461
|
const validated = TaskSchema.parse(doc.task);
|
|
2332
|
-
const filePath =
|
|
2462
|
+
const filePath = join11(paths.tasks, `${taskId}.md`);
|
|
2333
2463
|
const yamlText = stringifyYaml(validated);
|
|
2334
2464
|
const trimmedBody = doc.body.length === 0 ? "" : `
|
|
2335
2465
|
${doc.body.endsWith("\n") ? doc.body : `${doc.body}
|
|
@@ -2414,7 +2544,7 @@ async function safeUpdateTaskIndex(paths, op) {
|
|
|
2414
2544
|
}
|
|
2415
2545
|
var ARCHIVE_DIR_NAME = "archive";
|
|
2416
2546
|
function archiveTasksDir(paths) {
|
|
2417
|
-
return
|
|
2547
|
+
return join11(paths.tasks, ARCHIVE_DIR_NAME);
|
|
2418
2548
|
}
|
|
2419
2549
|
async function enumerateArchivedTaskIds(paths) {
|
|
2420
2550
|
let entries;
|
|
@@ -2444,10 +2574,10 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
|
|
|
2444
2574
|
throw error;
|
|
2445
2575
|
}
|
|
2446
2576
|
}
|
|
2447
|
-
const archiveFilePath =
|
|
2577
|
+
const archiveFilePath = join11(archiveTasksDir(paths), `${taskId}.md`);
|
|
2448
2578
|
let raw;
|
|
2449
2579
|
try {
|
|
2450
|
-
raw = await
|
|
2580
|
+
raw = await readFile7(archiveFilePath, "utf8");
|
|
2451
2581
|
} catch (error) {
|
|
2452
2582
|
if (findErrorCode(error, "ENOENT")) {
|
|
2453
2583
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2738,7 +2868,7 @@ async function createTaskAttachLocked(input) {
|
|
|
2738
2868
|
...sessionDoc,
|
|
2739
2869
|
session: { ...sessionDoc.session, task_id: input.taskId }
|
|
2740
2870
|
};
|
|
2741
|
-
await overwriteYamlFile(
|
|
2871
|
+
await overwriteYamlFile(join11(input.paths.sessions, input.sessionId, "session.yaml"), updated);
|
|
2742
2872
|
} catch (error) {
|
|
2743
2873
|
throw new TaskWriteAfterEventError({
|
|
2744
2874
|
taskId: input.taskId,
|
|
@@ -2997,17 +3127,17 @@ function buildUpdatedDoc(input) {
|
|
|
2997
3127
|
return { task: next, body: input.currentDoc.body };
|
|
2998
3128
|
}
|
|
2999
3129
|
async function computeTaskMdSnapshot(paths, taskId) {
|
|
3000
|
-
const filePath =
|
|
3001
|
-
const [stats, raw] = await Promise.all([stat2(filePath),
|
|
3130
|
+
const filePath = join11(paths.tasks, `${taskId}.md`);
|
|
3131
|
+
const [stats, raw] = await Promise.all([stat2(filePath), readFile7(filePath)]);
|
|
3002
3132
|
const hash = createHash2("sha256").update(raw).digest("hex");
|
|
3003
3133
|
return { mtimeMs: stats.mtimeMs, hash };
|
|
3004
3134
|
}
|
|
3005
3135
|
async function readTaskFileWithSnapshot(paths, taskId) {
|
|
3006
|
-
const filePath =
|
|
3136
|
+
const filePath = join11(paths.tasks, `${taskId}.md`);
|
|
3007
3137
|
let rawBuffer;
|
|
3008
3138
|
let stats;
|
|
3009
3139
|
try {
|
|
3010
|
-
[rawBuffer, stats] = await Promise.all([
|
|
3140
|
+
[rawBuffer, stats] = await Promise.all([readFile7(filePath), stat2(filePath)]);
|
|
3011
3141
|
} catch (error) {
|
|
3012
3142
|
if (findErrorCode(error, "ENOENT")) {
|
|
3013
3143
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -3495,7 +3625,7 @@ async function deleteTaskLocked(input) {
|
|
|
3495
3625
|
});
|
|
3496
3626
|
const eventId = adHoc.targetEventIds[0];
|
|
3497
3627
|
try {
|
|
3498
|
-
await unlink3(
|
|
3628
|
+
await unlink3(join11(input.paths.tasks, `${input.taskId}.md`));
|
|
3499
3629
|
} catch (error) {
|
|
3500
3630
|
throw new TaskWriteAfterEventError({
|
|
3501
3631
|
taskId: input.taskId,
|
|
@@ -3567,8 +3697,8 @@ async function archiveTaskLocked(input) {
|
|
|
3567
3697
|
);
|
|
3568
3698
|
await mkdir3(archiveTasksDir(input.paths), { recursive: true });
|
|
3569
3699
|
await rename2(
|
|
3570
|
-
|
|
3571
|
-
|
|
3700
|
+
join11(input.paths.tasks, `${input.taskId}.md`),
|
|
3701
|
+
join11(archiveTasksDir(input.paths), `${input.taskId}.md`)
|
|
3572
3702
|
);
|
|
3573
3703
|
} catch (error) {
|
|
3574
3704
|
throw new TaskWriteAfterEventError({
|
|
@@ -3604,7 +3734,7 @@ async function renderHandoff(input) {
|
|
|
3604
3734
|
const tasksCreated = [];
|
|
3605
3735
|
const tasksStatusChanged = [];
|
|
3606
3736
|
for (const entry of entries) {
|
|
3607
|
-
const sessionDir =
|
|
3737
|
+
const sessionDir = join12(input.paths.sessions, entry.sessionId);
|
|
3608
3738
|
try {
|
|
3609
3739
|
for await (const ev of replayEvents(sessionDir, {
|
|
3610
3740
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3895,6 +4025,17 @@ function parseDuration(input) {
|
|
|
3895
4025
|
return ms;
|
|
3896
4026
|
}
|
|
3897
4027
|
|
|
4028
|
+
// src/lib/format-duration.ts
|
|
4029
|
+
function formatDurationMs(ms) {
|
|
4030
|
+
const totalSeconds = Math.round(ms / 1e3);
|
|
4031
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
4032
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
4033
|
+
const seconds = totalSeconds % 60;
|
|
4034
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
4035
|
+
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
4036
|
+
return `${seconds}s`;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
3898
4039
|
// src/lib/id-resolver.ts
|
|
3899
4040
|
async function resolveSessionId(paths, input) {
|
|
3900
4041
|
return resolveIdInternal(paths, input, "session");
|
|
@@ -3950,352 +4091,61 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
|
|
|
3950
4091
|
return matches[0];
|
|
3951
4092
|
}
|
|
3952
4093
|
|
|
3953
|
-
// src/
|
|
3954
|
-
import {
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
4094
|
+
// src/report/report-renderer.ts
|
|
4095
|
+
import { join as join14 } from "path";
|
|
4096
|
+
|
|
4097
|
+
// src/stats/work-stats.ts
|
|
4098
|
+
import { join as join13 } from "path";
|
|
4099
|
+
function resolveTimeZone(timeZone) {
|
|
4100
|
+
if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
|
|
4101
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
4102
|
+
}
|
|
4103
|
+
var STATUS_ORDER = [
|
|
4104
|
+
"completed",
|
|
4105
|
+
"failed",
|
|
4106
|
+
"running",
|
|
4107
|
+
"interrupted",
|
|
4108
|
+
"waiting_approval",
|
|
4109
|
+
"initialized",
|
|
4110
|
+
"imported",
|
|
4111
|
+
"archived"
|
|
4112
|
+
];
|
|
4113
|
+
async function computeWorkStats(input) {
|
|
4114
|
+
const { now } = input;
|
|
4115
|
+
const timeZone = resolveTimeZone(input.timeZone);
|
|
4116
|
+
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
4117
|
+
const wrappedSkip = (sid, reason) => {
|
|
4118
|
+
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
4119
|
+
input.onSessionSkip?.(sid, reason);
|
|
4120
|
+
};
|
|
4121
|
+
const loadOpts = { now, onSkip: wrappedSkip };
|
|
4122
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
4123
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
4124
|
+
const sessions = [];
|
|
4125
|
+
for (const entry of entries) {
|
|
4126
|
+
const events = [];
|
|
4127
|
+
let eventsUnreadable = false;
|
|
3970
4128
|
try {
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
shell: false,
|
|
3976
|
-
detached: false
|
|
3977
|
-
});
|
|
3978
|
-
} catch (error) {
|
|
3979
|
-
throw classifySpawnError(error);
|
|
3980
|
-
}
|
|
3981
|
-
if (options.onSpawn) {
|
|
3982
|
-
try {
|
|
3983
|
-
options.onSpawn(child);
|
|
3984
|
-
} catch {
|
|
4129
|
+
for await (const ev of replayEvents(join13(input.paths.sessions, entry.sessionId), {
|
|
4130
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4131
|
+
})) {
|
|
4132
|
+
events.push(ev);
|
|
3985
4133
|
}
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
let settled = false;
|
|
3991
|
-
const triggerKill = () => {
|
|
3992
|
-
if (killed || child.exitCode !== null) return;
|
|
3993
|
-
killed = true;
|
|
3994
|
-
child.kill("SIGTERM");
|
|
3995
|
-
killTimer = setTimeout(() => {
|
|
3996
|
-
if (child.exitCode === null) {
|
|
3997
|
-
child.kill("SIGKILL");
|
|
3998
|
-
}
|
|
3999
|
-
}, DEFAULT_KILL_GRACE_MS);
|
|
4000
|
-
};
|
|
4001
|
-
const onAbort = () => {
|
|
4002
|
-
triggerKill();
|
|
4003
|
-
};
|
|
4004
|
-
options.signal?.addEventListener("abort", onAbort);
|
|
4005
|
-
if (options.signal?.aborted) {
|
|
4006
|
-
triggerKill();
|
|
4007
|
-
}
|
|
4008
|
-
let stdout = "";
|
|
4009
|
-
let stderr = "";
|
|
4010
|
-
if (captureMode === "buffer") {
|
|
4011
|
-
child.stdout?.setEncoding("utf8");
|
|
4012
|
-
child.stderr?.setEncoding("utf8");
|
|
4013
|
-
child.stdout?.on("data", (chunk) => {
|
|
4014
|
-
stdout += chunk;
|
|
4015
|
-
});
|
|
4016
|
-
child.stderr?.on("data", (chunk) => {
|
|
4017
|
-
stderr += chunk;
|
|
4018
|
-
});
|
|
4019
|
-
if (options.stdin !== void 0) {
|
|
4020
|
-
child.stdin?.end(options.stdin);
|
|
4021
|
-
} else {
|
|
4022
|
-
child.stdin?.end();
|
|
4134
|
+
} catch {
|
|
4135
|
+
eventsUnreadable = true;
|
|
4136
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4137
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4023
4138
|
}
|
|
4024
4139
|
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
child.once("error", (error) => {
|
|
4035
|
-
if (settled) return;
|
|
4036
|
-
settled = true;
|
|
4037
|
-
cleanup();
|
|
4038
|
-
reject(classifySpawnError(error));
|
|
4039
|
-
});
|
|
4040
|
-
child.once("close", (code, signal) => {
|
|
4041
|
-
if (settled) return;
|
|
4042
|
-
settled = true;
|
|
4043
|
-
cleanup();
|
|
4044
|
-
const ended_at = /* @__PURE__ */ new Date();
|
|
4045
|
-
resolve2({
|
|
4046
|
-
command: snapshotCommand,
|
|
4047
|
-
args: snapshotArgs,
|
|
4048
|
-
cwd: snapshotCwd,
|
|
4049
|
-
exit_code: code,
|
|
4050
|
-
signal,
|
|
4051
|
-
stdout,
|
|
4052
|
-
stderr,
|
|
4053
|
-
started_at: started_at.toISOString(),
|
|
4054
|
-
ended_at: ended_at.toISOString(),
|
|
4055
|
-
duration_ms: ended_at.getTime() - started_at.getTime(),
|
|
4056
|
-
pid: child.pid ?? null
|
|
4057
|
-
});
|
|
4058
|
-
});
|
|
4059
|
-
});
|
|
4060
|
-
}
|
|
4061
|
-
};
|
|
4062
|
-
function validateOptions(options) {
|
|
4063
|
-
if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
|
|
4064
|
-
throw new Error("Invalid timeout_ms");
|
|
4065
|
-
}
|
|
4066
|
-
if (options.capture === "none" && options.stdin !== void 0) {
|
|
4067
|
-
throw new Error('Combination of capture: "none" and stdin is not supported');
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
function classifySpawnError(error) {
|
|
4071
|
-
if (findErrorCode(error, "ENOENT")) {
|
|
4072
|
-
return new Error("Command not found", { cause: error });
|
|
4073
|
-
}
|
|
4074
|
-
return new Error("Failed to spawn child process", { cause: error });
|
|
4075
|
-
}
|
|
4076
|
-
|
|
4077
|
-
// src/schemas/json-schema.ts
|
|
4078
|
-
import { z as z11 } from "zod";
|
|
4079
|
-
|
|
4080
|
-
// src/schemas/manifest.schema.ts
|
|
4081
|
-
import { z as z9 } from "zod";
|
|
4082
|
-
var ProjectSchema = z9.object({
|
|
4083
|
-
name: z9.string().optional(),
|
|
4084
|
-
description: z9.string().optional(),
|
|
4085
|
-
repository_url: z9.string().nullable().optional()
|
|
4086
|
-
});
|
|
4087
|
-
var CapabilitiesSchema = z9.object({
|
|
4088
|
-
enabled: z9.array(z9.string())
|
|
4089
|
-
});
|
|
4090
|
-
var ApprovalConfigSchema = z9.object({
|
|
4091
|
-
required_for: z9.array(z9.string()).optional(),
|
|
4092
|
-
default_risk_level: z9.enum(["low", "medium", "high", "critical"])
|
|
4093
|
-
});
|
|
4094
|
-
var ClaudeCodeAdapterConfigSchema = z9.object({
|
|
4095
|
-
enabled: z9.boolean(),
|
|
4096
|
-
config_path: z9.string().optional()
|
|
4097
|
-
});
|
|
4098
|
-
var AdaptersSchema = z9.object({
|
|
4099
|
-
"claude-code": ClaudeCodeAdapterConfigSchema
|
|
4100
|
-
});
|
|
4101
|
-
var GitConfigSchema = z9.object({
|
|
4102
|
-
events_log: z9.enum(["ignore", "commit"]).default("ignore")
|
|
4103
|
-
});
|
|
4104
|
-
var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
|
|
4105
|
-
var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
|
|
4106
|
-
message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
|
|
4107
|
-
});
|
|
4108
|
-
var ImportConfigSchema = z9.object({
|
|
4109
|
-
source_roots: z9.array(SourceRootSchema).min(1).optional()
|
|
4110
|
-
});
|
|
4111
|
-
var WorkspaceMetaSchema = z9.object({
|
|
4112
|
-
id: WorkspaceIdSchema,
|
|
4113
|
-
name: z9.string().min(1),
|
|
4114
|
-
created_at: IsoTimestampSchema,
|
|
4115
|
-
updated_at: IsoTimestampSchema
|
|
4116
|
-
});
|
|
4117
|
-
var ManifestSchema = z9.object({
|
|
4118
|
-
schema_version: SchemaVersionSchema,
|
|
4119
|
-
basou_version: z9.literal("0.1.0"),
|
|
4120
|
-
workspace: WorkspaceMetaSchema,
|
|
4121
|
-
project: ProjectSchema,
|
|
4122
|
-
capabilities: CapabilitiesSchema,
|
|
4123
|
-
approval: ApprovalConfigSchema,
|
|
4124
|
-
adapters: AdaptersSchema,
|
|
4125
|
-
git: GitConfigSchema,
|
|
4126
|
-
import: ImportConfigSchema.optional()
|
|
4127
|
-
});
|
|
4128
|
-
|
|
4129
|
-
// src/schemas/session-import.schema.ts
|
|
4130
|
-
import { z as z10 } from "zod";
|
|
4131
|
-
var SessionInnerImportSchema = z10.object({
|
|
4132
|
-
id: SessionIdSchema.optional(),
|
|
4133
|
-
label: z10.string().optional(),
|
|
4134
|
-
task_id: TaskIdSchema.nullable().optional(),
|
|
4135
|
-
workspace_id: WorkspaceIdSchema,
|
|
4136
|
-
source: z10.object({
|
|
4137
|
-
kind: SessionSourceKindSchema,
|
|
4138
|
-
version: z10.literal("0.1.0"),
|
|
4139
|
-
// Source-tool-native id (e.g. Claude Code session UUID), retained so
|
|
4140
|
-
// re-imports of the same source can be deduplicated.
|
|
4141
|
-
external_id: z10.string().optional(),
|
|
4142
|
-
// Byte size of the source native log at import time. Declared here too
|
|
4143
|
-
// (not only in session.schema.ts) because this inner `source` object is
|
|
4144
|
-
// a plain z.object: zod strips keys it does not declare, so a field
|
|
4145
|
-
// absent here would be dropped from the parsed payload before persist
|
|
4146
|
-
// and the size could never be stored.
|
|
4147
|
-
source_size_bytes: z10.number().int().nonnegative().optional()
|
|
4148
|
-
}),
|
|
4149
|
-
started_at: IsoTimestampSchema,
|
|
4150
|
-
ended_at: IsoTimestampSchema.optional(),
|
|
4151
|
-
status: SessionStatusSchema,
|
|
4152
|
-
working_directory: z10.string().min(1),
|
|
4153
|
-
invocation: z10.object({
|
|
4154
|
-
command: z10.string().min(1),
|
|
4155
|
-
args: z10.array(z10.string()),
|
|
4156
|
-
exit_code: z10.number().int().nullable()
|
|
4157
|
-
}),
|
|
4158
|
-
related_files: z10.array(z10.string()).default([]),
|
|
4159
|
-
events_log: z10.string().optional(),
|
|
4160
|
-
summary: z10.string().nullable().optional(),
|
|
4161
|
-
metrics: SessionMetricsSchema.optional(),
|
|
4162
|
-
// Accepted so a payload assembled from an on-disk chained session.yaml
|
|
4163
|
-
// round-trips, and DISCARDED by the importer (buildSessionRecord never
|
|
4164
|
-
// copies it): the integrity anchor is computed at write time, never
|
|
4165
|
-
// imported. Mirrors the accept-and-discard of `prev_hash` on events.
|
|
4166
|
-
integrity: SessionIntegritySchema.optional()
|
|
4167
|
-
}).strict();
|
|
4168
|
-
var SessionImportPayloadSchema = z10.object({
|
|
4169
|
-
schema_version: z10.string(),
|
|
4170
|
-
session: SessionInnerImportSchema,
|
|
4171
|
-
events: z10.array(EventSchema)
|
|
4172
|
-
}).strict();
|
|
4173
|
-
|
|
4174
|
-
// src/schemas/json-schema.ts
|
|
4175
|
-
var JSON_SCHEMA_VERSION = "0.1.0";
|
|
4176
|
-
var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
|
|
4177
|
-
var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
4178
|
-
var DOCUMENTS = [
|
|
4179
|
-
{
|
|
4180
|
-
name: "manifest",
|
|
4181
|
-
schema: ManifestSchema,
|
|
4182
|
-
title: "Basou Manifest",
|
|
4183
|
-
description: "The `.basou/manifest.yaml` workspace manifest."
|
|
4184
|
-
},
|
|
4185
|
-
{
|
|
4186
|
-
name: "session",
|
|
4187
|
-
schema: SessionSchema,
|
|
4188
|
-
title: "Basou Session",
|
|
4189
|
-
description: "A `.basou/sessions/<id>/session.yaml` session record."
|
|
4190
|
-
},
|
|
4191
|
-
{
|
|
4192
|
-
name: "event",
|
|
4193
|
-
schema: EventSchema,
|
|
4194
|
-
title: "Basou Event",
|
|
4195
|
-
description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
|
|
4196
|
-
},
|
|
4197
|
-
{
|
|
4198
|
-
name: "task",
|
|
4199
|
-
schema: TaskSchema,
|
|
4200
|
-
title: "Basou Task",
|
|
4201
|
-
description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
|
|
4202
|
-
},
|
|
4203
|
-
{
|
|
4204
|
-
name: "approval",
|
|
4205
|
-
schema: ApprovalSchema,
|
|
4206
|
-
title: "Basou Approval",
|
|
4207
|
-
description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
|
|
4208
|
-
},
|
|
4209
|
-
{
|
|
4210
|
-
name: "status",
|
|
4211
|
-
schema: StatusSchema,
|
|
4212
|
-
title: "Basou Status",
|
|
4213
|
-
description: "The `.basou/status.json` workspace status snapshot."
|
|
4214
|
-
},
|
|
4215
|
-
{
|
|
4216
|
-
name: "task-index",
|
|
4217
|
-
schema: TaskIndexSchema,
|
|
4218
|
-
title: "Basou Task Index",
|
|
4219
|
-
description: "The `.basou/tasks/index.json` task lookup index."
|
|
4220
|
-
},
|
|
4221
|
-
{
|
|
4222
|
-
name: "session-import",
|
|
4223
|
-
schema: SessionImportPayloadSchema,
|
|
4224
|
-
title: "Basou Session Import Payload",
|
|
4225
|
-
description: "The portable session payload consumed by `basou session import`."
|
|
4226
|
-
}
|
|
4227
|
-
];
|
|
4228
|
-
function buildJsonSchemas() {
|
|
4229
|
-
return DOCUMENTS.map((doc) => {
|
|
4230
|
-
const generated = z11.toJSONSchema(doc.schema, { io: "input" });
|
|
4231
|
-
const { $schema, ...rest } = generated;
|
|
4232
|
-
const schema = {
|
|
4233
|
-
$schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
|
|
4234
|
-
$id: `${ID_BASE}/${doc.name}.schema.json`,
|
|
4235
|
-
title: doc.title,
|
|
4236
|
-
description: doc.description,
|
|
4237
|
-
...rest
|
|
4238
|
-
};
|
|
4239
|
-
return { name: doc.name, schema };
|
|
4240
|
-
});
|
|
4241
|
-
}
|
|
4242
|
-
function serializeJsonSchema(schema) {
|
|
4243
|
-
return `${JSON.stringify(schema, null, 2)}
|
|
4244
|
-
`;
|
|
4245
|
-
}
|
|
4246
|
-
|
|
4247
|
-
// src/stats/work-stats.ts
|
|
4248
|
-
import { join as join12 } from "path";
|
|
4249
|
-
function resolveTimeZone(timeZone) {
|
|
4250
|
-
if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
|
|
4251
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
4252
|
-
}
|
|
4253
|
-
var STATUS_ORDER = [
|
|
4254
|
-
"completed",
|
|
4255
|
-
"failed",
|
|
4256
|
-
"running",
|
|
4257
|
-
"interrupted",
|
|
4258
|
-
"waiting_approval",
|
|
4259
|
-
"initialized",
|
|
4260
|
-
"imported",
|
|
4261
|
-
"archived"
|
|
4262
|
-
];
|
|
4263
|
-
async function computeWorkStats(input) {
|
|
4264
|
-
const { now } = input;
|
|
4265
|
-
const timeZone = resolveTimeZone(input.timeZone);
|
|
4266
|
-
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
4267
|
-
const wrappedSkip = (sid, reason) => {
|
|
4268
|
-
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
4269
|
-
input.onSessionSkip?.(sid, reason);
|
|
4270
|
-
};
|
|
4271
|
-
const loadOpts = { now, onSkip: wrappedSkip };
|
|
4272
|
-
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
4273
|
-
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
4274
|
-
const sessions = [];
|
|
4275
|
-
for (const entry of entries) {
|
|
4276
|
-
const events = [];
|
|
4277
|
-
let eventsUnreadable = false;
|
|
4278
|
-
try {
|
|
4279
|
-
for await (const ev of replayEvents(join12(input.paths.sessions, entry.sessionId), {
|
|
4280
|
-
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4281
|
-
})) {
|
|
4282
|
-
events.push(ev);
|
|
4283
|
-
}
|
|
4284
|
-
} catch {
|
|
4285
|
-
eventsUnreadable = true;
|
|
4286
|
-
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4287
|
-
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4288
|
-
}
|
|
4289
|
-
}
|
|
4290
|
-
sessions.push(
|
|
4291
|
-
sessionWorkStatsFromEvents(
|
|
4292
|
-
entry.sessionId,
|
|
4293
|
-
entry.session.session,
|
|
4294
|
-
events,
|
|
4295
|
-
now,
|
|
4296
|
-
eventsUnreadable
|
|
4297
|
-
)
|
|
4298
|
-
);
|
|
4140
|
+
sessions.push(
|
|
4141
|
+
sessionWorkStatsFromEvents(
|
|
4142
|
+
entry.sessionId,
|
|
4143
|
+
entry.session.session,
|
|
4144
|
+
events,
|
|
4145
|
+
now,
|
|
4146
|
+
eventsUnreadable
|
|
4147
|
+
)
|
|
4148
|
+
);
|
|
4299
4149
|
}
|
|
4300
4150
|
const allIntervals = [];
|
|
4301
4151
|
for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
|
|
@@ -4474,85 +4324,690 @@ function computeBySource(sessions) {
|
|
|
4474
4324
|
if (s.availability.tokens) row.tokensAvailable = true;
|
|
4475
4325
|
if (s.availability.machineActive) row.machineActiveAvailable = true;
|
|
4476
4326
|
}
|
|
4477
|
-
return [...map.values()].sort((a, b) => a.sourceKind.localeCompare(b.sourceKind));
|
|
4478
|
-
}
|
|
4479
|
-
function computeByStatus(sessions) {
|
|
4480
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4481
|
-
for (const s of sessions) counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
|
|
4482
|
-
const ordered = [];
|
|
4483
|
-
for (const status of STATUS_ORDER) {
|
|
4484
|
-
const count = counts.get(status);
|
|
4485
|
-
if (count !== void 0 && count > 0) ordered.push({ status, count });
|
|
4327
|
+
return [...map.values()].sort((a, b) => a.sourceKind.localeCompare(b.sourceKind));
|
|
4328
|
+
}
|
|
4329
|
+
function computeByStatus(sessions) {
|
|
4330
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4331
|
+
for (const s of sessions) counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
|
|
4332
|
+
const ordered = [];
|
|
4333
|
+
for (const status of STATUS_ORDER) {
|
|
4334
|
+
const count = counts.get(status);
|
|
4335
|
+
if (count !== void 0 && count > 0) ordered.push({ status, count });
|
|
4336
|
+
}
|
|
4337
|
+
return ordered;
|
|
4338
|
+
}
|
|
4339
|
+
function computeByDay(sessions, unionMerged, timeZone) {
|
|
4340
|
+
const days = /* @__PURE__ */ new Map();
|
|
4341
|
+
const ensure = (date) => {
|
|
4342
|
+
let day = days.get(date);
|
|
4343
|
+
if (day === void 0) {
|
|
4344
|
+
day = {
|
|
4345
|
+
date,
|
|
4346
|
+
billableActiveTimeMs: 0,
|
|
4347
|
+
machineActiveTimeMs: 0,
|
|
4348
|
+
sessionCount: 0,
|
|
4349
|
+
commandCount: 0,
|
|
4350
|
+
fileChangedCount: 0,
|
|
4351
|
+
decisionCount: 0,
|
|
4352
|
+
tokens: emptyTokens()
|
|
4353
|
+
};
|
|
4354
|
+
days.set(date, day);
|
|
4355
|
+
}
|
|
4356
|
+
return day;
|
|
4357
|
+
};
|
|
4358
|
+
for (const [start, end] of unionMerged) {
|
|
4359
|
+
ensure(tzDate(start, timeZone)).billableActiveTimeMs += end - start;
|
|
4360
|
+
}
|
|
4361
|
+
for (const s of sessions) {
|
|
4362
|
+
const startedMs = Date.parse(s.startedAt);
|
|
4363
|
+
if (!Number.isFinite(startedMs)) continue;
|
|
4364
|
+
const day = ensure(tzDate(startedMs, timeZone));
|
|
4365
|
+
day.sessionCount++;
|
|
4366
|
+
day.machineActiveTimeMs += s.machineActiveTimeMs;
|
|
4367
|
+
day.commandCount += s.commandCount;
|
|
4368
|
+
day.fileChangedCount += s.fileChangedCount;
|
|
4369
|
+
day.decisionCount += s.decisionCount;
|
|
4370
|
+
addTokens(day.tokens, s.tokens);
|
|
4371
|
+
}
|
|
4372
|
+
return [...days.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
4373
|
+
}
|
|
4374
|
+
function tzDate(ms, timeZone) {
|
|
4375
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
4376
|
+
timeZone,
|
|
4377
|
+
year: "numeric",
|
|
4378
|
+
month: "2-digit",
|
|
4379
|
+
day: "2-digit"
|
|
4380
|
+
}).format(new Date(ms));
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// src/report/report-renderer.ts
|
|
4384
|
+
var CHANGED_FILES_MARKDOWN_LIMIT = 50;
|
|
4385
|
+
var DECISIONS_MARKDOWN_LIMIT = 20;
|
|
4386
|
+
var SESSIONS_MARKDOWN_LIMIT = 30;
|
|
4387
|
+
var TASKS_MARKDOWN_LIMIT = 30;
|
|
4388
|
+
var APPROVALS_MARKDOWN_LIMIT = 30;
|
|
4389
|
+
var SESSION_STATUS_ORDER = [
|
|
4390
|
+
"completed",
|
|
4391
|
+
"failed",
|
|
4392
|
+
"running",
|
|
4393
|
+
"waiting_approval",
|
|
4394
|
+
"interrupted",
|
|
4395
|
+
"initialized",
|
|
4396
|
+
"imported",
|
|
4397
|
+
"archived"
|
|
4398
|
+
];
|
|
4399
|
+
var TASK_STATUS_ORDER = ["planned", "in_progress", "done", "cancelled"];
|
|
4400
|
+
async function renderReport(input) {
|
|
4401
|
+
const now = new Date(input.nowIso);
|
|
4402
|
+
const unreadableEmitted = /* @__PURE__ */ new Set();
|
|
4403
|
+
const wrappedSkip = (sid, reason) => {
|
|
4404
|
+
if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
|
|
4405
|
+
input.onSessionSkip?.(sid, reason);
|
|
4406
|
+
};
|
|
4407
|
+
const loadOpts = { now, onSkip: wrappedSkip };
|
|
4408
|
+
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
4409
|
+
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
4410
|
+
const statsInput = { paths: input.paths, now };
|
|
4411
|
+
if (input.timeZone !== void 0) statsInput.timeZone = input.timeZone;
|
|
4412
|
+
const stats = await computeWorkStats(statsInput);
|
|
4413
|
+
const statsBySession = new Map(stats.sessions.map((s) => [s.sessionId, s]));
|
|
4414
|
+
const decisions = [];
|
|
4415
|
+
for (const entry of entries) {
|
|
4416
|
+
const sessionDir = join14(input.paths.sessions, entry.sessionId);
|
|
4417
|
+
try {
|
|
4418
|
+
for await (const ev of replayEvents(sessionDir, {
|
|
4419
|
+
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4420
|
+
})) {
|
|
4421
|
+
if (ev.type === "decision_recorded") {
|
|
4422
|
+
decisions.push({ id: ev.decision_id, title: ev.title, occurredAt: ev.occurred_at });
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
} catch {
|
|
4426
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4427
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
decisions.sort((a, b) => {
|
|
4432
|
+
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
4433
|
+
return c !== 0 ? c : a.id.localeCompare(b.id);
|
|
4434
|
+
});
|
|
4435
|
+
const taskLoadOpts = {};
|
|
4436
|
+
if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
|
|
4437
|
+
const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
|
|
4438
|
+
const taskItems = taskEntries.map((t2) => ({
|
|
4439
|
+
id: t2.task.task.id,
|
|
4440
|
+
title: t2.task.task.title,
|
|
4441
|
+
status: t2.task.task.status
|
|
4442
|
+
}));
|
|
4443
|
+
const tasksByStatus = tallyTaskStatus(taskItems);
|
|
4444
|
+
const approvalIds = await enumerateApprovals(input.paths);
|
|
4445
|
+
const resolvedSet = new Set(approvalIds.resolved);
|
|
4446
|
+
const pendingIds = approvalIds.pending.filter((id) => !resolvedSet.has(id));
|
|
4447
|
+
const loadedApprovals = (await Promise.all(
|
|
4448
|
+
[...pendingIds, ...approvalIds.resolved].map((id) => loadApproval(input.paths, id))
|
|
4449
|
+
)).filter((a) => a !== null);
|
|
4450
|
+
const approvalItems = loadedApprovals.map((a) => ({
|
|
4451
|
+
id: a.approval.id,
|
|
4452
|
+
reason: a.approval.reason,
|
|
4453
|
+
status: a.approval.status,
|
|
4454
|
+
riskLevel: a.approval.risk_level
|
|
4455
|
+
}));
|
|
4456
|
+
const approvalCounts = { pending: 0, approved: 0, rejected: 0, expired: 0 };
|
|
4457
|
+
for (const a of approvalItems) approvalCounts[a.status] += 1;
|
|
4458
|
+
const changedSet = /* @__PURE__ */ new Set();
|
|
4459
|
+
for (const entry of entries) {
|
|
4460
|
+
if (entry.session.session.source.kind === "import") continue;
|
|
4461
|
+
for (const f of entry.session.session.related_files) changedSet.add(f);
|
|
4462
|
+
}
|
|
4463
|
+
const changedFiles = [...changedSet].sort();
|
|
4464
|
+
const integrity = {
|
|
4465
|
+
total: 0,
|
|
4466
|
+
verified: 0,
|
|
4467
|
+
unchained: 0,
|
|
4468
|
+
empty: 0,
|
|
4469
|
+
incomplete: 0,
|
|
4470
|
+
in_progress: 0,
|
|
4471
|
+
tampered: 0,
|
|
4472
|
+
tamperedSessions: []
|
|
4473
|
+
};
|
|
4474
|
+
for (const entry of entries) {
|
|
4475
|
+
const verdict = await verifyEventsChain(input.paths, entry.sessionId).catch(() => null);
|
|
4476
|
+
if (verdict === null) {
|
|
4477
|
+
if (!unreadableEmitted.has(entry.sessionId)) {
|
|
4478
|
+
wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
|
|
4479
|
+
}
|
|
4480
|
+
continue;
|
|
4481
|
+
}
|
|
4482
|
+
integrity.total += 1;
|
|
4483
|
+
integrity[verdict.status] += 1;
|
|
4484
|
+
if (verdict.status === "tampered") integrity.tamperedSessions.push(entry.sessionId);
|
|
4485
|
+
}
|
|
4486
|
+
const sessionItems = [...entries].sort(
|
|
4487
|
+
(a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
|
|
4488
|
+
).map((e) => {
|
|
4489
|
+
const w = statsBySession.get(e.sessionId);
|
|
4490
|
+
return {
|
|
4491
|
+
id: e.sessionId,
|
|
4492
|
+
label: e.session.session.label ?? null,
|
|
4493
|
+
status: e.session.session.status,
|
|
4494
|
+
source: e.session.session.source.kind,
|
|
4495
|
+
startedAt: e.session.session.started_at,
|
|
4496
|
+
activeMs: w?.activeTimeMs ?? 0,
|
|
4497
|
+
outputTokens: w?.tokens.output ?? 0
|
|
4498
|
+
};
|
|
4499
|
+
});
|
|
4500
|
+
const period = computePeriod(entries, input.nowIso);
|
|
4501
|
+
const t = stats.totals;
|
|
4502
|
+
const data = {
|
|
4503
|
+
generatedAt: input.nowIso,
|
|
4504
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
4505
|
+
period,
|
|
4506
|
+
sessions: { total: entries.length, byStatus: stats.byStatus, items: sessionItems },
|
|
4507
|
+
volume: {
|
|
4508
|
+
outputTokens: t.tokens.output,
|
|
4509
|
+
reasoningTokens: t.tokens.reasoning,
|
|
4510
|
+
commandCount: t.commandCount,
|
|
4511
|
+
fileChangedCount: t.fileChangedCount,
|
|
4512
|
+
decisionCount: t.decisionCount,
|
|
4513
|
+
tokensAvailable: t.tokensAvailable
|
|
4514
|
+
},
|
|
4515
|
+
time: {
|
|
4516
|
+
activeMs: t.billableActiveTimeMs,
|
|
4517
|
+
machineActiveMs: t.machineActiveTimeMs,
|
|
4518
|
+
machineAvailable: t.machineActiveAvailable,
|
|
4519
|
+
spanMs: t.sessionSpanMs,
|
|
4520
|
+
commandTimeMs: t.commandTimeMs,
|
|
4521
|
+
timeZone: stats.timeZone
|
|
4522
|
+
},
|
|
4523
|
+
decisions: { count: decisions.length, items: decisions },
|
|
4524
|
+
approvals: { ...approvalCounts, items: approvalItems },
|
|
4525
|
+
tasks: { total: taskEntries.length, byStatus: tasksByStatus, items: taskItems },
|
|
4526
|
+
changedFiles,
|
|
4527
|
+
integrity
|
|
4528
|
+
};
|
|
4529
|
+
return { body: formatReportBody(data), data };
|
|
4530
|
+
}
|
|
4531
|
+
function computePeriod(entries, nowIso) {
|
|
4532
|
+
if (entries.length === 0) return { from: null, to: null };
|
|
4533
|
+
let from = entries[0]?.session.session.started_at ?? nowIso;
|
|
4534
|
+
let to = nowIso;
|
|
4535
|
+
let sawEnd = false;
|
|
4536
|
+
for (const e of entries) {
|
|
4537
|
+
const s = e.session.session.started_at;
|
|
4538
|
+
if (Date.parse(s) < Date.parse(from)) from = s;
|
|
4539
|
+
const end = e.session.session.ended_at ?? nowIso;
|
|
4540
|
+
if (!sawEnd || Date.parse(end) > Date.parse(to)) {
|
|
4541
|
+
to = end;
|
|
4542
|
+
sawEnd = true;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
if (Date.parse(to) < Date.parse(from)) to = from;
|
|
4546
|
+
return { from, to };
|
|
4547
|
+
}
|
|
4548
|
+
function tallyTaskStatus(items) {
|
|
4549
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4550
|
+
for (const i of items) counts.set(i.status, (counts.get(i.status) ?? 0) + 1);
|
|
4551
|
+
return TASK_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((status) => ({
|
|
4552
|
+
status,
|
|
4553
|
+
count: counts.get(status)
|
|
4554
|
+
}));
|
|
4555
|
+
}
|
|
4556
|
+
function formatReportBody(data) {
|
|
4557
|
+
const lines = [];
|
|
4558
|
+
const titleSuffix = data.title !== void 0 ? ` \u2014 ${data.title}` : "";
|
|
4559
|
+
lines.push(`# Report${titleSuffix}`);
|
|
4560
|
+
lines.push("");
|
|
4561
|
+
const periodSuffix = data.period.from !== null && data.period.to !== null ? ` (${data.period.from.slice(0, 10)}..${data.period.to.slice(0, 10)})` : "";
|
|
4562
|
+
lines.push(`> Generated at ${data.generatedAt}${periodSuffix}`);
|
|
4563
|
+
lines.push("");
|
|
4564
|
+
lines.push("## \u6982\u8981");
|
|
4565
|
+
lines.push("");
|
|
4566
|
+
lines.push(`- ${formatSessionsLine(data)}`);
|
|
4567
|
+
lines.push(
|
|
4568
|
+
`- Active time ${formatDurationMs(data.time.activeMs)}, ${formatInt(data.volume.outputTokens)} output tokens`
|
|
4569
|
+
);
|
|
4570
|
+
lines.push("");
|
|
4571
|
+
lines.push("## \u4F5C\u696D\u91CF");
|
|
4572
|
+
lines.push("");
|
|
4573
|
+
const tokenCaveat = data.volume.tokensAvailable ? "" : " (no token data captured)";
|
|
4574
|
+
lines.push(`- Output tokens: ${formatInt(data.volume.outputTokens)}${tokenCaveat}`);
|
|
4575
|
+
if (data.volume.reasoningTokens > 0) {
|
|
4576
|
+
lines.push(`- Reasoning tokens: ${formatInt(data.volume.reasoningTokens)} (Codex)`);
|
|
4577
|
+
}
|
|
4578
|
+
lines.push(
|
|
4579
|
+
`- Actions: ${data.volume.commandCount} commands, ${data.volume.fileChangedCount} files, ${data.volume.decisionCount} decisions`
|
|
4580
|
+
);
|
|
4581
|
+
lines.push(
|
|
4582
|
+
`- Active time: ${formatDurationMs(data.time.activeMs)} (union; idle gaps > 5m excluded; tz ${data.time.timeZone})`
|
|
4583
|
+
);
|
|
4584
|
+
if (data.time.machineAvailable) {
|
|
4585
|
+
lines.push(
|
|
4586
|
+
`- Model working: ${formatDurationMs(data.time.machineActiveMs)} (model compute, subset of active)`
|
|
4587
|
+
);
|
|
4588
|
+
}
|
|
4589
|
+
lines.push(`- Span: ${formatDurationMs(data.time.spanMs)} (total elapsed)`);
|
|
4590
|
+
lines.push("");
|
|
4591
|
+
lines.push("## \u5224\u65AD");
|
|
4592
|
+
lines.push("");
|
|
4593
|
+
if (data.decisions.items.length === 0) {
|
|
4594
|
+
lines.push("(no decisions recorded yet)");
|
|
4595
|
+
} else {
|
|
4596
|
+
const total = data.decisions.items.length;
|
|
4597
|
+
const shown = total > DECISIONS_MARKDOWN_LIMIT ? data.decisions.items.slice(-DECISIONS_MARKDOWN_LIMIT) : data.decisions.items;
|
|
4598
|
+
if (total > DECISIONS_MARKDOWN_LIMIT) {
|
|
4599
|
+
lines.push(`(showing the ${DECISIONS_MARKDOWN_LIMIT} most recent of ${total})`);
|
|
4600
|
+
lines.push("");
|
|
4601
|
+
}
|
|
4602
|
+
for (const d of shown) {
|
|
4603
|
+
lines.push(`- ${d.occurredAt.slice(0, 10)} \xB7 ${d.title}`);
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
lines.push("");
|
|
4607
|
+
lines.push("## \u627F\u8A8D");
|
|
4608
|
+
lines.push("");
|
|
4609
|
+
if (data.approvals.items.length === 0) {
|
|
4610
|
+
lines.push("(none)");
|
|
4611
|
+
} else {
|
|
4612
|
+
const a = data.approvals;
|
|
4613
|
+
lines.push(
|
|
4614
|
+
`Pending ${a.pending} \xB7 Approved ${a.approved} \xB7 Rejected ${a.rejected} \xB7 Expired ${a.expired}`
|
|
4615
|
+
);
|
|
4616
|
+
lines.push("");
|
|
4617
|
+
for (const item of data.approvals.items.slice(0, APPROVALS_MARKDOWN_LIMIT)) {
|
|
4618
|
+
lines.push(`- ${item.reason} (${item.status}, ${item.riskLevel})`);
|
|
4619
|
+
}
|
|
4620
|
+
const overflow = data.approvals.items.length - APPROVALS_MARKDOWN_LIMIT;
|
|
4621
|
+
if (overflow > 0) lines.push(`- ... +${overflow} more`);
|
|
4622
|
+
}
|
|
4623
|
+
lines.push("");
|
|
4624
|
+
lines.push("## \u30BF\u30B9\u30AF");
|
|
4625
|
+
lines.push("");
|
|
4626
|
+
if (data.tasks.items.length === 0) {
|
|
4627
|
+
lines.push("(no tasks recorded yet)");
|
|
4628
|
+
} else {
|
|
4629
|
+
const breakdown = data.tasks.byStatus.map((s) => `${s.status} ${s.count}`).join(", ");
|
|
4630
|
+
lines.push(`Tasks: ${data.tasks.total} (${breakdown})`);
|
|
4631
|
+
lines.push("");
|
|
4632
|
+
for (const item of data.tasks.items.slice(0, TASKS_MARKDOWN_LIMIT)) {
|
|
4633
|
+
lines.push(`- ${item.title} (${item.status})`);
|
|
4634
|
+
}
|
|
4635
|
+
const overflow = data.tasks.items.length - TASKS_MARKDOWN_LIMIT;
|
|
4636
|
+
if (overflow > 0) lines.push(`- ... +${overflow} more`);
|
|
4637
|
+
}
|
|
4638
|
+
lines.push("");
|
|
4639
|
+
lines.push("## \u5909\u66F4\u30D5\u30A1\u30A4\u30EB");
|
|
4640
|
+
lines.push("");
|
|
4641
|
+
if (data.changedFiles.length === 0) {
|
|
4642
|
+
lines.push("(no related files recorded)");
|
|
4643
|
+
} else {
|
|
4644
|
+
for (const f of data.changedFiles.slice(0, CHANGED_FILES_MARKDOWN_LIMIT)) lines.push(`- ${f}`);
|
|
4645
|
+
const overflow = data.changedFiles.length - CHANGED_FILES_MARKDOWN_LIMIT;
|
|
4646
|
+
if (overflow > 0) lines.push(`- ... +${overflow} more`);
|
|
4647
|
+
}
|
|
4648
|
+
lines.push("");
|
|
4649
|
+
lines.push("## \u30BB\u30C3\u30B7\u30E7\u30F3\u4E00\u89A7");
|
|
4650
|
+
lines.push("");
|
|
4651
|
+
if (data.sessions.items.length === 0) {
|
|
4652
|
+
lines.push("(no sessions yet)");
|
|
4653
|
+
} else {
|
|
4654
|
+
lines.push("| started_at | source | status | active | out tok |");
|
|
4655
|
+
lines.push("|---|---|---|---|---|");
|
|
4656
|
+
for (const s of data.sessions.items.slice(0, SESSIONS_MARKDOWN_LIMIT)) {
|
|
4657
|
+
lines.push(
|
|
4658
|
+
`| ${s.startedAt} | ${s.source} | ${s.status} | ${formatDurationMs(s.activeMs)} | ${formatInt(s.outputTokens)} |`
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
const overflow = data.sessions.items.length - SESSIONS_MARKDOWN_LIMIT;
|
|
4662
|
+
if (overflow > 0) {
|
|
4663
|
+
lines.push("");
|
|
4664
|
+
lines.push(`... +${overflow} more sessions`);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
lines.push("");
|
|
4668
|
+
lines.push("## \u6574\u5408\u6027");
|
|
4669
|
+
lines.push("");
|
|
4670
|
+
const i = data.integrity;
|
|
4671
|
+
lines.push(
|
|
4672
|
+
`Provenance internally tamper-checked: ${i.verified} verified, ${i.unchained} unchained, ${i.empty} empty, ${i.incomplete} incomplete, ${i.in_progress} in_progress, ${i.tampered} tampered (of ${i.total} sessions).`
|
|
4673
|
+
);
|
|
4674
|
+
lines.push("");
|
|
4675
|
+
lines.push(
|
|
4676
|
+
"This reflects internal consistency of the local event-log hash chain \u2014 not a third-party cryptographic proof."
|
|
4677
|
+
);
|
|
4678
|
+
if (i.tampered > 0) {
|
|
4679
|
+
lines.push("");
|
|
4680
|
+
for (const id of i.tamperedSessions) lines.push(`- Tampered: ${id}`);
|
|
4681
|
+
}
|
|
4682
|
+
return lines.join("\n");
|
|
4683
|
+
}
|
|
4684
|
+
function formatSessionsLine(data) {
|
|
4685
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4686
|
+
for (const s of data.sessions.byStatus) counts.set(s.status, s.count);
|
|
4687
|
+
const breakdown = SESSION_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((s) => `${s} ${counts.get(s)}`).join(", ");
|
|
4688
|
+
return breakdown !== "" ? `Sessions: ${data.sessions.total} (${breakdown})` : `Sessions: ${data.sessions.total}`;
|
|
4689
|
+
}
|
|
4690
|
+
function formatInt(n) {
|
|
4691
|
+
return n.toLocaleString("en-US");
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
// src/runtime/child-process-runner.ts
|
|
4695
|
+
import { spawn as spawn2 } from "child_process";
|
|
4696
|
+
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
4697
|
+
var ChildProcessRunner = class {
|
|
4698
|
+
async run(command, args, options) {
|
|
4699
|
+
validateOptions(options);
|
|
4700
|
+
if (options.signal?.aborted) {
|
|
4701
|
+
throw new Error("Process aborted before spawn", {
|
|
4702
|
+
cause: options.signal.reason
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
const snapshotCommand = command;
|
|
4706
|
+
const snapshotArgs = [...args];
|
|
4707
|
+
const snapshotCwd = options.cwd;
|
|
4708
|
+
const captureMode = options.capture ?? "buffer";
|
|
4709
|
+
const started_at = /* @__PURE__ */ new Date();
|
|
4710
|
+
let child;
|
|
4711
|
+
try {
|
|
4712
|
+
child = spawn2(snapshotCommand, [...snapshotArgs], {
|
|
4713
|
+
cwd: snapshotCwd,
|
|
4714
|
+
env: options.env ?? process.env,
|
|
4715
|
+
stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
|
|
4716
|
+
shell: false,
|
|
4717
|
+
detached: false
|
|
4718
|
+
});
|
|
4719
|
+
} catch (error) {
|
|
4720
|
+
throw classifySpawnError(error);
|
|
4721
|
+
}
|
|
4722
|
+
if (options.onSpawn) {
|
|
4723
|
+
try {
|
|
4724
|
+
options.onSpawn(child);
|
|
4725
|
+
} catch {
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
let timeoutTimer = null;
|
|
4729
|
+
let killTimer = null;
|
|
4730
|
+
let killed = false;
|
|
4731
|
+
let settled = false;
|
|
4732
|
+
const triggerKill = () => {
|
|
4733
|
+
if (killed || child.exitCode !== null) return;
|
|
4734
|
+
killed = true;
|
|
4735
|
+
child.kill("SIGTERM");
|
|
4736
|
+
killTimer = setTimeout(() => {
|
|
4737
|
+
if (child.exitCode === null) {
|
|
4738
|
+
child.kill("SIGKILL");
|
|
4739
|
+
}
|
|
4740
|
+
}, DEFAULT_KILL_GRACE_MS);
|
|
4741
|
+
};
|
|
4742
|
+
const onAbort = () => {
|
|
4743
|
+
triggerKill();
|
|
4744
|
+
};
|
|
4745
|
+
options.signal?.addEventListener("abort", onAbort);
|
|
4746
|
+
if (options.signal?.aborted) {
|
|
4747
|
+
triggerKill();
|
|
4748
|
+
}
|
|
4749
|
+
let stdout = "";
|
|
4750
|
+
let stderr = "";
|
|
4751
|
+
if (captureMode === "buffer") {
|
|
4752
|
+
child.stdout?.setEncoding("utf8");
|
|
4753
|
+
child.stderr?.setEncoding("utf8");
|
|
4754
|
+
child.stdout?.on("data", (chunk) => {
|
|
4755
|
+
stdout += chunk;
|
|
4756
|
+
});
|
|
4757
|
+
child.stderr?.on("data", (chunk) => {
|
|
4758
|
+
stderr += chunk;
|
|
4759
|
+
});
|
|
4760
|
+
if (options.stdin !== void 0) {
|
|
4761
|
+
child.stdin?.end(options.stdin);
|
|
4762
|
+
} else {
|
|
4763
|
+
child.stdin?.end();
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
if (options.timeout_ms !== void 0) {
|
|
4767
|
+
timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
|
|
4768
|
+
}
|
|
4769
|
+
const cleanup = () => {
|
|
4770
|
+
if (timeoutTimer !== null) clearTimeout(timeoutTimer);
|
|
4771
|
+
if (killTimer !== null) clearTimeout(killTimer);
|
|
4772
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
4773
|
+
};
|
|
4774
|
+
return new Promise((resolve2, reject) => {
|
|
4775
|
+
child.once("error", (error) => {
|
|
4776
|
+
if (settled) return;
|
|
4777
|
+
settled = true;
|
|
4778
|
+
cleanup();
|
|
4779
|
+
reject(classifySpawnError(error));
|
|
4780
|
+
});
|
|
4781
|
+
child.once("close", (code, signal) => {
|
|
4782
|
+
if (settled) return;
|
|
4783
|
+
settled = true;
|
|
4784
|
+
cleanup();
|
|
4785
|
+
const ended_at = /* @__PURE__ */ new Date();
|
|
4786
|
+
resolve2({
|
|
4787
|
+
command: snapshotCommand,
|
|
4788
|
+
args: snapshotArgs,
|
|
4789
|
+
cwd: snapshotCwd,
|
|
4790
|
+
exit_code: code,
|
|
4791
|
+
signal,
|
|
4792
|
+
stdout,
|
|
4793
|
+
stderr,
|
|
4794
|
+
started_at: started_at.toISOString(),
|
|
4795
|
+
ended_at: ended_at.toISOString(),
|
|
4796
|
+
duration_ms: ended_at.getTime() - started_at.getTime(),
|
|
4797
|
+
pid: child.pid ?? null
|
|
4798
|
+
});
|
|
4799
|
+
});
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
};
|
|
4803
|
+
function validateOptions(options) {
|
|
4804
|
+
if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
|
|
4805
|
+
throw new Error("Invalid timeout_ms");
|
|
4806
|
+
}
|
|
4807
|
+
if (options.capture === "none" && options.stdin !== void 0) {
|
|
4808
|
+
throw new Error('Combination of capture: "none" and stdin is not supported');
|
|
4486
4809
|
}
|
|
4487
|
-
return ordered;
|
|
4488
4810
|
}
|
|
4489
|
-
function
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
let day = days.get(date);
|
|
4493
|
-
if (day === void 0) {
|
|
4494
|
-
day = {
|
|
4495
|
-
date,
|
|
4496
|
-
billableActiveTimeMs: 0,
|
|
4497
|
-
machineActiveTimeMs: 0,
|
|
4498
|
-
sessionCount: 0,
|
|
4499
|
-
commandCount: 0,
|
|
4500
|
-
fileChangedCount: 0,
|
|
4501
|
-
decisionCount: 0,
|
|
4502
|
-
tokens: emptyTokens()
|
|
4503
|
-
};
|
|
4504
|
-
days.set(date, day);
|
|
4505
|
-
}
|
|
4506
|
-
return day;
|
|
4507
|
-
};
|
|
4508
|
-
for (const [start, end] of unionMerged) {
|
|
4509
|
-
ensure(tzDate(start, timeZone)).billableActiveTimeMs += end - start;
|
|
4811
|
+
function classifySpawnError(error) {
|
|
4812
|
+
if (findErrorCode(error, "ENOENT")) {
|
|
4813
|
+
return new Error("Command not found", { cause: error });
|
|
4510
4814
|
}
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4815
|
+
return new Error("Failed to spawn child process", { cause: error });
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
// src/schemas/json-schema.ts
|
|
4819
|
+
import { z as z11 } from "zod";
|
|
4820
|
+
|
|
4821
|
+
// src/schemas/manifest.schema.ts
|
|
4822
|
+
import { z as z9 } from "zod";
|
|
4823
|
+
var ProjectSchema = z9.object({
|
|
4824
|
+
name: z9.string().optional(),
|
|
4825
|
+
description: z9.string().optional(),
|
|
4826
|
+
repository_url: z9.string().nullable().optional()
|
|
4827
|
+
});
|
|
4828
|
+
var CapabilitiesSchema = z9.object({
|
|
4829
|
+
enabled: z9.array(z9.string())
|
|
4830
|
+
});
|
|
4831
|
+
var ApprovalConfigSchema = z9.object({
|
|
4832
|
+
required_for: z9.array(z9.string()).optional(),
|
|
4833
|
+
default_risk_level: z9.enum(["low", "medium", "high", "critical"])
|
|
4834
|
+
});
|
|
4835
|
+
var ClaudeCodeAdapterConfigSchema = z9.object({
|
|
4836
|
+
enabled: z9.boolean(),
|
|
4837
|
+
config_path: z9.string().optional()
|
|
4838
|
+
});
|
|
4839
|
+
var AdaptersSchema = z9.object({
|
|
4840
|
+
"claude-code": ClaudeCodeAdapterConfigSchema
|
|
4841
|
+
});
|
|
4842
|
+
var GitConfigSchema = z9.object({
|
|
4843
|
+
events_log: z9.enum(["ignore", "commit"]).default("ignore")
|
|
4844
|
+
});
|
|
4845
|
+
var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
|
|
4846
|
+
var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
|
|
4847
|
+
message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
|
|
4848
|
+
});
|
|
4849
|
+
var ImportConfigSchema = z9.object({
|
|
4850
|
+
source_roots: z9.array(SourceRootSchema).min(1).optional()
|
|
4851
|
+
});
|
|
4852
|
+
var WorkspaceMetaSchema = z9.object({
|
|
4853
|
+
id: WorkspaceIdSchema,
|
|
4854
|
+
name: z9.string().min(1),
|
|
4855
|
+
created_at: IsoTimestampSchema,
|
|
4856
|
+
updated_at: IsoTimestampSchema
|
|
4857
|
+
});
|
|
4858
|
+
var ManifestSchema = z9.object({
|
|
4859
|
+
schema_version: SchemaVersionSchema,
|
|
4860
|
+
basou_version: z9.literal("0.1.0"),
|
|
4861
|
+
workspace: WorkspaceMetaSchema,
|
|
4862
|
+
project: ProjectSchema,
|
|
4863
|
+
capabilities: CapabilitiesSchema,
|
|
4864
|
+
approval: ApprovalConfigSchema,
|
|
4865
|
+
adapters: AdaptersSchema,
|
|
4866
|
+
git: GitConfigSchema,
|
|
4867
|
+
import: ImportConfigSchema.optional()
|
|
4868
|
+
});
|
|
4869
|
+
|
|
4870
|
+
// src/schemas/session-import.schema.ts
|
|
4871
|
+
import { z as z10 } from "zod";
|
|
4872
|
+
var SessionInnerImportSchema = z10.object({
|
|
4873
|
+
id: SessionIdSchema.optional(),
|
|
4874
|
+
label: z10.string().optional(),
|
|
4875
|
+
task_id: TaskIdSchema.nullable().optional(),
|
|
4876
|
+
workspace_id: WorkspaceIdSchema,
|
|
4877
|
+
source: z10.object({
|
|
4878
|
+
kind: SessionSourceKindSchema,
|
|
4879
|
+
version: z10.literal("0.1.0"),
|
|
4880
|
+
// Source-tool-native id (e.g. Claude Code session UUID), retained so
|
|
4881
|
+
// re-imports of the same source can be deduplicated.
|
|
4882
|
+
external_id: z10.string().optional(),
|
|
4883
|
+
// Byte size of the source native log at import time. Declared here too
|
|
4884
|
+
// (not only in session.schema.ts) because this inner `source` object is
|
|
4885
|
+
// a plain z.object: zod strips keys it does not declare, so a field
|
|
4886
|
+
// absent here would be dropped from the parsed payload before persist
|
|
4887
|
+
// and the size could never be stored.
|
|
4888
|
+
source_size_bytes: z10.number().int().nonnegative().optional()
|
|
4889
|
+
}),
|
|
4890
|
+
started_at: IsoTimestampSchema,
|
|
4891
|
+
ended_at: IsoTimestampSchema.optional(),
|
|
4892
|
+
status: SessionStatusSchema,
|
|
4893
|
+
working_directory: z10.string().min(1),
|
|
4894
|
+
invocation: z10.object({
|
|
4895
|
+
command: z10.string().min(1),
|
|
4896
|
+
args: z10.array(z10.string()),
|
|
4897
|
+
exit_code: z10.number().int().nullable()
|
|
4898
|
+
}),
|
|
4899
|
+
related_files: z10.array(z10.string()).default([]),
|
|
4900
|
+
events_log: z10.string().optional(),
|
|
4901
|
+
summary: z10.string().nullable().optional(),
|
|
4902
|
+
metrics: SessionMetricsSchema.optional(),
|
|
4903
|
+
// Accepted so a payload assembled from an on-disk chained session.yaml
|
|
4904
|
+
// round-trips, and DISCARDED by the importer (buildSessionRecord never
|
|
4905
|
+
// copies it): the integrity anchor is computed at write time, never
|
|
4906
|
+
// imported. Mirrors the accept-and-discard of `prev_hash` on events.
|
|
4907
|
+
integrity: SessionIntegritySchema.optional()
|
|
4908
|
+
}).strict();
|
|
4909
|
+
var SessionImportPayloadSchema = z10.object({
|
|
4910
|
+
schema_version: z10.string(),
|
|
4911
|
+
session: SessionInnerImportSchema,
|
|
4912
|
+
events: z10.array(EventSchema)
|
|
4913
|
+
}).strict();
|
|
4914
|
+
|
|
4915
|
+
// src/schemas/json-schema.ts
|
|
4916
|
+
var JSON_SCHEMA_VERSION = "0.1.0";
|
|
4917
|
+
var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
|
|
4918
|
+
var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
4919
|
+
var DOCUMENTS = [
|
|
4920
|
+
{
|
|
4921
|
+
name: "manifest",
|
|
4922
|
+
schema: ManifestSchema,
|
|
4923
|
+
title: "Basou Manifest",
|
|
4924
|
+
description: "The `.basou/manifest.yaml` workspace manifest."
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
name: "session",
|
|
4928
|
+
schema: SessionSchema,
|
|
4929
|
+
title: "Basou Session",
|
|
4930
|
+
description: "A `.basou/sessions/<id>/session.yaml` session record."
|
|
4931
|
+
},
|
|
4932
|
+
{
|
|
4933
|
+
name: "event",
|
|
4934
|
+
schema: EventSchema,
|
|
4935
|
+
title: "Basou Event",
|
|
4936
|
+
description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
|
|
4937
|
+
},
|
|
4938
|
+
{
|
|
4939
|
+
name: "task",
|
|
4940
|
+
schema: TaskSchema,
|
|
4941
|
+
title: "Basou Task",
|
|
4942
|
+
description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
|
|
4943
|
+
},
|
|
4944
|
+
{
|
|
4945
|
+
name: "approval",
|
|
4946
|
+
schema: ApprovalSchema,
|
|
4947
|
+
title: "Basou Approval",
|
|
4948
|
+
description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
|
|
4949
|
+
},
|
|
4950
|
+
{
|
|
4951
|
+
name: "status",
|
|
4952
|
+
schema: StatusSchema,
|
|
4953
|
+
title: "Basou Status",
|
|
4954
|
+
description: "The `.basou/status.json` workspace status snapshot."
|
|
4955
|
+
},
|
|
4956
|
+
{
|
|
4957
|
+
name: "task-index",
|
|
4958
|
+
schema: TaskIndexSchema,
|
|
4959
|
+
title: "Basou Task Index",
|
|
4960
|
+
description: "The `.basou/tasks/index.json` task lookup index."
|
|
4961
|
+
},
|
|
4962
|
+
{
|
|
4963
|
+
name: "session-import",
|
|
4964
|
+
schema: SessionImportPayloadSchema,
|
|
4965
|
+
title: "Basou Session Import Payload",
|
|
4966
|
+
description: "The portable session payload consumed by `basou session import`."
|
|
4521
4967
|
}
|
|
4522
|
-
|
|
4968
|
+
];
|
|
4969
|
+
function buildJsonSchemas() {
|
|
4970
|
+
return DOCUMENTS.map((doc) => {
|
|
4971
|
+
const generated = z11.toJSONSchema(doc.schema, { io: "input" });
|
|
4972
|
+
const { $schema, ...rest } = generated;
|
|
4973
|
+
const schema = {
|
|
4974
|
+
$schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
|
|
4975
|
+
$id: `${ID_BASE}/${doc.name}.schema.json`,
|
|
4976
|
+
title: doc.title,
|
|
4977
|
+
description: doc.description,
|
|
4978
|
+
...rest
|
|
4979
|
+
};
|
|
4980
|
+
return { name: doc.name, schema };
|
|
4981
|
+
});
|
|
4523
4982
|
}
|
|
4524
|
-
function
|
|
4525
|
-
return
|
|
4526
|
-
|
|
4527
|
-
year: "numeric",
|
|
4528
|
-
month: "2-digit",
|
|
4529
|
-
day: "2-digit"
|
|
4530
|
-
}).format(new Date(ms));
|
|
4983
|
+
function serializeJsonSchema(schema) {
|
|
4984
|
+
return `${JSON.stringify(schema, null, 2)}
|
|
4985
|
+
`;
|
|
4531
4986
|
}
|
|
4532
4987
|
|
|
4533
4988
|
// src/storage/basou-dir.ts
|
|
4534
4989
|
import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
|
|
4535
|
-
import { join as
|
|
4990
|
+
import { join as join15 } from "path";
|
|
4536
4991
|
function basouPaths(repositoryRoot) {
|
|
4537
|
-
const root =
|
|
4538
|
-
const approvalsBase =
|
|
4992
|
+
const root = join15(repositoryRoot, ".basou");
|
|
4993
|
+
const approvalsBase = join15(root, "approvals");
|
|
4539
4994
|
return {
|
|
4540
4995
|
root,
|
|
4541
|
-
sessions:
|
|
4542
|
-
tasks:
|
|
4996
|
+
sessions: join15(root, "sessions"),
|
|
4997
|
+
tasks: join15(root, "tasks"),
|
|
4543
4998
|
approvals: {
|
|
4544
|
-
pending:
|
|
4545
|
-
resolved:
|
|
4999
|
+
pending: join15(approvalsBase, "pending"),
|
|
5000
|
+
resolved: join15(approvalsBase, "resolved")
|
|
4546
5001
|
},
|
|
4547
|
-
locks:
|
|
4548
|
-
logs:
|
|
4549
|
-
raw:
|
|
4550
|
-
tmp:
|
|
5002
|
+
locks: join15(root, "locks"),
|
|
5003
|
+
logs: join15(root, "logs"),
|
|
5004
|
+
raw: join15(root, "raw"),
|
|
5005
|
+
tmp: join15(root, "tmp"),
|
|
4551
5006
|
files: {
|
|
4552
|
-
manifest:
|
|
4553
|
-
status:
|
|
4554
|
-
handoff:
|
|
4555
|
-
decisions:
|
|
5007
|
+
manifest: join15(root, "manifest.yaml"),
|
|
5008
|
+
status: join15(root, "status.json"),
|
|
5009
|
+
handoff: join15(root, "handoff.md"),
|
|
5010
|
+
decisions: join15(root, "decisions.md")
|
|
4556
5011
|
}
|
|
4557
5012
|
};
|
|
4558
5013
|
}
|
|
@@ -4608,16 +5063,16 @@ function hasErrorCode3(error) {
|
|
|
4608
5063
|
}
|
|
4609
5064
|
|
|
4610
5065
|
// src/storage/gitignore.ts
|
|
4611
|
-
import { readFile as
|
|
4612
|
-
import { join as
|
|
5066
|
+
import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
|
|
5067
|
+
import { join as join16 } from "path";
|
|
4613
5068
|
var MARKER = "# Basou - default ignore";
|
|
4614
5069
|
var BASOU_GITIGNORE_BLOCK = "# Basou - default ignore\n.basou/logs/\n.basou/raw/\n.basou/tmp/\n.basou/locks/\n.basou/status.json\n.basou/sessions/*/events.jsonl\n.basou/sessions/*/artifacts/\n.basou/approvals/pending/\n.basou/approvals/resolved/\n\n# Basou - default commit\n# .basou/manifest.yaml\n# .basou/handoff.md\n# .basou/decisions.md\n# .basou/tasks/\n# .basou/sessions/*/session.yaml\n# .basou/sessions/*/transcript.md\n# .basou/sessions/*/changed-files.json\n";
|
|
4615
5070
|
async function appendBasouGitignore(repositoryRoot) {
|
|
4616
|
-
const gitignorePath =
|
|
5071
|
+
const gitignorePath = join16(repositoryRoot, ".gitignore");
|
|
4617
5072
|
let body;
|
|
4618
5073
|
let existed;
|
|
4619
5074
|
try {
|
|
4620
|
-
body = await
|
|
5075
|
+
body = await readFile8(gitignorePath, "utf8");
|
|
4621
5076
|
existed = true;
|
|
4622
5077
|
} catch (error) {
|
|
4623
5078
|
if (hasErrorCode4(error) && error.code === "ENOENT") {
|
|
@@ -4723,12 +5178,12 @@ function hasErrorCode5(error) {
|
|
|
4723
5178
|
}
|
|
4724
5179
|
|
|
4725
5180
|
// src/storage/markdown-store.ts
|
|
4726
|
-
import { readFile as
|
|
5181
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4727
5182
|
var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
|
|
4728
5183
|
var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
|
|
4729
5184
|
async function readMarkdownFile(filePath) {
|
|
4730
5185
|
try {
|
|
4731
|
-
return await
|
|
5186
|
+
return await readFile9(filePath, "utf8");
|
|
4732
5187
|
} catch (error) {
|
|
4733
5188
|
if (hasErrorCode6(error) && error.code === "ENOENT") return null;
|
|
4734
5189
|
throw new Error("Failed to read markdown file", { cause: error });
|
|
@@ -4826,9 +5281,9 @@ function hasErrorCode6(error) {
|
|
|
4826
5281
|
}
|
|
4827
5282
|
|
|
4828
5283
|
// src/storage/session-import.ts
|
|
4829
|
-
import { mkdir as mkdir5, readFile as
|
|
5284
|
+
import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
|
|
4830
5285
|
import { homedir as homedir2 } from "os";
|
|
4831
|
-
import { join as
|
|
5286
|
+
import { join as join17 } from "path";
|
|
4832
5287
|
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
4833
5288
|
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
4834
5289
|
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
@@ -4853,7 +5308,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
4853
5308
|
pathSanitizeReport
|
|
4854
5309
|
};
|
|
4855
5310
|
}
|
|
4856
|
-
const sessionDir =
|
|
5311
|
+
const sessionDir = join17(paths.sessions, newSessionId);
|
|
4857
5312
|
try {
|
|
4858
5313
|
await mkdir5(sessionDir, { recursive: true });
|
|
4859
5314
|
} catch (error) {
|
|
@@ -4867,7 +5322,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
4867
5322
|
throw error;
|
|
4868
5323
|
}
|
|
4869
5324
|
try {
|
|
4870
|
-
const sessionYamlPath =
|
|
5325
|
+
const sessionYamlPath = join17(sessionDir, "session.yaml");
|
|
4871
5326
|
await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
|
|
4872
5327
|
} catch (error) {
|
|
4873
5328
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
@@ -5035,7 +5490,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
|
|
|
5035
5490
|
async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
|
|
5036
5491
|
const sessionId = priorSessionId;
|
|
5037
5492
|
const importSource = freshPayload.session.source.kind;
|
|
5038
|
-
const sessionDir =
|
|
5493
|
+
const sessionDir = join17(paths.sessions, priorSessionId);
|
|
5039
5494
|
const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
|
|
5040
5495
|
try {
|
|
5041
5496
|
const priorVerdict = await verifyEventsChain(paths, priorSessionId);
|
|
@@ -5077,10 +5532,10 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5077
5532
|
};
|
|
5078
5533
|
const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
|
|
5079
5534
|
if (options.dryRun !== true) {
|
|
5080
|
-
const eventsPath =
|
|
5535
|
+
const eventsPath = join17(sessionDir, "events.jsonl");
|
|
5081
5536
|
let priorEventsRaw = null;
|
|
5082
5537
|
try {
|
|
5083
|
-
priorEventsRaw = await
|
|
5538
|
+
priorEventsRaw = await readFile10(eventsPath);
|
|
5084
5539
|
} catch (error) {
|
|
5085
5540
|
if (!findErrorCode(error, "ENOENT")) {
|
|
5086
5541
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
@@ -5089,7 +5544,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5089
5544
|
const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
|
|
5090
5545
|
try {
|
|
5091
5546
|
await overwriteYamlFile(
|
|
5092
|
-
|
|
5547
|
+
join17(sessionDir, "session.yaml"),
|
|
5093
5548
|
withIntegrity(updatedRecord, chainResult)
|
|
5094
5549
|
);
|
|
5095
5550
|
} catch (error) {
|
|
@@ -5113,7 +5568,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
5113
5568
|
}
|
|
5114
5569
|
}
|
|
5115
5570
|
async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
5116
|
-
const sessionDir =
|
|
5571
|
+
const sessionDir = join17(paths.sessions, sessionId);
|
|
5117
5572
|
let lock;
|
|
5118
5573
|
try {
|
|
5119
5574
|
lock = await acquireLock(paths, "session", sessionId);
|
|
@@ -5146,10 +5601,10 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
5146
5601
|
if (verdict.status !== "unchained") {
|
|
5147
5602
|
return { status: "skipped", reason: "tampered" };
|
|
5148
5603
|
}
|
|
5149
|
-
const eventsPath =
|
|
5604
|
+
const eventsPath = join17(sessionDir, "events.jsonl");
|
|
5150
5605
|
let priorRaw;
|
|
5151
5606
|
try {
|
|
5152
|
-
priorRaw = await
|
|
5607
|
+
priorRaw = await readFile10(eventsPath);
|
|
5153
5608
|
} catch (error) {
|
|
5154
5609
|
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
5155
5610
|
}
|
|
@@ -5194,7 +5649,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
|
5194
5649
|
}
|
|
5195
5650
|
try {
|
|
5196
5651
|
await overwriteYamlFile(
|
|
5197
|
-
|
|
5652
|
+
join17(sessionDir, "session.yaml"),
|
|
5198
5653
|
withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
|
|
5199
5654
|
);
|
|
5200
5655
|
} catch (error) {
|
|
@@ -5248,6 +5703,8 @@ export {
|
|
|
5248
5703
|
WorkspaceIdSchema,
|
|
5249
5704
|
acquireLock,
|
|
5250
5705
|
appendBasouGitignore,
|
|
5706
|
+
appendChainedEvent,
|
|
5707
|
+
appendChainedEventLocked,
|
|
5251
5708
|
appendEvent,
|
|
5252
5709
|
appendEventToExistingSession,
|
|
5253
5710
|
archiveTask,
|
|
@@ -5272,11 +5729,14 @@ export {
|
|
|
5272
5729
|
enumerateArchivedTaskIds,
|
|
5273
5730
|
enumerateSessionDirs,
|
|
5274
5731
|
enumerateTaskIds,
|
|
5732
|
+
finalizeSessionYaml,
|
|
5275
5733
|
findErrorCode,
|
|
5734
|
+
formatDurationMs,
|
|
5276
5735
|
genesisHash,
|
|
5277
5736
|
getDiff,
|
|
5278
5737
|
getSnapshot,
|
|
5279
5738
|
importSessionFromJson,
|
|
5739
|
+
inspectChainTail,
|
|
5280
5740
|
isImportDerivedSource,
|
|
5281
5741
|
isLazyExpired,
|
|
5282
5742
|
isValidPrefixedId,
|
|
@@ -5304,6 +5764,7 @@ export {
|
|
|
5304
5764
|
reimportPreservingId,
|
|
5305
5765
|
renderDecisions,
|
|
5306
5766
|
renderHandoff,
|
|
5767
|
+
renderReport,
|
|
5307
5768
|
renderWithMarkers,
|
|
5308
5769
|
replayEvents,
|
|
5309
5770
|
resolveClaudeCodeCommand,
|