@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.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 join4, resolve } from "path";
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 join3 } from "path";
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 = join3(paths.sessions, sessionId, "session.yaml");
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 = join3(paths.sessions, sessionId);
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 = join4(input.paths.sessions, entry.sessionId);
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 = dirname(input.paths.root);
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 join5 } from "path";
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 appendFile(join5(sessionDir, "events.jsonl"), line, "utf8");
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 = join5(sessionDir, "events.jsonl");
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 readFile2 } from "fs/promises";
1391
- import { join as join6 } from "path";
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 sessionDir = join6(paths.sessions, sessionId);
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 readFile2(join6(sessionDir, "events.jsonl"));
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 = { kind: "present", integrity: session.session.integrity };
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 ? [] : splitLinesBytes(raw);
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 carriesPrevHash = (s) => {
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 && carriesPrevHash(l)) || tailFragment !== null && carriesPrevHash(tailFragment);
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 splitLinesBytes(buf) {
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 join11 } from "path";
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 readFile6, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
1818
- import { join as join10 } from "path";
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 join7 } from "path";
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 = join7(input.paths.sessions, sessionId);
1943
- try {
1944
- await mkdir(sessionDir, { recursive: true });
1945
- } catch (error) {
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
- await linkYamlFile(sessionYamlPath, initialSession);
1951
- } catch (error) {
1952
- await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
1953
- if (findErrorCode(error, "EEXIST")) {
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
- throw error;
1959
- }
1960
- try {
1961
- const targetEvents = input.targetEventBuilders.map((build, index) => {
1962
- const targetEventId = targetEventIds[index];
1963
- return assertTargetEventIdentity(build(sessionId, targetEventId), sessionId, targetEventId);
1964
- });
1965
- const events = [
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
- await overwriteYamlFile(sessionYamlPath, finalSession);
2021
- } catch (error) {
2022
- throw new FailedToFinalizeError(sessionId, targetEventIds, error);
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
- const sessionDir = join7(input.paths.sessions, input.sessionId);
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 readFile5 } from "fs/promises";
2166
- import { join as join9 } from "path";
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 join9(paths.tasks, "index.json");
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 readFile5(filePath, "utf8");
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 = join10(paths.tasks, `${taskId}.md`);
2429
+ const filePath = join11(paths.tasks, `${taskId}.md`);
2300
2430
  let raw;
2301
2431
  try {
2302
- raw = await readFile6(filePath, "utf8");
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 = join10(paths.tasks, `${taskId}.md`);
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 join10(paths.tasks, ARCHIVE_DIR_NAME);
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 = join10(archiveTasksDir(paths), `${taskId}.md`);
2577
+ const archiveFilePath = join11(archiveTasksDir(paths), `${taskId}.md`);
2448
2578
  let raw;
2449
2579
  try {
2450
- raw = await readFile6(archiveFilePath, "utf8");
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(join10(input.paths.sessions, input.sessionId, "session.yaml"), updated);
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 = join10(paths.tasks, `${taskId}.md`);
3001
- const [stats, raw] = await Promise.all([stat2(filePath), readFile6(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 = join10(paths.tasks, `${taskId}.md`);
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([readFile6(filePath), stat2(filePath)]);
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(join10(input.paths.tasks, `${input.taskId}.md`));
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
- join10(input.paths.tasks, `${input.taskId}.md`),
3571
- join10(archiveTasksDir(input.paths), `${input.taskId}.md`)
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 = join11(input.paths.sessions, entry.sessionId);
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/runtime/child-process-runner.ts
3954
- import { spawn as spawn2 } from "child_process";
3955
- var DEFAULT_KILL_GRACE_MS = 5e3;
3956
- var ChildProcessRunner = class {
3957
- async run(command, args, options) {
3958
- validateOptions(options);
3959
- if (options.signal?.aborted) {
3960
- throw new Error("Process aborted before spawn", {
3961
- cause: options.signal.reason
3962
- });
3963
- }
3964
- const snapshotCommand = command;
3965
- const snapshotArgs = [...args];
3966
- const snapshotCwd = options.cwd;
3967
- const captureMode = options.capture ?? "buffer";
3968
- const started_at = /* @__PURE__ */ new Date();
3969
- let child;
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
- child = spawn2(snapshotCommand, [...snapshotArgs], {
3972
- cwd: snapshotCwd,
3973
- env: options.env ?? process.env,
3974
- stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
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
- let timeoutTimer = null;
3988
- let killTimer = null;
3989
- let killed = false;
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
- if (options.timeout_ms !== void 0) {
4026
- timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
4027
- }
4028
- const cleanup = () => {
4029
- if (timeoutTimer !== null) clearTimeout(timeoutTimer);
4030
- if (killTimer !== null) clearTimeout(killTimer);
4031
- options.signal?.removeEventListener("abort", onAbort);
4032
- };
4033
- return new Promise((resolve2, reject) => {
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 computeByDay(sessions, unionMerged, timeZone) {
4490
- const days = /* @__PURE__ */ new Map();
4491
- const ensure = (date) => {
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
- for (const s of sessions) {
4512
- const startedMs = Date.parse(s.startedAt);
4513
- if (!Number.isFinite(startedMs)) continue;
4514
- const day = ensure(tzDate(startedMs, timeZone));
4515
- day.sessionCount++;
4516
- day.machineActiveTimeMs += s.machineActiveTimeMs;
4517
- day.commandCount += s.commandCount;
4518
- day.fileChangedCount += s.fileChangedCount;
4519
- day.decisionCount += s.decisionCount;
4520
- addTokens(day.tokens, s.tokens);
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
- return [...days.values()].sort((a, b) => a.date.localeCompare(b.date));
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 tzDate(ms, timeZone) {
4525
- return new Intl.DateTimeFormat("en-CA", {
4526
- timeZone,
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 join13 } from "path";
4990
+ import { join as join15 } from "path";
4536
4991
  function basouPaths(repositoryRoot) {
4537
- const root = join13(repositoryRoot, ".basou");
4538
- const approvalsBase = join13(root, "approvals");
4992
+ const root = join15(repositoryRoot, ".basou");
4993
+ const approvalsBase = join15(root, "approvals");
4539
4994
  return {
4540
4995
  root,
4541
- sessions: join13(root, "sessions"),
4542
- tasks: join13(root, "tasks"),
4996
+ sessions: join15(root, "sessions"),
4997
+ tasks: join15(root, "tasks"),
4543
4998
  approvals: {
4544
- pending: join13(approvalsBase, "pending"),
4545
- resolved: join13(approvalsBase, "resolved")
4999
+ pending: join15(approvalsBase, "pending"),
5000
+ resolved: join15(approvalsBase, "resolved")
4546
5001
  },
4547
- locks: join13(root, "locks"),
4548
- logs: join13(root, "logs"),
4549
- raw: join13(root, "raw"),
4550
- tmp: join13(root, "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: join13(root, "manifest.yaml"),
4553
- status: join13(root, "status.json"),
4554
- handoff: join13(root, "handoff.md"),
4555
- decisions: join13(root, "decisions.md")
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 readFile7, writeFile as writeFile2 } from "fs/promises";
4612
- import { join as join14 } from "path";
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 = join14(repositoryRoot, ".gitignore");
5071
+ const gitignorePath = join16(repositoryRoot, ".gitignore");
4617
5072
  let body;
4618
5073
  let existed;
4619
5074
  try {
4620
- body = await readFile7(gitignorePath, "utf8");
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 readFile8 } from "fs/promises";
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 readFile8(filePath, "utf8");
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 readFile9, rm as rm2 } from "fs/promises";
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 join15 } from "path";
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 = join15(paths.sessions, newSessionId);
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 = join15(sessionDir, "session.yaml");
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 = join15(paths.sessions, priorSessionId);
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 = join15(sessionDir, "events.jsonl");
5535
+ const eventsPath = join17(sessionDir, "events.jsonl");
5081
5536
  let priorEventsRaw = null;
5082
5537
  try {
5083
- priorEventsRaw = await readFile9(eventsPath);
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
- join15(sessionDir, "session.yaml"),
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 = join15(paths.sessions, sessionId);
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 = join15(sessionDir, "events.jsonl");
5604
+ const eventsPath = join17(sessionDir, "events.jsonl");
5150
5605
  let priorRaw;
5151
5606
  try {
5152
- priorRaw = await readFile9(eventsPath);
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
- join15(sessionDir, "session.yaml"),
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,