@basou/core 0.8.0 → 0.9.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 +293 -3
- package/dist/index.js +423 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/event.schema.json +57 -0
- package/schemas/session-import.schema.json +75 -0
- package/schemas/session.schema.json +18 -0
package/dist/index.js
CHANGED
|
@@ -781,7 +781,14 @@ var BaseEventSchema = z3.object({
|
|
|
781
781
|
id: EventIdSchema,
|
|
782
782
|
session_id: SessionIdSchema,
|
|
783
783
|
occurred_at: IsoTimestampSchema,
|
|
784
|
-
source: EventSourceSchema
|
|
784
|
+
source: EventSourceSchema,
|
|
785
|
+
// Tamper-evidence back-pointer (hex sha-256 of the PREVIOUS event line's
|
|
786
|
+
// written bytes; the first line carries the session-bound genesis hash).
|
|
787
|
+
// Present only on sessions written with chaining enabled (import paths);
|
|
788
|
+
// live/ad-hoc sessions omit it. Declared on the base so the `.strict()`
|
|
789
|
+
// variants below treat it as a known key. Additive optional => no
|
|
790
|
+
// schema_version bump.
|
|
791
|
+
prev_hash: z3.string().optional()
|
|
785
792
|
});
|
|
786
793
|
var SessionStartedEventSchema = BaseEventSchema.extend({
|
|
787
794
|
type: z3.literal("session_started")
|
|
@@ -1062,6 +1069,10 @@ var SessionMetricsSchema = z4.object({
|
|
|
1062
1069
|
active_time_method: z4.string().optional(),
|
|
1063
1070
|
machine_active_time_ms: z4.number().int().nonnegative().optional()
|
|
1064
1071
|
});
|
|
1072
|
+
var SessionIntegritySchema = z4.object({
|
|
1073
|
+
head_hash: z4.string(),
|
|
1074
|
+
event_count: z4.number().int().nonnegative()
|
|
1075
|
+
}).strict();
|
|
1065
1076
|
var SessionInnerSchema = z4.object({
|
|
1066
1077
|
id: SessionIdSchema,
|
|
1067
1078
|
label: z4.string().optional(),
|
|
@@ -1077,7 +1088,8 @@ var SessionInnerSchema = z4.object({
|
|
|
1077
1088
|
related_files: z4.array(z4.string()).default([]),
|
|
1078
1089
|
events_log: z4.string().default("events.jsonl"),
|
|
1079
1090
|
summary: z4.string().nullable().optional(),
|
|
1080
|
-
metrics: SessionMetricsSchema.optional()
|
|
1091
|
+
metrics: SessionMetricsSchema.optional(),
|
|
1092
|
+
integrity: SessionIntegritySchema.optional()
|
|
1081
1093
|
});
|
|
1082
1094
|
var SessionSchema = z4.object({
|
|
1083
1095
|
schema_version: SchemaVersionSchema,
|
|
@@ -1286,9 +1298,50 @@ function shortDecisionSessionId(sessionId) {
|
|
|
1286
1298
|
return sessionId.slice(0, 10);
|
|
1287
1299
|
}
|
|
1288
1300
|
|
|
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
|
+
|
|
1289
1342
|
// src/events/event-writer.ts
|
|
1290
1343
|
import { appendFile } from "fs/promises";
|
|
1291
|
-
import { join as join5 } from "path";
|
|
1344
|
+
import { basename, join as join5 } from "path";
|
|
1292
1345
|
async function appendEvent(sessionDir, event) {
|
|
1293
1346
|
let validated;
|
|
1294
1347
|
try {
|
|
@@ -1296,7 +1349,7 @@ async function appendEvent(sessionDir, event) {
|
|
|
1296
1349
|
} catch (error) {
|
|
1297
1350
|
throw new Error("Invalid Basou event payload", { cause: error });
|
|
1298
1351
|
}
|
|
1299
|
-
const line = `${
|
|
1352
|
+
const line = `${serializeEventLine(validated)}
|
|
1300
1353
|
`;
|
|
1301
1354
|
try {
|
|
1302
1355
|
await appendFile(join5(sessionDir, "events.jsonl"), line, "utf8");
|
|
@@ -1304,7 +1357,7 @@ async function appendEvent(sessionDir, event) {
|
|
|
1304
1357
|
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
1305
1358
|
}
|
|
1306
1359
|
}
|
|
1307
|
-
async function writeEventsBulk(sessionDir, events) {
|
|
1360
|
+
async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
1308
1361
|
const validated = [];
|
|
1309
1362
|
try {
|
|
1310
1363
|
for (const event of events) {
|
|
@@ -1314,13 +1367,153 @@ async function writeEventsBulk(sessionDir, events) {
|
|
|
1314
1367
|
throw new Error("Invalid Basou event payload", { cause: error });
|
|
1315
1368
|
}
|
|
1316
1369
|
const filePath = join5(sessionDir, "events.jsonl");
|
|
1317
|
-
|
|
1370
|
+
let body;
|
|
1371
|
+
let result = null;
|
|
1372
|
+
if (options.chain === true) {
|
|
1373
|
+
const { lines, headHash, count } = chainEvents(validated, basename(sessionDir));
|
|
1374
|
+
body = lines.length > 0 ? `${lines.join("\n")}
|
|
1318
1375
|
` : "";
|
|
1376
|
+
result = count > 0 ? { headHash, count } : null;
|
|
1377
|
+
} else {
|
|
1378
|
+
body = validated.length > 0 ? `${validated.map(serializeEventLine).join("\n")}
|
|
1379
|
+
` : "";
|
|
1380
|
+
}
|
|
1319
1381
|
try {
|
|
1320
1382
|
await atomicReplace(filePath, body);
|
|
1321
1383
|
} catch (error) {
|
|
1322
1384
|
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
1323
1385
|
}
|
|
1386
|
+
return result;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/events/verify.ts
|
|
1390
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1391
|
+
import { join as join6 } from "path";
|
|
1392
|
+
async function verifyEventsChain(paths, sessionId) {
|
|
1393
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
1394
|
+
let raw = null;
|
|
1395
|
+
try {
|
|
1396
|
+
raw = await readFile2(join6(sessionDir, "events.jsonl"));
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (!findErrorCode(error, "ENOENT")) {
|
|
1399
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
let anchor;
|
|
1403
|
+
try {
|
|
1404
|
+
const session = await readSessionYaml(paths, sessionId);
|
|
1405
|
+
anchor = { kind: "present", integrity: session.session.integrity };
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
1408
|
+
anchor = { kind: "absent" };
|
|
1409
|
+
} else {
|
|
1410
|
+
anchor = { kind: "unreadable" };
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const terminated = raw === null || raw.length === 0 || raw[raw.length - 1] === 10;
|
|
1414
|
+
const segments = raw === null ? [] : splitLinesBytes(raw);
|
|
1415
|
+
const tailFragment = !terminated && segments.length > 0 ? segments.pop() : null;
|
|
1416
|
+
const lines = segments;
|
|
1417
|
+
const carriesPrevHash = (s) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const obj = JSON.parse(s.toString("utf8"));
|
|
1420
|
+
return typeof obj === "object" && obj !== null && "prev_hash" in obj;
|
|
1421
|
+
} catch {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
const chained = lines.some((l) => l.length > 0 && carriesPrevHash(l)) || tailFragment !== null && carriesPrevHash(tailFragment);
|
|
1426
|
+
if (!chained) {
|
|
1427
|
+
if (anchor.kind === "present" && anchor.integrity !== void 0) {
|
|
1428
|
+
return {
|
|
1429
|
+
status: "tampered",
|
|
1430
|
+
eventCount: lines.length,
|
|
1431
|
+
reason: "anchor_without_chain"
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
if (raw === null || raw.length === 0) {
|
|
1435
|
+
return { status: "empty", eventCount: 0 };
|
|
1436
|
+
}
|
|
1437
|
+
return { status: "unchained", eventCount: lines.length };
|
|
1438
|
+
}
|
|
1439
|
+
let expected = genesisHash(sessionId);
|
|
1440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1441
|
+
const line = lines[i];
|
|
1442
|
+
const lineNo = i + 1;
|
|
1443
|
+
if (line.length === 0) {
|
|
1444
|
+
return { status: "tampered", eventCount: lines.length, reason: "blank_line", line: lineNo };
|
|
1445
|
+
}
|
|
1446
|
+
let parsed;
|
|
1447
|
+
try {
|
|
1448
|
+
parsed = JSON.parse(line.toString("utf8"));
|
|
1449
|
+
} catch {
|
|
1450
|
+
return {
|
|
1451
|
+
status: "tampered",
|
|
1452
|
+
eventCount: lines.length,
|
|
1453
|
+
reason: "malformed_line",
|
|
1454
|
+
line: lineNo
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
const record = parsed;
|
|
1458
|
+
if (typeof record.prev_hash !== "string") {
|
|
1459
|
+
return {
|
|
1460
|
+
status: "tampered",
|
|
1461
|
+
eventCount: lines.length,
|
|
1462
|
+
reason: "missing_prev_hash",
|
|
1463
|
+
line: lineNo
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
if (record.prev_hash !== expected) {
|
|
1467
|
+
return {
|
|
1468
|
+
status: "tampered",
|
|
1469
|
+
eventCount: lines.length,
|
|
1470
|
+
reason: i === 0 ? "genesis_mismatch" : "broken_link",
|
|
1471
|
+
line: lineNo
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
if (record.session_id !== sessionId) {
|
|
1475
|
+
return {
|
|
1476
|
+
status: "tampered",
|
|
1477
|
+
eventCount: lines.length,
|
|
1478
|
+
reason: "session_id_mismatch",
|
|
1479
|
+
line: lineNo
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
expected = lineHash(line);
|
|
1483
|
+
}
|
|
1484
|
+
if (tailFragment !== null || !terminated) {
|
|
1485
|
+
return {
|
|
1486
|
+
status: "tampered",
|
|
1487
|
+
eventCount: lines.length,
|
|
1488
|
+
reason: "torn_tail",
|
|
1489
|
+
line: lines.length + 1
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
if (anchor.kind === "absent") {
|
|
1493
|
+
return { status: "incomplete", eventCount: lines.length, reason: "yaml_missing" };
|
|
1494
|
+
}
|
|
1495
|
+
if (anchor.kind === "unreadable") {
|
|
1496
|
+
return { status: "tampered", eventCount: lines.length, reason: "yaml_unreadable" };
|
|
1497
|
+
}
|
|
1498
|
+
if (anchor.integrity === void 0) {
|
|
1499
|
+
return { status: "tampered", eventCount: lines.length, reason: "anchor_missing" };
|
|
1500
|
+
}
|
|
1501
|
+
if (anchor.integrity.event_count !== lines.length || anchor.integrity.head_hash !== expected) {
|
|
1502
|
+
return { status: "tampered", eventCount: lines.length, reason: "anchor_mismatch" };
|
|
1503
|
+
}
|
|
1504
|
+
return { status: "verified", eventCount: lines.length };
|
|
1505
|
+
}
|
|
1506
|
+
function splitLinesBytes(buf) {
|
|
1507
|
+
const out = [];
|
|
1508
|
+
let start = 0;
|
|
1509
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1510
|
+
if (buf[i] === 10) {
|
|
1511
|
+
out.push(buf.subarray(start, i));
|
|
1512
|
+
start = i + 1;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (start < buf.length) out.push(buf.subarray(start));
|
|
1516
|
+
return out;
|
|
1324
1517
|
}
|
|
1325
1518
|
|
|
1326
1519
|
// src/git/snapshot.ts
|
|
@@ -1617,12 +1810,12 @@ function parseDiffNameStatus(raw) {
|
|
|
1617
1810
|
}
|
|
1618
1811
|
|
|
1619
1812
|
// src/handoff/handoff-renderer.ts
|
|
1620
|
-
import { join as
|
|
1813
|
+
import { join as join11 } from "path";
|
|
1621
1814
|
|
|
1622
1815
|
// src/storage/tasks.ts
|
|
1623
|
-
import { createHash } from "crypto";
|
|
1624
|
-
import { mkdir as
|
|
1625
|
-
import { join as
|
|
1816
|
+
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";
|
|
1626
1819
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1627
1820
|
import { z as z8 } from "zod";
|
|
1628
1821
|
|
|
@@ -1666,7 +1859,7 @@ var TaskSchema = z6.object({
|
|
|
1666
1859
|
// src/storage/ad-hoc-session.ts
|
|
1667
1860
|
import { mkdir, rm } from "fs/promises";
|
|
1668
1861
|
import { homedir } from "os";
|
|
1669
|
-
import { join as
|
|
1862
|
+
import { join as join7 } from "path";
|
|
1670
1863
|
|
|
1671
1864
|
// src/lib/path-sanitizer.ts
|
|
1672
1865
|
import { posix as path } from "path";
|
|
@@ -1746,13 +1939,13 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
1746
1939
|
taskId: input.taskId ?? null
|
|
1747
1940
|
})
|
|
1748
1941
|
);
|
|
1749
|
-
const sessionDir =
|
|
1942
|
+
const sessionDir = join7(input.paths.sessions, sessionId);
|
|
1750
1943
|
try {
|
|
1751
1944
|
await mkdir(sessionDir, { recursive: true });
|
|
1752
1945
|
} catch (error) {
|
|
1753
1946
|
throw new Error("Failed to create session directory", { cause: error });
|
|
1754
1947
|
}
|
|
1755
|
-
const sessionYamlPath =
|
|
1948
|
+
const sessionYamlPath = join7(sessionDir, "session.yaml");
|
|
1756
1949
|
try {
|
|
1757
1950
|
await linkYamlFile(sessionYamlPath, initialSession);
|
|
1758
1951
|
} catch (error) {
|
|
@@ -1875,7 +2068,7 @@ async function appendEventToExistingSession(input) {
|
|
|
1875
2068
|
}
|
|
1876
2069
|
const eventId = prefixedUlid("evt");
|
|
1877
2070
|
const event = assertTargetEventIdentity(input.eventBuilder(eventId), input.sessionId, eventId);
|
|
1878
|
-
const sessionDir =
|
|
2071
|
+
const sessionDir = join7(input.paths.sessions, input.sessionId);
|
|
1879
2072
|
await appendEvent(sessionDir, event);
|
|
1880
2073
|
return { eventId, sessionStatus: status };
|
|
1881
2074
|
}
|
|
@@ -1890,8 +2083,8 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
|
|
|
1890
2083
|
}
|
|
1891
2084
|
|
|
1892
2085
|
// src/storage/lockfile.ts
|
|
1893
|
-
import { readFile as
|
|
1894
|
-
import { join as
|
|
2086
|
+
import { mkdir as mkdir2, readFile as readFile4, unlink as unlink2 } from "fs/promises";
|
|
2087
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
1895
2088
|
var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
1896
2089
|
async function acquireLock(paths, scope, resourceId) {
|
|
1897
2090
|
const lockPath = lockfilePath(paths, scope, resourceId);
|
|
@@ -1903,6 +2096,19 @@ async function acquireLock(paths, scope, resourceId) {
|
|
|
1903
2096
|
try {
|
|
1904
2097
|
await atomicCreate(lockPath, serialised);
|
|
1905
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
|
+
}
|
|
1906
2112
|
if (!findErrorCode(error, "EEXIST")) {
|
|
1907
2113
|
throw error;
|
|
1908
2114
|
}
|
|
@@ -1926,7 +2132,7 @@ async function acquireLock(paths, scope, resourceId) {
|
|
|
1926
2132
|
async function isStaleLock(lockPath) {
|
|
1927
2133
|
let body;
|
|
1928
2134
|
try {
|
|
1929
|
-
const raw = await
|
|
2135
|
+
const raw = await readFile4(lockPath, "utf8");
|
|
1930
2136
|
const parsed = JSON.parse(raw);
|
|
1931
2137
|
if (typeof parsed !== "object" || parsed === null) return true;
|
|
1932
2138
|
const candidate = parsed;
|
|
@@ -1952,12 +2158,12 @@ async function isStaleLock(lockPath) {
|
|
|
1952
2158
|
function lockfilePath(paths, scope, resourceId) {
|
|
1953
2159
|
const sep = resourceId.indexOf("_");
|
|
1954
2160
|
const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
|
|
1955
|
-
return
|
|
2161
|
+
return join8(paths.locks, `${scope}_${ulid2}.lock`);
|
|
1956
2162
|
}
|
|
1957
2163
|
|
|
1958
2164
|
// src/storage/task-index.ts
|
|
1959
|
-
import { readFile as
|
|
1960
|
-
import { join as
|
|
2165
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2166
|
+
import { join as join9 } from "path";
|
|
1961
2167
|
|
|
1962
2168
|
// src/schemas/task-index.schema.ts
|
|
1963
2169
|
import { z as z7 } from "zod";
|
|
@@ -1976,13 +2182,13 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
|
|
|
1976
2182
|
|
|
1977
2183
|
// src/storage/task-index.ts
|
|
1978
2184
|
function taskIndexPath(paths) {
|
|
1979
|
-
return
|
|
2185
|
+
return join9(paths.tasks, "index.json");
|
|
1980
2186
|
}
|
|
1981
2187
|
async function readTaskIndex(paths) {
|
|
1982
2188
|
const filePath = taskIndexPath(paths);
|
|
1983
2189
|
let raw;
|
|
1984
2190
|
try {
|
|
1985
|
-
raw = await
|
|
2191
|
+
raw = await readFile5(filePath, "utf8");
|
|
1986
2192
|
} catch (error) {
|
|
1987
2193
|
if (findErrorCode(error, "ENOENT")) {
|
|
1988
2194
|
throw new Error("Task index not found", { cause: error });
|
|
@@ -2090,10 +2296,10 @@ function splitFrontMatter(raw) {
|
|
|
2090
2296
|
return { yamlText, body };
|
|
2091
2297
|
}
|
|
2092
2298
|
async function readTaskFile(paths, taskId) {
|
|
2093
|
-
const filePath =
|
|
2299
|
+
const filePath = join10(paths.tasks, `${taskId}.md`);
|
|
2094
2300
|
let raw;
|
|
2095
2301
|
try {
|
|
2096
|
-
raw = await
|
|
2302
|
+
raw = await readFile6(filePath, "utf8");
|
|
2097
2303
|
} catch (error) {
|
|
2098
2304
|
if (findErrorCode(error, "ENOENT")) {
|
|
2099
2305
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2123,7 +2329,7 @@ async function readTaskFile(paths, taskId) {
|
|
|
2123
2329
|
}
|
|
2124
2330
|
async function writeTaskFile(paths, taskId, doc, options) {
|
|
2125
2331
|
const validated = TaskSchema.parse(doc.task);
|
|
2126
|
-
const filePath =
|
|
2332
|
+
const filePath = join10(paths.tasks, `${taskId}.md`);
|
|
2127
2333
|
const yamlText = stringifyYaml(validated);
|
|
2128
2334
|
const trimmedBody = doc.body.length === 0 ? "" : `
|
|
2129
2335
|
${doc.body.endsWith("\n") ? doc.body : `${doc.body}
|
|
@@ -2208,7 +2414,7 @@ async function safeUpdateTaskIndex(paths, op) {
|
|
|
2208
2414
|
}
|
|
2209
2415
|
var ARCHIVE_DIR_NAME = "archive";
|
|
2210
2416
|
function archiveTasksDir(paths) {
|
|
2211
|
-
return
|
|
2417
|
+
return join10(paths.tasks, ARCHIVE_DIR_NAME);
|
|
2212
2418
|
}
|
|
2213
2419
|
async function enumerateArchivedTaskIds(paths) {
|
|
2214
2420
|
let entries;
|
|
@@ -2238,10 +2444,10 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
|
|
|
2238
2444
|
throw error;
|
|
2239
2445
|
}
|
|
2240
2446
|
}
|
|
2241
|
-
const archiveFilePath =
|
|
2447
|
+
const archiveFilePath = join10(archiveTasksDir(paths), `${taskId}.md`);
|
|
2242
2448
|
let raw;
|
|
2243
2449
|
try {
|
|
2244
|
-
raw = await
|
|
2450
|
+
raw = await readFile6(archiveFilePath, "utf8");
|
|
2245
2451
|
} catch (error) {
|
|
2246
2452
|
if (findErrorCode(error, "ENOENT")) {
|
|
2247
2453
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2532,7 +2738,7 @@ async function createTaskAttachLocked(input) {
|
|
|
2532
2738
|
...sessionDoc,
|
|
2533
2739
|
session: { ...sessionDoc.session, task_id: input.taskId }
|
|
2534
2740
|
};
|
|
2535
|
-
await overwriteYamlFile(
|
|
2741
|
+
await overwriteYamlFile(join10(input.paths.sessions, input.sessionId, "session.yaml"), updated);
|
|
2536
2742
|
} catch (error) {
|
|
2537
2743
|
throw new TaskWriteAfterEventError({
|
|
2538
2744
|
taskId: input.taskId,
|
|
@@ -2791,17 +2997,17 @@ function buildUpdatedDoc(input) {
|
|
|
2791
2997
|
return { task: next, body: input.currentDoc.body };
|
|
2792
2998
|
}
|
|
2793
2999
|
async function computeTaskMdSnapshot(paths, taskId) {
|
|
2794
|
-
const filePath =
|
|
2795
|
-
const [stats, raw] = await Promise.all([stat2(filePath),
|
|
2796
|
-
const hash =
|
|
3000
|
+
const filePath = join10(paths.tasks, `${taskId}.md`);
|
|
3001
|
+
const [stats, raw] = await Promise.all([stat2(filePath), readFile6(filePath)]);
|
|
3002
|
+
const hash = createHash2("sha256").update(raw).digest("hex");
|
|
2797
3003
|
return { mtimeMs: stats.mtimeMs, hash };
|
|
2798
3004
|
}
|
|
2799
3005
|
async function readTaskFileWithSnapshot(paths, taskId) {
|
|
2800
|
-
const filePath =
|
|
3006
|
+
const filePath = join10(paths.tasks, `${taskId}.md`);
|
|
2801
3007
|
let rawBuffer;
|
|
2802
3008
|
let stats;
|
|
2803
3009
|
try {
|
|
2804
|
-
[rawBuffer, stats] = await Promise.all([
|
|
3010
|
+
[rawBuffer, stats] = await Promise.all([readFile6(filePath), stat2(filePath)]);
|
|
2805
3011
|
} catch (error) {
|
|
2806
3012
|
if (findErrorCode(error, "ENOENT")) {
|
|
2807
3013
|
throw new Error("Task file not found", { cause: error });
|
|
@@ -2809,7 +3015,7 @@ async function readTaskFileWithSnapshot(paths, taskId) {
|
|
|
2809
3015
|
throw new Error("Failed to read task file", { cause: error });
|
|
2810
3016
|
}
|
|
2811
3017
|
const raw = rawBuffer.toString("utf8");
|
|
2812
|
-
const hash =
|
|
3018
|
+
const hash = createHash2("sha256").update(rawBuffer).digest("hex");
|
|
2813
3019
|
let split;
|
|
2814
3020
|
try {
|
|
2815
3021
|
split = splitFrontMatter(raw);
|
|
@@ -3289,7 +3495,7 @@ async function deleteTaskLocked(input) {
|
|
|
3289
3495
|
});
|
|
3290
3496
|
const eventId = adHoc.targetEventIds[0];
|
|
3291
3497
|
try {
|
|
3292
|
-
await unlink3(
|
|
3498
|
+
await unlink3(join10(input.paths.tasks, `${input.taskId}.md`));
|
|
3293
3499
|
} catch (error) {
|
|
3294
3500
|
throw new TaskWriteAfterEventError({
|
|
3295
3501
|
taskId: input.taskId,
|
|
@@ -3359,10 +3565,10 @@ async function archiveTaskLocked(input) {
|
|
|
3359
3565
|
{ task: next, body: doc.body },
|
|
3360
3566
|
{ mode: "overwrite" }
|
|
3361
3567
|
);
|
|
3362
|
-
await
|
|
3568
|
+
await mkdir3(archiveTasksDir(input.paths), { recursive: true });
|
|
3363
3569
|
await rename2(
|
|
3364
|
-
|
|
3365
|
-
|
|
3570
|
+
join10(input.paths.tasks, `${input.taskId}.md`),
|
|
3571
|
+
join10(archiveTasksDir(input.paths), `${input.taskId}.md`)
|
|
3366
3572
|
);
|
|
3367
3573
|
} catch (error) {
|
|
3368
3574
|
throw new TaskWriteAfterEventError({
|
|
@@ -3398,7 +3604,7 @@ async function renderHandoff(input) {
|
|
|
3398
3604
|
const tasksCreated = [];
|
|
3399
3605
|
const tasksStatusChanged = [];
|
|
3400
3606
|
for (const entry of entries) {
|
|
3401
|
-
const sessionDir =
|
|
3607
|
+
const sessionDir = join11(input.paths.sessions, entry.sessionId);
|
|
3402
3608
|
try {
|
|
3403
3609
|
for await (const ev of replayEvents(sessionDir, {
|
|
3404
3610
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3952,7 +4158,12 @@ var SessionInnerImportSchema = z10.object({
|
|
|
3952
4158
|
related_files: z10.array(z10.string()).default([]),
|
|
3953
4159
|
events_log: z10.string().optional(),
|
|
3954
4160
|
summary: z10.string().nullable().optional(),
|
|
3955
|
-
metrics: SessionMetricsSchema.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()
|
|
3956
4167
|
}).strict();
|
|
3957
4168
|
var SessionImportPayloadSchema = z10.object({
|
|
3958
4169
|
schema_version: z10.string(),
|
|
@@ -4034,7 +4245,7 @@ function serializeJsonSchema(schema) {
|
|
|
4034
4245
|
}
|
|
4035
4246
|
|
|
4036
4247
|
// src/stats/work-stats.ts
|
|
4037
|
-
import { join as
|
|
4248
|
+
import { join as join12 } from "path";
|
|
4038
4249
|
function resolveTimeZone(timeZone) {
|
|
4039
4250
|
if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
|
|
4040
4251
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -4065,7 +4276,7 @@ async function computeWorkStats(input) {
|
|
|
4065
4276
|
const events = [];
|
|
4066
4277
|
let eventsUnreadable = false;
|
|
4067
4278
|
try {
|
|
4068
|
-
for await (const ev of replayEvents(
|
|
4279
|
+
for await (const ev of replayEvents(join12(input.paths.sessions, entry.sessionId), {
|
|
4069
4280
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
4070
4281
|
})) {
|
|
4071
4282
|
events.push(ev);
|
|
@@ -4320,28 +4531,28 @@ function tzDate(ms, timeZone) {
|
|
|
4320
4531
|
}
|
|
4321
4532
|
|
|
4322
4533
|
// src/storage/basou-dir.ts
|
|
4323
|
-
import { lstat as lstat3, mkdir as
|
|
4324
|
-
import { join as
|
|
4534
|
+
import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
|
|
4535
|
+
import { join as join13 } from "path";
|
|
4325
4536
|
function basouPaths(repositoryRoot) {
|
|
4326
|
-
const root =
|
|
4327
|
-
const approvalsBase =
|
|
4537
|
+
const root = join13(repositoryRoot, ".basou");
|
|
4538
|
+
const approvalsBase = join13(root, "approvals");
|
|
4328
4539
|
return {
|
|
4329
4540
|
root,
|
|
4330
|
-
sessions:
|
|
4331
|
-
tasks:
|
|
4541
|
+
sessions: join13(root, "sessions"),
|
|
4542
|
+
tasks: join13(root, "tasks"),
|
|
4332
4543
|
approvals: {
|
|
4333
|
-
pending:
|
|
4334
|
-
resolved:
|
|
4544
|
+
pending: join13(approvalsBase, "pending"),
|
|
4545
|
+
resolved: join13(approvalsBase, "resolved")
|
|
4335
4546
|
},
|
|
4336
|
-
locks:
|
|
4337
|
-
logs:
|
|
4338
|
-
raw:
|
|
4339
|
-
tmp:
|
|
4547
|
+
locks: join13(root, "locks"),
|
|
4548
|
+
logs: join13(root, "logs"),
|
|
4549
|
+
raw: join13(root, "raw"),
|
|
4550
|
+
tmp: join13(root, "tmp"),
|
|
4340
4551
|
files: {
|
|
4341
|
-
manifest:
|
|
4342
|
-
status:
|
|
4343
|
-
handoff:
|
|
4344
|
-
decisions:
|
|
4552
|
+
manifest: join13(root, "manifest.yaml"),
|
|
4553
|
+
status: join13(root, "status.json"),
|
|
4554
|
+
handoff: join13(root, "handoff.md"),
|
|
4555
|
+
decisions: join13(root, "decisions.md")
|
|
4345
4556
|
}
|
|
4346
4557
|
};
|
|
4347
4558
|
}
|
|
@@ -4382,7 +4593,7 @@ async function ensureBasouDirectory(repositoryRoot) {
|
|
|
4382
4593
|
}
|
|
4383
4594
|
async function mkdirLabeled(target, label) {
|
|
4384
4595
|
try {
|
|
4385
|
-
await
|
|
4596
|
+
await mkdir4(target, { recursive: true });
|
|
4386
4597
|
} catch (error) {
|
|
4387
4598
|
if (hasErrorCode3(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
|
|
4388
4599
|
throw new Error(`${label} exists but is not a directory`, { cause: error });
|
|
@@ -4397,16 +4608,16 @@ function hasErrorCode3(error) {
|
|
|
4397
4608
|
}
|
|
4398
4609
|
|
|
4399
4610
|
// src/storage/gitignore.ts
|
|
4400
|
-
import { readFile as
|
|
4401
|
-
import { join as
|
|
4611
|
+
import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
|
|
4612
|
+
import { join as join14 } from "path";
|
|
4402
4613
|
var MARKER = "# Basou - default ignore";
|
|
4403
4614
|
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";
|
|
4404
4615
|
async function appendBasouGitignore(repositoryRoot) {
|
|
4405
|
-
const gitignorePath =
|
|
4616
|
+
const gitignorePath = join14(repositoryRoot, ".gitignore");
|
|
4406
4617
|
let body;
|
|
4407
4618
|
let existed;
|
|
4408
4619
|
try {
|
|
4409
|
-
body = await
|
|
4620
|
+
body = await readFile7(gitignorePath, "utf8");
|
|
4410
4621
|
existed = true;
|
|
4411
4622
|
} catch (error) {
|
|
4412
4623
|
if (hasErrorCode4(error) && error.code === "ENOENT") {
|
|
@@ -4512,12 +4723,12 @@ function hasErrorCode5(error) {
|
|
|
4512
4723
|
}
|
|
4513
4724
|
|
|
4514
4725
|
// src/storage/markdown-store.ts
|
|
4515
|
-
import { readFile as
|
|
4726
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
4516
4727
|
var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
|
|
4517
4728
|
var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
|
|
4518
4729
|
async function readMarkdownFile(filePath) {
|
|
4519
4730
|
try {
|
|
4520
|
-
return await
|
|
4731
|
+
return await readFile8(filePath, "utf8");
|
|
4521
4732
|
} catch (error) {
|
|
4522
4733
|
if (hasErrorCode6(error) && error.code === "ENOENT") return null;
|
|
4523
4734
|
throw new Error("Failed to read markdown file", { cause: error });
|
|
@@ -4615,9 +4826,9 @@ function hasErrorCode6(error) {
|
|
|
4615
4826
|
}
|
|
4616
4827
|
|
|
4617
4828
|
// src/storage/session-import.ts
|
|
4618
|
-
import { mkdir as
|
|
4829
|
+
import { mkdir as mkdir5, readFile as readFile9, rm as rm2 } from "fs/promises";
|
|
4619
4830
|
import { homedir as homedir2 } from "os";
|
|
4620
|
-
import { join as
|
|
4831
|
+
import { join as join15 } from "path";
|
|
4621
4832
|
async function importSessionFromJson(paths, manifest, payload, options) {
|
|
4622
4833
|
if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
|
|
4623
4834
|
throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
|
|
@@ -4642,21 +4853,22 @@ async function importSessionFromJson(paths, manifest, payload, options) {
|
|
|
4642
4853
|
pathSanitizeReport
|
|
4643
4854
|
};
|
|
4644
4855
|
}
|
|
4645
|
-
const sessionDir =
|
|
4856
|
+
const sessionDir = join15(paths.sessions, newSessionId);
|
|
4646
4857
|
try {
|
|
4647
|
-
await
|
|
4858
|
+
await mkdir5(sessionDir, { recursive: true });
|
|
4648
4859
|
} catch (error) {
|
|
4649
4860
|
throw new Error("Failed to create session directory", { cause: error });
|
|
4650
4861
|
}
|
|
4862
|
+
let chainResult;
|
|
4651
4863
|
try {
|
|
4652
|
-
await writeEventsBulk(sessionDir, rewrittenEvents);
|
|
4864
|
+
chainResult = await writeEventsBulk(sessionDir, rewrittenEvents, { chain: true });
|
|
4653
4865
|
} catch (error) {
|
|
4654
4866
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
4655
4867
|
throw error;
|
|
4656
4868
|
}
|
|
4657
4869
|
try {
|
|
4658
|
-
const sessionYamlPath =
|
|
4659
|
-
await linkYamlFile(sessionYamlPath, sessionRecord);
|
|
4870
|
+
const sessionYamlPath = join15(sessionDir, "session.yaml");
|
|
4871
|
+
await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
|
|
4660
4872
|
} catch (error) {
|
|
4661
4873
|
await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
4662
4874
|
if (findErrorCode(error, "EEXIST")) {
|
|
@@ -4713,6 +4925,16 @@ function assertChronologicalOrder(events) {
|
|
|
4713
4925
|
}
|
|
4714
4926
|
}
|
|
4715
4927
|
}
|
|
4928
|
+
function withIntegrity(record, chainResult) {
|
|
4929
|
+
if (chainResult === null) return record;
|
|
4930
|
+
return {
|
|
4931
|
+
...record,
|
|
4932
|
+
session: {
|
|
4933
|
+
...record.session,
|
|
4934
|
+
integrity: { head_hash: chainResult.headHash, event_count: chainResult.count }
|
|
4935
|
+
}
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4716
4938
|
function buildSessionRecord(input, manifest, newSessionId, options) {
|
|
4717
4939
|
const home = homedir2();
|
|
4718
4940
|
const workingDirectoryRaw = input.working_directory;
|
|
@@ -4813,9 +5035,13 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
|
|
|
4813
5035
|
async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
|
|
4814
5036
|
const sessionId = priorSessionId;
|
|
4815
5037
|
const importSource = freshPayload.session.source.kind;
|
|
4816
|
-
const sessionDir =
|
|
5038
|
+
const sessionDir = join15(paths.sessions, priorSessionId);
|
|
4817
5039
|
const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
|
|
4818
5040
|
try {
|
|
5041
|
+
const priorVerdict = await verifyEventsChain(paths, priorSessionId);
|
|
5042
|
+
if (priorVerdict.status === "tampered") {
|
|
5043
|
+
return { status: "skipped", reason: "prior_chain_broken" };
|
|
5044
|
+
}
|
|
4819
5045
|
let priorUnreadable = false;
|
|
4820
5046
|
const priorEvents = await readAllEvents(sessionDir, {
|
|
4821
5047
|
onWarning: () => {
|
|
@@ -4843,20 +5069,35 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
4843
5069
|
const { record } = buildSessionRecord(freshPayload.session, manifest, sessionId, {});
|
|
4844
5070
|
const preservedInner = {
|
|
4845
5071
|
...record.session,
|
|
4846
|
-
//
|
|
4847
|
-
//
|
|
4848
|
-
// sessions); never drop that link on a re-derive.
|
|
5072
|
+
// Defensive: keep any task_id already present on the prior yaml so a
|
|
5073
|
+
// re-derive never drops a link, whatever wrote it.
|
|
4849
5074
|
task_id: prior.session.task_id ?? null,
|
|
4850
5075
|
// Re-derivation always yields a null summary; keep a prior non-null one.
|
|
4851
5076
|
summary: prior.session.summary ?? record.session.summary ?? null
|
|
4852
5077
|
};
|
|
4853
5078
|
const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
|
|
4854
5079
|
if (options.dryRun !== true) {
|
|
4855
|
-
|
|
5080
|
+
const eventsPath = join15(sessionDir, "events.jsonl");
|
|
5081
|
+
let priorEventsRaw = null;
|
|
5082
|
+
try {
|
|
5083
|
+
priorEventsRaw = await readFile9(eventsPath);
|
|
5084
|
+
} catch (error) {
|
|
5085
|
+
if (!findErrorCode(error, "ENOENT")) {
|
|
5086
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
|
|
4856
5090
|
try {
|
|
4857
|
-
await overwriteYamlFile(
|
|
5091
|
+
await overwriteYamlFile(
|
|
5092
|
+
join15(sessionDir, "session.yaml"),
|
|
5093
|
+
withIntegrity(updatedRecord, chainResult)
|
|
5094
|
+
);
|
|
4858
5095
|
} catch (error) {
|
|
4859
|
-
|
|
5096
|
+
if (priorEventsRaw !== null) {
|
|
5097
|
+
await atomicReplace(eventsPath, priorEventsRaw).catch(() => void 0);
|
|
5098
|
+
} else {
|
|
5099
|
+
await rm2(eventsPath, { force: true }).catch(() => void 0);
|
|
5100
|
+
}
|
|
4860
5101
|
throw error;
|
|
4861
5102
|
}
|
|
4862
5103
|
}
|
|
@@ -4871,6 +5112,100 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
|
|
|
4871
5112
|
await lock?.release();
|
|
4872
5113
|
}
|
|
4873
5114
|
}
|
|
5115
|
+
async function rechainSessionInPlace(paths, sessionId, options = {}) {
|
|
5116
|
+
const sessionDir = join15(paths.sessions, sessionId);
|
|
5117
|
+
let lock;
|
|
5118
|
+
try {
|
|
5119
|
+
lock = await acquireLock(paths, "session", sessionId);
|
|
5120
|
+
} catch (error) {
|
|
5121
|
+
if (error instanceof Error && error.message === "Lock is held by another process") {
|
|
5122
|
+
throw error;
|
|
5123
|
+
}
|
|
5124
|
+
throw new Error("Failed to acquire lock", { cause: error });
|
|
5125
|
+
}
|
|
5126
|
+
try {
|
|
5127
|
+
let record;
|
|
5128
|
+
try {
|
|
5129
|
+
record = await readSessionYaml(paths, sessionId);
|
|
5130
|
+
} catch (error) {
|
|
5131
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
5132
|
+
return { status: "skipped", reason: "yaml_missing" };
|
|
5133
|
+
}
|
|
5134
|
+
return { status: "skipped", reason: "yaml_unreadable" };
|
|
5135
|
+
}
|
|
5136
|
+
if (record.session.status !== "imported") {
|
|
5137
|
+
return { status: "skipped", reason: "not_imported" };
|
|
5138
|
+
}
|
|
5139
|
+
const verdict = await verifyEventsChain(paths, sessionId);
|
|
5140
|
+
if (verdict.status === "verified") {
|
|
5141
|
+
return { status: "skipped", reason: "already_chained" };
|
|
5142
|
+
}
|
|
5143
|
+
if (verdict.status === "empty") {
|
|
5144
|
+
return { status: "skipped", reason: "empty" };
|
|
5145
|
+
}
|
|
5146
|
+
if (verdict.status !== "unchained") {
|
|
5147
|
+
return { status: "skipped", reason: "tampered" };
|
|
5148
|
+
}
|
|
5149
|
+
const eventsPath = join15(sessionDir, "events.jsonl");
|
|
5150
|
+
let priorRaw;
|
|
5151
|
+
try {
|
|
5152
|
+
priorRaw = await readFile9(eventsPath);
|
|
5153
|
+
} catch (error) {
|
|
5154
|
+
throw new Error("Failed to read events.jsonl", { cause: error });
|
|
5155
|
+
}
|
|
5156
|
+
if (priorRaw.length === 0 || priorRaw[priorRaw.length - 1] !== 10) {
|
|
5157
|
+
return { status: "skipped", reason: "events_unreadable" };
|
|
5158
|
+
}
|
|
5159
|
+
const text = priorRaw.toString("utf8");
|
|
5160
|
+
if (!priorRaw.equals(Buffer.from(text, "utf8"))) {
|
|
5161
|
+
return { status: "skipped", reason: "events_unreadable" };
|
|
5162
|
+
}
|
|
5163
|
+
const rawLines = text.slice(0, -1).split("\n");
|
|
5164
|
+
for (const line of rawLines) {
|
|
5165
|
+
if (line.trim().length === 0) {
|
|
5166
|
+
return { status: "skipped", reason: "events_unreadable" };
|
|
5167
|
+
}
|
|
5168
|
+
let parsed;
|
|
5169
|
+
try {
|
|
5170
|
+
parsed = JSON.parse(line);
|
|
5171
|
+
} catch {
|
|
5172
|
+
return { status: "skipped", reason: "events_unreadable" };
|
|
5173
|
+
}
|
|
5174
|
+
if (JSON.stringify(parsed) !== line) {
|
|
5175
|
+
return { status: "skipped", reason: "events_unreadable" };
|
|
5176
|
+
}
|
|
5177
|
+
if (!EventSchema.safeParse(parsed).success) {
|
|
5178
|
+
return { status: "skipped", reason: "events_unreadable" };
|
|
5179
|
+
}
|
|
5180
|
+
if (parsed.session_id !== sessionId) {
|
|
5181
|
+
return { status: "skipped", reason: "session_id_mismatch" };
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
if (options.dryRun === true) {
|
|
5185
|
+
return { status: "rechained", eventCount: rawLines.length };
|
|
5186
|
+
}
|
|
5187
|
+
const chainResult = chainRawJsonLines(rawLines, sessionId);
|
|
5188
|
+
const body = `${chainResult.lines.join("\n")}
|
|
5189
|
+
`;
|
|
5190
|
+
try {
|
|
5191
|
+
await atomicReplace(eventsPath, body);
|
|
5192
|
+
} catch (error) {
|
|
5193
|
+
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
5194
|
+
}
|
|
5195
|
+
try {
|
|
5196
|
+
await overwriteYamlFile(
|
|
5197
|
+
join15(sessionDir, "session.yaml"),
|
|
5198
|
+
withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
|
|
5199
|
+
);
|
|
5200
|
+
} catch (error) {
|
|
5201
|
+
await atomicReplace(eventsPath, priorRaw).catch(() => void 0);
|
|
5202
|
+
throw error;
|
|
5203
|
+
}
|
|
5204
|
+
return { status: "rechained", eventCount: chainResult.count };
|
|
5205
|
+
} finally {
|
|
5206
|
+
await lock.release();
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
4874
5209
|
|
|
4875
5210
|
// src/index.ts
|
|
4876
5211
|
var BASOU_CORE_VERSION = "0.1.0";
|
|
@@ -4900,6 +5235,7 @@ export {
|
|
|
4900
5235
|
SessionIdSchema,
|
|
4901
5236
|
SessionImportPayloadSchema,
|
|
4902
5237
|
SessionInnerImportSchema,
|
|
5238
|
+
SessionIntegritySchema,
|
|
4903
5239
|
SessionMetricsSchema,
|
|
4904
5240
|
SessionSchema,
|
|
4905
5241
|
SessionSourceKindSchema,
|
|
@@ -4919,6 +5255,8 @@ export {
|
|
|
4919
5255
|
basouPaths,
|
|
4920
5256
|
buildJsonSchemas,
|
|
4921
5257
|
buildStatusSnapshot,
|
|
5258
|
+
chainEvents,
|
|
5259
|
+
chainRawJsonLines,
|
|
4922
5260
|
classifySuspect,
|
|
4923
5261
|
claudeCodeAdapterMetadata,
|
|
4924
5262
|
claudeTranscriptToImportPayload,
|
|
@@ -4935,12 +5273,14 @@ export {
|
|
|
4935
5273
|
enumerateSessionDirs,
|
|
4936
5274
|
enumerateTaskIds,
|
|
4937
5275
|
findErrorCode,
|
|
5276
|
+
genesisHash,
|
|
4938
5277
|
getDiff,
|
|
4939
5278
|
getSnapshot,
|
|
4940
5279
|
importSessionFromJson,
|
|
4941
5280
|
isImportDerivedSource,
|
|
4942
5281
|
isLazyExpired,
|
|
4943
5282
|
isValidPrefixedId,
|
|
5283
|
+
lineHash,
|
|
4944
5284
|
linkYamlFile,
|
|
4945
5285
|
loadApproval,
|
|
4946
5286
|
loadSessionEntries,
|
|
@@ -4957,6 +5297,7 @@ export {
|
|
|
4957
5297
|
readTaskFile,
|
|
4958
5298
|
readTaskFileWithArchiveFallback,
|
|
4959
5299
|
readYamlFile,
|
|
5300
|
+
rechainSessionInPlace,
|
|
4960
5301
|
reconcileAllTasks,
|
|
4961
5302
|
reconcileTask,
|
|
4962
5303
|
refreshTaskLinkedSessions,
|
|
@@ -4972,12 +5313,14 @@ export {
|
|
|
4972
5313
|
sanitizePath,
|
|
4973
5314
|
sanitizeRelatedFiles,
|
|
4974
5315
|
sanitizeWorkingDirectory,
|
|
5316
|
+
serializeEventLine,
|
|
4975
5317
|
serializeJsonSchema,
|
|
4976
5318
|
sessionWorkStatsFromEvents,
|
|
4977
5319
|
summarizeAdapterOutput,
|
|
4978
5320
|
tryRemoteUrl,
|
|
4979
5321
|
ulid,
|
|
4980
5322
|
updateTaskStatusWithEvent,
|
|
5323
|
+
verifyEventsChain,
|
|
4981
5324
|
writeEventsBulk,
|
|
4982
5325
|
writeManifest,
|
|
4983
5326
|
writeMarkdownFile,
|