@basou/cli 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/program.js CHANGED
@@ -8,7 +8,8 @@ import { join } from "path";
8
8
  import {
9
9
  ApprovalSchema,
10
10
  ApprovalStatusSchema,
11
- appendEvent,
11
+ acquireLock,
12
+ appendChainedEventLocked,
12
13
  assertBasouRootSafe,
13
14
  basouPaths,
14
15
  enumerateApprovals,
@@ -320,55 +321,63 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
320
321
  if (approval.status !== "pending") {
321
322
  throw new Error(`Approval status mismatch: pending YAML has status=${approval.status}`);
322
323
  }
323
- const sessionDir = join(paths.sessions, approval.session_id);
324
- for await (const ev of replayEvents(sessionDir, {
325
- onWarning: (w) => printReplayWarning(w, approval.session_id)
326
- })) {
327
- if (isApprovalEvent(ev) && ev.approval_id === approval.id && (ev.type === "approval_approved" || ev.type === "approval_rejected" || ev.type === "approval_expired")) {
328
- throw new Error(`Approval already resolved (per events.jsonl): ${idInput}`);
329
- }
330
- }
331
324
  const now = /* @__PURE__ */ new Date();
332
- if (isLazyExpired(approval, now)) {
333
- throw new Error(`Approval already expired: ${idInput}`);
334
- }
335
- let sessionStatus = null;
336
- try {
337
- sessionStatus = (await readSessionYaml(paths, approval.session_id)).session.status;
338
- } catch {
339
- sessionStatus = null;
340
- }
341
- if (sessionStatus === "imported") {
342
- throw new Error(`Cannot resolve an approval for an imported session: ${idInput}`);
343
- }
344
325
  const occurredAt = now.toISOString();
345
326
  const eventId = prefixedUlid("evt");
346
- if (decision === "approve") {
347
- const note = options.note ?? null;
348
- await appendEvent(sessionDir, {
349
- schema_version: "0.1.0",
350
- id: eventId,
351
- session_id: approval.session_id,
352
- occurred_at: occurredAt,
353
- source: "local-cli",
354
- type: "approval_approved",
355
- approval_id: approval.id,
356
- resolver: "local-cli",
357
- note
358
- });
359
- } else {
360
- const reason = options.reason;
361
- await appendEvent(sessionDir, {
362
- schema_version: "0.1.0",
363
- id: eventId,
364
- session_id: approval.session_id,
365
- occurred_at: occurredAt,
366
- source: "local-cli",
367
- type: "approval_rejected",
368
- approval_id: approval.id,
369
- resolver: "local-cli",
370
- reason
371
- });
327
+ const sessionLock = await acquireLock(paths, "session", approval.session_id);
328
+ try {
329
+ const sessionDir = join(paths.sessions, approval.session_id);
330
+ for await (const ev of replayEvents(sessionDir, {
331
+ onWarning: (w) => printReplayWarning(w, approval.session_id)
332
+ })) {
333
+ if (isApprovalEvent(ev) && ev.approval_id === approval.id && (ev.type === "approval_approved" || ev.type === "approval_rejected" || ev.type === "approval_expired")) {
334
+ throw new Error(`Approval already resolved (per events.jsonl): ${idInput}`);
335
+ }
336
+ }
337
+ if (isLazyExpired(approval, now)) {
338
+ throw new Error(`Approval already expired: ${idInput}`);
339
+ }
340
+ let sessionStatus = null;
341
+ try {
342
+ sessionStatus = (await readSessionYaml(paths, approval.session_id)).session.status;
343
+ } catch {
344
+ sessionStatus = null;
345
+ }
346
+ const attachable = sessionStatus === "initialized" || sessionStatus === "running" || sessionStatus === "waiting_approval";
347
+ if (sessionStatus !== null && !attachable) {
348
+ throw new Error(
349
+ `Cannot resolve an approval for a session that is not active (status=${sessionStatus}): ${idInput}`
350
+ );
351
+ }
352
+ if (decision === "approve") {
353
+ const note = options.note ?? null;
354
+ await appendChainedEventLocked(paths, approval.session_id, {
355
+ schema_version: "0.1.0",
356
+ id: eventId,
357
+ session_id: approval.session_id,
358
+ occurred_at: occurredAt,
359
+ source: "local-cli",
360
+ type: "approval_approved",
361
+ approval_id: approval.id,
362
+ resolver: "local-cli",
363
+ note
364
+ });
365
+ } else {
366
+ const reason = options.reason;
367
+ await appendChainedEventLocked(paths, approval.session_id, {
368
+ schema_version: "0.1.0",
369
+ id: eventId,
370
+ session_id: approval.session_id,
371
+ occurred_at: occurredAt,
372
+ source: "local-cli",
373
+ type: "approval_rejected",
374
+ approval_id: approval.id,
375
+ resolver: "local-cli",
376
+ reason
377
+ });
378
+ }
379
+ } finally {
380
+ await sessionLock.release();
372
381
  }
373
382
  const resolvedApproval = decision === "approve" ? {
374
383
  ...approval,
@@ -625,7 +634,7 @@ function printNoApprovals(options) {
625
634
 
626
635
  // src/commands/decision.ts
627
636
  import {
628
- acquireLock,
637
+ acquireLock as acquireLock2,
629
638
  appendEventToExistingSession,
630
639
  assertBasouRootSafe as assertBasouRootSafe2,
631
640
  basouPaths as basouPaths2,
@@ -690,7 +699,7 @@ async function doRunDecisionRecord(options, ctx) {
690
699
  if (options.session !== void 0) {
691
700
  const sessionId = await resolveSessionId(paths, options.session);
692
701
  const sesId = sessionId;
693
- const sessionLock = await acquireLock(paths, "session", sesId);
702
+ const sessionLock = await acquireLock2(paths, "session", sesId);
694
703
  let result;
695
704
  try {
696
705
  result = await appendEventToExistingSession({
@@ -954,10 +963,12 @@ import { mkdir } from "fs/promises";
954
963
  import { homedir } from "os";
955
964
  import { join as join2 } from "path";
956
965
  import {
966
+ acquireLock as acquireLock3,
957
967
  assertBasouRootSafe as assertBasouRootSafe4,
958
968
  basouPaths as basouPaths4,
959
969
  ChildProcessRunner,
960
- appendEvent as coreAppendEvent,
970
+ appendChainedEvent as coreAppendChainedEvent,
971
+ finalizeSessionYaml,
961
972
  getSnapshot,
962
973
  overwriteYamlFile,
963
974
  parseDuration,
@@ -983,7 +994,6 @@ function registerExecCommand(program) {
983
994
  async function runExec(command, args, options, ctx = {}) {
984
995
  const runner = ctx.runner ?? new ChildProcessRunner();
985
996
  const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
986
- const appendEvent2 = ctx.appendEvent ?? coreAppendEvent;
987
997
  const cwd = options.cwd ?? process.cwd();
988
998
  const timeout_ms = options.timeout !== void 0 ? parseDuration(options.timeout) : void 0;
989
999
  const repoRoot = await resolveRepositoryRootForExec(cwd);
@@ -993,6 +1003,9 @@ async function runExec(command, args, options, ctx = {}) {
993
1003
  const sessionId = prefixedUlid3("ses");
994
1004
  const sessionDir = join2(paths.sessions, sessionId);
995
1005
  await mkdir(sessionDir, { recursive: true });
1006
+ const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
1007
+ await coreAppendChainedEvent(paths, sessionId, event);
1008
+ });
996
1009
  const startedAt = now().toISOString();
997
1010
  const sessionYamlPath = join2(sessionDir, "session.yaml");
998
1011
  const session = buildInitialSession({
@@ -1004,7 +1017,7 @@ async function runExec(command, args, options, ctx = {}) {
1004
1017
  startedAt
1005
1018
  });
1006
1019
  await writeYamlFile(sessionYamlPath, session);
1007
- await appendEvent2(sessionDir, {
1020
+ await appendEvent(sessionDir, {
1008
1021
  schema_version: "0.1.0",
1009
1022
  type: "session_started",
1010
1023
  id: prefixedUlid3("evt"),
@@ -1013,10 +1026,10 @@ async function runExec(command, args, options, ctx = {}) {
1013
1026
  source: "terminal-recording"
1014
1027
  });
1015
1028
  if (options.snapshot !== false) {
1016
- await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent2);
1029
+ await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent);
1017
1030
  }
1018
1031
  const runningAt = now().toISOString();
1019
- await appendEvent2(sessionDir, {
1032
+ await appendEvent(sessionDir, {
1020
1033
  schema_version: "0.1.0",
1021
1034
  type: "session_status_changed",
1022
1035
  id: prefixedUlid3("evt"),
@@ -1026,9 +1039,14 @@ async function runExec(command, args, options, ctx = {}) {
1026
1039
  from: "initialized",
1027
1040
  to: "running"
1028
1041
  });
1029
- await mutateSessionYaml(sessionYamlPath, (s) => {
1030
- s.session.status = "running";
1031
- });
1042
+ const runningLock = await acquireLock3(paths, "session", sessionId);
1043
+ try {
1044
+ await mutateSessionYaml(sessionYamlPath, (s) => {
1045
+ s.session.status = "running";
1046
+ });
1047
+ } finally {
1048
+ await runningLock.release();
1049
+ }
1032
1050
  const controller = new AbortController();
1033
1051
  let signalReceived = null;
1034
1052
  let activeChild = null;
@@ -1064,7 +1082,7 @@ async function runExec(command, args, options, ctx = {}) {
1064
1082
  }
1065
1083
  });
1066
1084
  } catch (spawnError) {
1067
- await finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, appendEvent2, {
1085
+ await finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent, {
1068
1086
  command,
1069
1087
  args,
1070
1088
  cwd,
@@ -1080,7 +1098,7 @@ async function runExec(command, args, options, ctx = {}) {
1080
1098
  activeChild = null;
1081
1099
  }
1082
1100
  const endedAt = now().toISOString();
1083
- await appendEvent2(sessionDir, {
1101
+ await appendEvent(sessionDir, {
1084
1102
  schema_version: "0.1.0",
1085
1103
  type: "command_executed",
1086
1104
  id: prefixedUlid3("evt"),
@@ -1096,10 +1114,10 @@ async function runExec(command, args, options, ctx = {}) {
1096
1114
  duration_ms: result.duration_ms
1097
1115
  });
1098
1116
  if (options.snapshot !== false) {
1099
- await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent2);
1117
+ await tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent);
1100
1118
  }
1101
1119
  const finalStatus = decideFinalStatus(result, signalReceived);
1102
- await appendEvent2(sessionDir, {
1120
+ await appendEvent(sessionDir, {
1103
1121
  schema_version: "0.1.0",
1104
1122
  type: "session_status_changed",
1105
1123
  id: prefixedUlid3("evt"),
@@ -1109,7 +1127,7 @@ async function runExec(command, args, options, ctx = {}) {
1109
1127
  from: "running",
1110
1128
  to: finalStatus
1111
1129
  });
1112
- await appendEvent2(sessionDir, {
1130
+ await appendEvent(sessionDir, {
1113
1131
  schema_version: "0.1.0",
1114
1132
  type: "session_ended",
1115
1133
  id: prefixedUlid3("evt"),
@@ -1118,7 +1136,7 @@ async function runExec(command, args, options, ctx = {}) {
1118
1136
  source: "terminal-recording",
1119
1137
  ...result.exit_code !== null ? { exit_code: result.exit_code } : {}
1120
1138
  });
1121
- await mutateSessionYaml(sessionYamlPath, (s) => {
1139
+ await finalizeSessionYaml(paths, sessionId, (s) => {
1122
1140
  s.session.status = finalStatus;
1123
1141
  s.session.ended_at = endedAt;
1124
1142
  s.session.invocation.exit_code = result.exit_code;
@@ -1148,7 +1166,7 @@ function signalToExitCode(sig) {
1148
1166
  const num = SIGNUM_MAP[sig] ?? 1;
1149
1167
  return 128 + num;
1150
1168
  }
1151
- async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent2) {
1169
+ async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, appendEvent) {
1152
1170
  let snapshot;
1153
1171
  try {
1154
1172
  snapshot = await getSnapshot(repoRoot);
@@ -1156,7 +1174,7 @@ async function tryAppendGitSnapshot(sessionDir, sessionId, repoRoot, now, append
1156
1174
  console.warn(normalizeGitSnapshotSkipMessage(error));
1157
1175
  return;
1158
1176
  }
1159
- await appendEvent2(sessionDir, {
1177
+ await appendEvent(sessionDir, {
1160
1178
  schema_version: "0.1.0",
1161
1179
  type: "git_snapshot",
1162
1180
  id: prefixedUlid3("evt"),
@@ -1212,8 +1230,8 @@ async function mutateSessionYaml(filePath, mutator) {
1212
1230
  const validated = SessionSchema.parse(parsed);
1213
1231
  await overwriteYamlFile(filePath, validated);
1214
1232
  }
1215
- async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, appendEvent2, ctx) {
1216
- await appendEvent2(sessionDir, {
1233
+ async function finalizeSessionAsFailed(paths, sessionDir, sessionId, appendEvent, ctx) {
1234
+ await appendEvent(sessionDir, {
1217
1235
  schema_version: "0.1.0",
1218
1236
  type: "command_executed",
1219
1237
  id: prefixedUlid3("evt"),
@@ -1228,7 +1246,7 @@ async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, a
1228
1246
  ...ctx.signalReceived !== null ? { received_signal: ctx.signalReceived } : {},
1229
1247
  duration_ms: 0
1230
1248
  });
1231
- await appendEvent2(sessionDir, {
1249
+ await appendEvent(sessionDir, {
1232
1250
  schema_version: "0.1.0",
1233
1251
  type: "session_status_changed",
1234
1252
  id: prefixedUlid3("evt"),
@@ -1238,7 +1256,7 @@ async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, a
1238
1256
  from: "running",
1239
1257
  to: "failed"
1240
1258
  });
1241
- await appendEvent2(sessionDir, {
1259
+ await appendEvent(sessionDir, {
1242
1260
  schema_version: "0.1.0",
1243
1261
  type: "session_ended",
1244
1262
  id: prefixedUlid3("evt"),
@@ -1246,7 +1264,7 @@ async function finalizeSessionAsFailed(sessionDir, sessionYamlPath, sessionId, a
1246
1264
  occurred_at: ctx.occurredAt,
1247
1265
  source: "terminal-recording"
1248
1266
  });
1249
- await mutateSessionYaml(sessionYamlPath, (s) => {
1267
+ await finalizeSessionYaml(paths, sessionId, (s) => {
1250
1268
  s.session.status = "failed";
1251
1269
  s.session.ended_at = ctx.occurredAt;
1252
1270
  s.session.invocation.exit_code = null;
@@ -2241,19 +2259,19 @@ function parseInterval(value) {
2241
2259
  return seconds;
2242
2260
  }
2243
2261
  function abortableSleep(ms, signal) {
2244
- return new Promise((resolve3) => {
2262
+ return new Promise((resolve4) => {
2245
2263
  if (signal.aborted) {
2246
- resolve3();
2264
+ resolve4();
2247
2265
  return;
2248
2266
  }
2249
2267
  let timer;
2250
2268
  const onAbort = () => {
2251
2269
  clearTimeout(timer);
2252
- resolve3();
2270
+ resolve4();
2253
2271
  };
2254
2272
  timer = setTimeout(() => {
2255
2273
  signal.removeEventListener("abort", onAbort);
2256
- resolve3();
2274
+ resolve4();
2257
2275
  }, ms);
2258
2276
  signal.addEventListener("abort", onAbort, { once: true });
2259
2277
  });
@@ -2393,16 +2411,96 @@ async function assertWorkspaceInitialized6(basouRoot) {
2393
2411
  }
2394
2412
  }
2395
2413
 
2414
+ // src/commands/report.ts
2415
+ import { isAbsolute, resolve as resolve3 } from "path";
2416
+ import {
2417
+ assertBasouRootSafe as assertBasouRootSafe8,
2418
+ basouPaths as basouPaths8,
2419
+ findErrorCode as findErrorCode8,
2420
+ renderReport,
2421
+ resolveRepositoryRoot as resolveRepositoryRoot9,
2422
+ writeMarkdownFile as writeMarkdownFile4
2423
+ } from "@basou/core";
2424
+ function registerReportCommand(program) {
2425
+ const report = program.command("report").description(
2426
+ "Generate a work report \u2014 a shareable export explaining the work in this workspace"
2427
+ );
2428
+ report.command("generate").description("Generate a work report from the current workspace state").option("--out <path>", "Write the markdown report to a file instead of stdout").option("--json", "Emit the structured report data as JSON to stdout").option("--title <text>", "Subject line shown in the report header").option("-v, --verbose", "Show error causes").action(async (opts) => {
2429
+ await runReportGenerate(opts);
2430
+ });
2431
+ }
2432
+ async function runReportGenerate(options, ctx = {}) {
2433
+ try {
2434
+ await doRunReportGenerate(options, ctx);
2435
+ } catch (error) {
2436
+ renderCliError(error, { verbose: isVerbose(options) });
2437
+ process.exitCode = 1;
2438
+ }
2439
+ }
2440
+ async function doRunReportGenerate(options, ctx) {
2441
+ const cwd = ctx.cwd ?? process.cwd();
2442
+ const repositoryRoot = await resolveRepositoryRootForReport(cwd);
2443
+ const paths = basouPaths8(repositoryRoot);
2444
+ await assertWorkspaceInitialized7(paths.root);
2445
+ const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2446
+ const result = await renderReport({
2447
+ paths,
2448
+ nowIso,
2449
+ ...options.title !== void 0 ? { title: options.title } : {},
2450
+ onWarning: (w, sid) => printReplayWarning(w, sid),
2451
+ onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
2452
+ onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2453
+ });
2454
+ if (options.out !== void 0) {
2455
+ const outPath = isAbsolute(options.out) ? options.out : resolve3(cwd, options.out);
2456
+ await writeMarkdownFile4(outPath, result.body);
2457
+ const { sessions, decisions, tasks } = result.data;
2458
+ console.error(
2459
+ `Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
2460
+ );
2461
+ }
2462
+ if (options.json === true) {
2463
+ console.log(JSON.stringify(result.data, null, 2));
2464
+ } else if (options.out === void 0) {
2465
+ console.log(result.body);
2466
+ }
2467
+ }
2468
+ async function resolveRepositoryRootForReport(cwd) {
2469
+ try {
2470
+ return await resolveRepositoryRoot9(cwd);
2471
+ } catch (error) {
2472
+ if (error instanceof Error && error.message === "Not a git repository") {
2473
+ throw new Error(
2474
+ "Not a git repository. Run 'git init' first, then re-run 'basou report generate'.",
2475
+ { cause: error }
2476
+ );
2477
+ }
2478
+ throw error;
2479
+ }
2480
+ }
2481
+ async function assertWorkspaceInitialized7(basouRoot) {
2482
+ try {
2483
+ await assertBasouRootSafe8(basouRoot);
2484
+ } catch (error) {
2485
+ if (findErrorCode8(error, "ENOENT")) {
2486
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
2487
+ }
2488
+ throw error;
2489
+ }
2490
+ }
2491
+
2396
2492
  // src/commands/run.ts
2397
2493
  import { mkdir as mkdir2 } from "fs/promises";
2398
2494
  import { homedir as homedir4 } from "os";
2399
2495
  import { join as join5 } from "path";
2400
2496
  import {
2401
- assertBasouRootSafe as assertBasouRootSafe8,
2402
- basouPaths as basouPaths8,
2497
+ acquireLock as acquireLock4,
2498
+ assertBasouRootSafe as assertBasouRootSafe9,
2499
+ basouPaths as basouPaths9,
2403
2500
  ChildProcessRunner as ChildProcessRunner2,
2404
2501
  claudeCodeAdapterMetadata,
2405
- appendEvent as coreAppendEvent2,
2502
+ appendChainedEvent as coreAppendChainedEvent2,
2503
+ finalizeSessionYaml as finalizeSessionYaml2,
2406
2504
  getDiff,
2407
2505
  getSnapshot as getSnapshot2,
2408
2506
  overwriteYamlFile as overwriteYamlFile2,
@@ -2410,7 +2508,7 @@ import {
2410
2508
  readManifest as readManifest4,
2411
2509
  readYamlFile as readYamlFile3,
2412
2510
  resolveClaudeCodeCommand,
2413
- resolveRepositoryRoot as resolveRepositoryRoot9,
2511
+ resolveRepositoryRoot as resolveRepositoryRoot10,
2414
2512
  SessionSchema as SessionSchema2,
2415
2513
  sanitizeRelatedFiles,
2416
2514
  sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
@@ -2438,18 +2536,20 @@ function registerRunCommand(program, ctx = {}) {
2438
2536
  async function runClaudeCode(args, options, ctx = {}) {
2439
2537
  const runner = ctx.runner ?? new ChildProcessRunner2();
2440
2538
  const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
2441
- const appendEvent2 = ctx.appendEvent ?? coreAppendEvent2;
2442
2539
  const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
2443
2540
  const getDiffFn = ctx.getDiff ?? getDiff;
2444
2541
  const { command } = await resolveCommand();
2445
2542
  const cwd = options.cwd ?? process.cwd();
2446
2543
  const repoRoot = await resolveRepositoryRootForRun(cwd);
2447
- const paths = basouPaths8(repoRoot);
2448
- await assertBasouRootSafe8(paths.root);
2544
+ const paths = basouPaths9(repoRoot);
2545
+ await assertBasouRootSafe9(paths.root);
2449
2546
  const manifest = await readManifest4(paths);
2450
2547
  const sessionId = prefixedUlid4("ses");
2451
2548
  const sessionDir = join5(paths.sessions, sessionId);
2452
2549
  await mkdir2(sessionDir, { recursive: true });
2550
+ const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
2551
+ await coreAppendChainedEvent2(paths, sessionId, event);
2552
+ });
2453
2553
  const startedAt = now().toISOString();
2454
2554
  const sessionYamlPath = join5(sessionDir, "session.yaml");
2455
2555
  const session = buildInitialSession2({
@@ -2461,7 +2561,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2461
2561
  startedAt
2462
2562
  });
2463
2563
  await writeYamlFile2(sessionYamlPath, session);
2464
- await appendEvent2(sessionDir, {
2564
+ await appendEvent(sessionDir, {
2465
2565
  schema_version: "0.1.0",
2466
2566
  type: "session_started",
2467
2567
  id: prefixedUlid4("evt"),
@@ -2471,10 +2571,10 @@ async function runClaudeCode(args, options, ctx = {}) {
2471
2571
  });
2472
2572
  let preSnapshot = null;
2473
2573
  if (options.snapshot !== false) {
2474
- preSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent2);
2574
+ preSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent);
2475
2575
  }
2476
2576
  const runningAt = now().toISOString();
2477
- await appendEvent2(sessionDir, {
2577
+ await appendEvent(sessionDir, {
2478
2578
  schema_version: "0.1.0",
2479
2579
  type: "session_status_changed",
2480
2580
  id: prefixedUlid4("evt"),
@@ -2484,9 +2584,14 @@ async function runClaudeCode(args, options, ctx = {}) {
2484
2584
  from: "initialized",
2485
2585
  to: "running"
2486
2586
  });
2487
- await mutateSessionYaml2(sessionYamlPath, (s) => {
2488
- s.session.status = "running";
2489
- });
2587
+ const runningLock = await acquireLock4(paths, "session", sessionId);
2588
+ try {
2589
+ await mutateSessionYaml2(sessionYamlPath, (s) => {
2590
+ s.session.status = "running";
2591
+ });
2592
+ } finally {
2593
+ await runningLock.release();
2594
+ }
2490
2595
  const controller = new AbortController();
2491
2596
  let signalReceived = null;
2492
2597
  let activeChild = null;
@@ -2521,7 +2626,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2521
2626
  }
2522
2627
  });
2523
2628
  } catch (spawnError) {
2524
- await finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId, appendEvent2, {
2629
+ await finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, {
2525
2630
  command,
2526
2631
  args,
2527
2632
  cwd: repoRoot,
@@ -2537,7 +2642,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2537
2642
  activeChild = null;
2538
2643
  }
2539
2644
  const endedAt = now().toISOString();
2540
- await appendEvent2(sessionDir, {
2645
+ await appendEvent(sessionDir, {
2541
2646
  schema_version: "0.1.0",
2542
2647
  type: "command_executed",
2543
2648
  id: prefixedUlid4("evt"),
@@ -2554,7 +2659,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2554
2659
  });
2555
2660
  let postSnapshot = null;
2556
2661
  if (options.snapshot !== false) {
2557
- postSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent2);
2662
+ postSnapshot = await tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent);
2558
2663
  }
2559
2664
  let diff = null;
2560
2665
  if (preSnapshot !== null && postSnapshot !== null) {
@@ -2565,7 +2670,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2565
2670
  preSnapshot.head,
2566
2671
  postSnapshot.head,
2567
2672
  now().toISOString(),
2568
- appendEvent2,
2673
+ appendEvent,
2569
2674
  getDiffFn
2570
2675
  );
2571
2676
  }
@@ -2575,7 +2680,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2575
2680
  homedir: homedir4()
2576
2681
  }).sanitized;
2577
2682
  const finalStatus = decideFinalStatus2(result, signalReceived);
2578
- await appendEvent2(sessionDir, {
2683
+ await appendEvent(sessionDir, {
2579
2684
  schema_version: "0.1.0",
2580
2685
  type: "session_status_changed",
2581
2686
  id: prefixedUlid4("evt"),
@@ -2585,7 +2690,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2585
2690
  from: "running",
2586
2691
  to: finalStatus
2587
2692
  });
2588
- await appendEvent2(sessionDir, {
2693
+ await appendEvent(sessionDir, {
2589
2694
  schema_version: "0.1.0",
2590
2695
  type: "session_ended",
2591
2696
  id: prefixedUlid4("evt"),
@@ -2594,7 +2699,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2594
2699
  source: claudeCodeAdapterMetadata.kind,
2595
2700
  ...result.exit_code !== null ? { exit_code: result.exit_code } : {}
2596
2701
  });
2597
- await mutateSessionYaml2(sessionYamlPath, (s) => {
2702
+ await finalizeSessionYaml2(paths, sessionId, (s) => {
2598
2703
  s.session.status = finalStatus;
2599
2704
  s.session.ended_at = endedAt;
2600
2705
  s.session.invocation.exit_code = result.exit_code;
@@ -2623,7 +2728,7 @@ function signalToExitCode2(sig) {
2623
2728
  const num = SIGNUM_MAP2[sig] ?? 1;
2624
2729
  return 128 + num;
2625
2730
  }
2626
- async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent2) {
2731
+ async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appendEvent) {
2627
2732
  let snapshot;
2628
2733
  try {
2629
2734
  snapshot = await getSnapshot2(repoRoot);
@@ -2631,7 +2736,7 @@ async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appen
2631
2736
  console.warn(normalizeGitSnapshotSkipMessage2(error));
2632
2737
  return null;
2633
2738
  }
2634
- await appendEvent2(sessionDir, {
2739
+ await appendEvent(sessionDir, {
2635
2740
  schema_version: "0.1.0",
2636
2741
  type: "git_snapshot",
2637
2742
  id: prefixedUlid4("evt"),
@@ -2642,7 +2747,7 @@ async function tryAppendGitSnapshot2(sessionDir, sessionId, repoRoot, now, appen
2642
2747
  });
2643
2748
  return snapshot;
2644
2749
  }
2645
- async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseRef, headRef, occurredAt, appendEvent2, getDiffFn) {
2750
+ async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseRef, headRef, occurredAt, appendEvent, getDiffFn) {
2646
2751
  let diff;
2647
2752
  try {
2648
2753
  diff = await getDiffFn(repoRoot, baseRef, headRef);
@@ -2651,7 +2756,7 @@ async function tryAppendFileChangedEvents(sessionDir, sessionId, repoRoot, baseR
2651
2756
  return null;
2652
2757
  }
2653
2758
  for (const change of diff.changed_files) {
2654
- await appendEvent2(sessionDir, {
2759
+ await appendEvent(sessionDir, {
2655
2760
  schema_version: "0.1.0",
2656
2761
  type: "file_changed",
2657
2762
  id: prefixedUlid4("evt"),
@@ -2734,8 +2839,8 @@ async function mutateSessionYaml2(filePath, mutator) {
2734
2839
  const validated = SessionSchema2.parse(parsed);
2735
2840
  await overwriteYamlFile2(filePath, validated);
2736
2841
  }
2737
- async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId, appendEvent2, ctx) {
2738
- await appendEvent2(sessionDir, {
2842
+ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, ctx) {
2843
+ await appendEvent(sessionDir, {
2739
2844
  schema_version: "0.1.0",
2740
2845
  type: "command_executed",
2741
2846
  id: prefixedUlid4("evt"),
@@ -2750,7 +2855,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
2750
2855
  ...ctx.signalReceived !== null ? { received_signal: ctx.signalReceived } : {},
2751
2856
  duration_ms: 0
2752
2857
  });
2753
- await appendEvent2(sessionDir, {
2858
+ await appendEvent(sessionDir, {
2754
2859
  schema_version: "0.1.0",
2755
2860
  type: "session_status_changed",
2756
2861
  id: prefixedUlid4("evt"),
@@ -2760,7 +2865,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
2760
2865
  from: "running",
2761
2866
  to: "failed"
2762
2867
  });
2763
- await appendEvent2(sessionDir, {
2868
+ await appendEvent(sessionDir, {
2764
2869
  schema_version: "0.1.0",
2765
2870
  type: "session_ended",
2766
2871
  id: prefixedUlid4("evt"),
@@ -2768,7 +2873,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
2768
2873
  occurred_at: ctx.occurredAt,
2769
2874
  source: claudeCodeAdapterMetadata.kind
2770
2875
  });
2771
- await mutateSessionYaml2(sessionYamlPath, (s) => {
2876
+ await finalizeSessionYaml2(paths, sessionId, (s) => {
2772
2877
  s.session.status = "failed";
2773
2878
  s.session.ended_at = ctx.occurredAt;
2774
2879
  s.session.invocation.exit_code = null;
@@ -2776,7 +2881,7 @@ async function finalizeSessionAsFailed2(sessionDir, sessionYamlPath, sessionId,
2776
2881
  }
2777
2882
  async function resolveRepositoryRootForRun(cwd) {
2778
2883
  try {
2779
- return await resolveRepositoryRoot9(cwd);
2884
+ return await resolveRepositoryRoot10(cwd);
2780
2885
  } catch (error) {
2781
2886
  if (error instanceof Error && error.message === "Not a git repository") {
2782
2887
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -2789,21 +2894,21 @@ async function resolveRepositoryRootForRun(cwd) {
2789
2894
 
2790
2895
  // src/commands/session.ts
2791
2896
  import { readFile as readFile2 } from "fs/promises";
2792
- import { basename as basename3, isAbsolute, join as join6, relative as relative2 } from "path";
2897
+ import { basename as basename3, isAbsolute as isAbsolute2, join as join6, relative as relative2 } from "path";
2793
2898
  import {
2794
- acquireLock as acquireLock2,
2899
+ acquireLock as acquireLock5,
2795
2900
  appendEventToExistingSession as appendEventToExistingSession2,
2796
- assertBasouRootSafe as assertBasouRootSafe9,
2797
- basouPaths as basouPaths9,
2901
+ assertBasouRootSafe as assertBasouRootSafe10,
2902
+ basouPaths as basouPaths10,
2798
2903
  enumerateSessionDirs as enumerateSessionDirs2,
2799
- findErrorCode as findErrorCode8,
2904
+ findErrorCode as findErrorCode9,
2800
2905
  importSessionFromJson as importSessionFromJson2,
2801
2906
  loadSessionEntries,
2802
2907
  readAllEvents,
2803
2908
  readManifest as readManifest5,
2804
2909
  readYamlFile as readYamlFile4,
2805
2910
  rechainSessionInPlace,
2806
- resolveRepositoryRoot as resolveRepositoryRoot10,
2911
+ resolveRepositoryRoot as resolveRepositoryRoot11,
2807
2912
  resolveSessionId as resolveSessionId2,
2808
2913
  resolveTaskId,
2809
2914
  SessionImportPayloadSchema as SessionImportPayloadSchema2,
@@ -2814,15 +2919,7 @@ import {
2814
2919
  import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
2815
2920
 
2816
2921
  // src/lib/format-duration.ts
2817
- function formatDurationMs(ms) {
2818
- const totalSeconds = Math.round(ms / 1e3);
2819
- const hours = Math.floor(totalSeconds / 3600);
2820
- const minutes = Math.floor(totalSeconds % 3600 / 60);
2821
- const seconds = totalSeconds % 60;
2822
- if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
2823
- if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
2824
- return `${seconds}s`;
2825
- }
2922
+ import { formatDurationMs } from "@basou/core";
2826
2923
 
2827
2924
  // src/commands/session.ts
2828
2925
  var SES_PREFIX3 = "ses_";
@@ -2868,8 +2965,8 @@ async function runSessionList(options, ctx = {}) {
2868
2965
  async function doRunSessionList(options, ctx) {
2869
2966
  const cwd = ctx.cwd ?? process.cwd();
2870
2967
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
2871
- const paths = basouPaths9(repositoryRoot);
2872
- await assertWorkspaceInitialized7(paths.root);
2968
+ const paths = basouPaths10(repositoryRoot);
2969
+ await assertWorkspaceInitialized8(paths.root);
2873
2970
  const now = /* @__PURE__ */ new Date();
2874
2971
  const records = (await loadSessionEntries(paths, {
2875
2972
  now,
@@ -2920,8 +3017,8 @@ async function runSessionShow(idInput, options, ctx = {}) {
2920
3017
  async function doRunSessionShow(idInput, options, ctx) {
2921
3018
  const cwd = ctx.cwd ?? process.cwd();
2922
3019
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
2923
- const paths = basouPaths9(repositoryRoot);
2924
- await assertWorkspaceInitialized7(paths.root);
3020
+ const paths = basouPaths10(repositoryRoot);
3021
+ await assertWorkspaceInitialized8(paths.root);
2925
3022
  const sessionId = await resolveSessionId2(paths, idInput);
2926
3023
  const sessionDir = join6(paths.sessions, sessionId);
2927
3024
  const sessionYamlPath = join6(sessionDir, "session.yaml");
@@ -2930,7 +3027,7 @@ async function doRunSessionShow(idInput, options, ctx) {
2930
3027
  const raw = await readYamlFile4(sessionYamlPath);
2931
3028
  session = SessionSchema3.parse(raw);
2932
3029
  } catch (error) {
2933
- if (findErrorCode8(error, "ENOENT")) {
3030
+ if (findErrorCode9(error, "ENOENT")) {
2934
3031
  throw new Error(`Session not found: ${idInput}`);
2935
3032
  }
2936
3033
  throw new Error("Failed to read session", { cause: error });
@@ -3045,7 +3142,7 @@ function formatSessionWork(session, events, now) {
3045
3142
  }
3046
3143
  function formatWorkingDir(workingDir, repositoryRoot, options) {
3047
3144
  if (options.fullPath === true) return workingDir;
3048
- if (!isAbsolute(workingDir)) {
3145
+ if (!isAbsolute2(workingDir)) {
3049
3146
  if (workingDir === ".") return "<repository_root>";
3050
3147
  return workingDir;
3051
3148
  }
@@ -3165,7 +3262,7 @@ function maxLen2(values, floor) {
3165
3262
  }
3166
3263
  async function resolveRepositoryRootForSession(cwd, subcmd) {
3167
3264
  try {
3168
- return await resolveRepositoryRoot10(cwd);
3265
+ return await resolveRepositoryRoot11(cwd);
3169
3266
  } catch (error) {
3170
3267
  if (error instanceof Error && error.message === "Not a git repository") {
3171
3268
  throw new Error(
@@ -3176,11 +3273,11 @@ async function resolveRepositoryRootForSession(cwd, subcmd) {
3176
3273
  throw error;
3177
3274
  }
3178
3275
  }
3179
- async function assertWorkspaceInitialized7(basouRoot) {
3276
+ async function assertWorkspaceInitialized8(basouRoot) {
3180
3277
  try {
3181
- await assertBasouRootSafe9(basouRoot);
3278
+ await assertBasouRootSafe10(basouRoot);
3182
3279
  } catch (error) {
3183
- if (findErrorCode8(error, "ENOENT")) {
3280
+ if (findErrorCode9(error, "ENOENT")) {
3184
3281
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3185
3282
  }
3186
3283
  throw error;
@@ -3218,8 +3315,8 @@ async function runSessionImport(options, ctx = {}) {
3218
3315
  async function doRunSessionImport(options, ctx) {
3219
3316
  const cwd = ctx.cwd ?? process.cwd();
3220
3317
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
3221
- const paths = basouPaths9(repositoryRoot);
3222
- await assertWorkspaceInitialized7(paths.root);
3318
+ const paths = basouPaths10(repositoryRoot);
3319
+ await assertWorkspaceInitialized8(paths.root);
3223
3320
  const manifest = await readManifest5(paths);
3224
3321
  const rawBody = await readInputFile(options.from);
3225
3322
  const json = parseJsonStrict(rawBody);
@@ -3249,10 +3346,10 @@ async function readInputFile(path) {
3249
3346
  try {
3250
3347
  return await readFile2(path, "utf8");
3251
3348
  } catch (error) {
3252
- if (findErrorCode8(error, "ENOENT")) {
3349
+ if (findErrorCode9(error, "ENOENT")) {
3253
3350
  throw new Error("Import source not found", { cause: error });
3254
3351
  }
3255
- if (findErrorCode8(error, "EISDIR")) {
3352
+ if (findErrorCode9(error, "EISDIR")) {
3256
3353
  throw new Error("Import source is not a file", { cause: error });
3257
3354
  }
3258
3355
  throw new Error("Failed to read import source", { cause: error });
@@ -3332,8 +3429,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
3332
3429
  }
3333
3430
  const cwd = ctx.cwd ?? process.cwd();
3334
3431
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
3335
- const paths = basouPaths9(repositoryRoot);
3336
- await assertWorkspaceInitialized7(paths.root);
3432
+ const paths = basouPaths10(repositoryRoot);
3433
+ await assertWorkspaceInitialized8(paths.root);
3337
3434
  const sessionId = await resolveSessionId2(paths, sessionIdInput);
3338
3435
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
3339
3436
  if (body.length === 0) {
@@ -3341,7 +3438,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
3341
3438
  }
3342
3439
  const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
3343
3440
  const sesId = sessionId;
3344
- const sessionLock = await acquireLock2(paths, "session", sesId);
3441
+ const sessionLock = await acquireLock5(paths, "session", sesId);
3345
3442
  let result;
3346
3443
  try {
3347
3444
  result = await appendEventToExistingSession2({
@@ -3366,10 +3463,10 @@ async function readNoteFile(path) {
3366
3463
  try {
3367
3464
  return await readFile2(path, "utf8");
3368
3465
  } catch (error) {
3369
- if (findErrorCode8(error, "ENOENT")) {
3466
+ if (findErrorCode9(error, "ENOENT")) {
3370
3467
  throw new Error("Note source not found", { cause: error });
3371
3468
  }
3372
- if (findErrorCode8(error, "EISDIR")) {
3469
+ if (findErrorCode9(error, "EISDIR")) {
3373
3470
  throw new Error("Note source is not a file", { cause: error });
3374
3471
  }
3375
3472
  throw new Error("Failed to read note source", { cause: error });
@@ -3414,8 +3511,8 @@ async function doRunSessionRechain(options, ctx) {
3414
3511
  }
3415
3512
  const cwd = ctx.cwd ?? process.cwd();
3416
3513
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
3417
- const paths = basouPaths9(repositoryRoot);
3418
- await assertWorkspaceInitialized7(paths.root);
3514
+ const paths = basouPaths10(repositoryRoot);
3515
+ await assertWorkspaceInitialized8(paths.root);
3419
3516
  const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
3420
3517
  const dryRun = options.dryRun === true;
3421
3518
  const rows = [];
@@ -3468,11 +3565,11 @@ function renderRechainRow(row, dryRun) {
3468
3565
 
3469
3566
  // src/commands/stats.ts
3470
3567
  import {
3471
- assertBasouRootSafe as assertBasouRootSafe10,
3472
- basouPaths as basouPaths10,
3568
+ assertBasouRootSafe as assertBasouRootSafe11,
3569
+ basouPaths as basouPaths11,
3473
3570
  computeWorkStats,
3474
- findErrorCode as findErrorCode9,
3475
- resolveRepositoryRoot as resolveRepositoryRoot11
3571
+ findErrorCode as findErrorCode10,
3572
+ resolveRepositoryRoot as resolveRepositoryRoot12
3476
3573
  } from "@basou/core";
3477
3574
  function registerStatsCommand(program) {
3478
3575
  program.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
@@ -3490,8 +3587,8 @@ async function runStats(options, ctx = {}) {
3490
3587
  async function doRunStats(options, ctx) {
3491
3588
  const cwd = ctx.cwd ?? process.cwd();
3492
3589
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
3493
- const paths = basouPaths10(repositoryRoot);
3494
- await assertWorkspaceInitialized8(paths.root);
3590
+ const paths = basouPaths11(repositoryRoot);
3591
+ await assertWorkspaceInitialized9(paths.root);
3495
3592
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
3496
3593
  const result = await computeWorkStats({
3497
3594
  paths,
@@ -3575,7 +3672,7 @@ function formatInt(n) {
3575
3672
  }
3576
3673
  async function resolveRepositoryRootForStats(cwd) {
3577
3674
  try {
3578
- return await resolveRepositoryRoot11(cwd);
3675
+ return await resolveRepositoryRoot12(cwd);
3579
3676
  } catch (error) {
3580
3677
  if (error instanceof Error && error.message === "Not a git repository") {
3581
3678
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -3585,11 +3682,11 @@ async function resolveRepositoryRootForStats(cwd) {
3585
3682
  throw error;
3586
3683
  }
3587
3684
  }
3588
- async function assertWorkspaceInitialized8(basouRoot) {
3685
+ async function assertWorkspaceInitialized9(basouRoot) {
3589
3686
  try {
3590
- await assertBasouRootSafe10(basouRoot);
3687
+ await assertBasouRootSafe11(basouRoot);
3591
3688
  } catch (error) {
3592
- if (findErrorCode9(error, "ENOENT")) {
3689
+ if (findErrorCode10(error, "ENOENT")) {
3593
3690
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3594
3691
  }
3595
3692
  throw error;
@@ -3598,12 +3695,12 @@ async function assertWorkspaceInitialized8(basouRoot) {
3598
3695
 
3599
3696
  // src/commands/status.ts
3600
3697
  import {
3601
- assertBasouRootSafe as assertBasouRootSafe11,
3602
- basouPaths as basouPaths11,
3698
+ assertBasouRootSafe as assertBasouRootSafe12,
3699
+ basouPaths as basouPaths12,
3603
3700
  buildStatusSnapshot,
3604
- findErrorCode as findErrorCode10,
3701
+ findErrorCode as findErrorCode11,
3605
3702
  readManifest as readManifest6,
3606
- resolveRepositoryRoot as resolveRepositoryRoot12,
3703
+ resolveRepositoryRoot as resolveRepositoryRoot13,
3607
3704
  writeStatus
3608
3705
  } from "@basou/core";
3609
3706
  function registerStatusCommand(program) {
@@ -3622,11 +3719,11 @@ async function runStatus(options, ctx = {}) {
3622
3719
  async function doRunStatus(options, ctx) {
3623
3720
  const cwd = ctx.cwd ?? process.cwd();
3624
3721
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
3625
- const paths = basouPaths11(repositoryRoot);
3722
+ const paths = basouPaths12(repositoryRoot);
3626
3723
  try {
3627
- await assertBasouRootSafe11(paths.root);
3724
+ await assertBasouRootSafe12(paths.root);
3628
3725
  } catch (error) {
3629
- if (findErrorCode10(error, "ENOENT")) {
3726
+ if (findErrorCode11(error, "ENOENT")) {
3630
3727
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3631
3728
  }
3632
3729
  throw error;
@@ -3635,7 +3732,7 @@ async function doRunStatus(options, ctx) {
3635
3732
  try {
3636
3733
  manifest = await readManifest6(paths);
3637
3734
  } catch (error) {
3638
- if (findErrorCode10(error, "ENOENT")) {
3735
+ if (findErrorCode11(error, "ENOENT")) {
3639
3736
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3640
3737
  }
3641
3738
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -3659,7 +3756,7 @@ function renderTextStatus(s) {
3659
3756
  }
3660
3757
  async function resolveRepositoryRootForStatus(cwd) {
3661
3758
  try {
3662
- return await resolveRepositoryRoot12(cwd);
3759
+ return await resolveRepositoryRoot13(cwd);
3663
3760
  } catch (error) {
3664
3761
  if (error instanceof Error && error.message === "Not a git repository") {
3665
3762
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -3675,13 +3772,13 @@ import { readFile as readFile3 } from "fs/promises";
3675
3772
  import { join as join7 } from "path";
3676
3773
  import {
3677
3774
  archiveTask,
3678
- assertBasouRootSafe as assertBasouRootSafe12,
3679
- basouPaths as basouPaths12,
3775
+ assertBasouRootSafe as assertBasouRootSafe13,
3776
+ basouPaths as basouPaths13,
3680
3777
  createTaskWithEvent,
3681
3778
  deleteTask,
3682
3779
  editTask,
3683
3780
  enumerateArchivedTaskIds,
3684
- findErrorCode as findErrorCode11,
3781
+ findErrorCode as findErrorCode12,
3685
3782
  loadSessionEntries as loadSessionEntries2,
3686
3783
  loadTaskEntries,
3687
3784
  prefixedUlid as prefixedUlid5,
@@ -3692,7 +3789,7 @@ import {
3692
3789
  reconcileTask,
3693
3790
  refreshTaskLinkedSessions,
3694
3791
  replayEvents as replayEvents2,
3695
- resolveRepositoryRoot as resolveRepositoryRoot13,
3792
+ resolveRepositoryRoot as resolveRepositoryRoot14,
3696
3793
  resolveSessionId as resolveSessionId3,
3697
3794
  resolveTaskId as resolveTaskId2,
3698
3795
  TaskStatusSchema,
@@ -3778,8 +3875,8 @@ async function doRunTaskNew(options, ctx) {
3778
3875
  }
3779
3876
  const cwd = ctx.cwd ?? process.cwd();
3780
3877
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
3781
- const paths = basouPaths12(repositoryRoot);
3782
- await assertWorkspaceInitialized9(paths.root);
3878
+ const paths = basouPaths13(repositoryRoot);
3879
+ await assertWorkspaceInitialized10(paths.root);
3783
3880
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
3784
3881
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
3785
3882
  const occurredAt = now.toISOString();
@@ -3887,8 +3984,8 @@ async function runTaskList(options, ctx = {}) {
3887
3984
  async function doRunTaskList(options, ctx) {
3888
3985
  const cwd = ctx.cwd ?? process.cwd();
3889
3986
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
3890
- const paths = basouPaths12(repositoryRoot);
3891
- await assertWorkspaceInitialized9(paths.root);
3987
+ const paths = basouPaths13(repositoryRoot);
3988
+ await assertWorkspaceInitialized10(paths.root);
3892
3989
  const entries = await loadTaskEntries(paths, {
3893
3990
  onSkip: (id, reason) => printTaskSkip(id, reason)
3894
3991
  });
@@ -3991,8 +4088,8 @@ async function runTaskShow(idInput, options, ctx = {}) {
3991
4088
  async function doRunTaskShow(idInput, options, ctx) {
3992
4089
  const cwd = ctx.cwd ?? process.cwd();
3993
4090
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
3994
- const paths = basouPaths12(repositoryRoot);
3995
- await assertWorkspaceInitialized9(paths.root);
4091
+ const paths = basouPaths13(repositoryRoot);
4092
+ await assertWorkspaceInitialized10(paths.root);
3996
4093
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
3997
4094
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
3998
4095
  const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
@@ -4135,8 +4232,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
4135
4232
  const newStatus = parseTaskStatusPositional(newStatusInput);
4136
4233
  const cwd = ctx.cwd ?? process.cwd();
4137
4234
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
4138
- const paths = basouPaths12(repositoryRoot);
4139
- await assertWorkspaceInitialized9(paths.root);
4235
+ const paths = basouPaths13(repositoryRoot);
4236
+ await assertWorkspaceInitialized10(paths.root);
4140
4237
  const taskId = await resolveTaskId2(paths, taskIdInput);
4141
4238
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4142
4239
  const occurredAt = now.toISOString();
@@ -4212,8 +4309,8 @@ async function runTaskReconcile(options, ctx = {}) {
4212
4309
  async function doRunTaskReconcile(options, ctx) {
4213
4310
  const cwd = ctx.cwd ?? process.cwd();
4214
4311
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
4215
- const paths = basouPaths12(repositoryRoot);
4216
- await assertWorkspaceInitialized9(paths.root);
4312
+ const paths = basouPaths13(repositoryRoot);
4313
+ await assertWorkspaceInitialized10(paths.root);
4217
4314
  const manifest = await readManifest7(paths);
4218
4315
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
4219
4316
  const write = options.write === true;
@@ -4392,8 +4489,8 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
4392
4489
  }
4393
4490
  const cwd = ctx.cwd ?? process.cwd();
4394
4491
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
4395
- const paths = basouPaths12(repositoryRoot);
4396
- await assertWorkspaceInitialized9(paths.root);
4492
+ const paths = basouPaths13(repositoryRoot);
4493
+ await assertWorkspaceInitialized10(paths.root);
4397
4494
  const manifest = await readManifest7(paths);
4398
4495
  const taskId = await resolveTaskId2(paths, taskIdInput);
4399
4496
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
@@ -4472,8 +4569,8 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
4472
4569
  }
4473
4570
  const cwd = ctx.cwd ?? process.cwd();
4474
4571
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
4475
- const paths = basouPaths12(repositoryRoot);
4476
- await assertWorkspaceInitialized9(paths.root);
4572
+ const paths = basouPaths13(repositoryRoot);
4573
+ await assertWorkspaceInitialized10(paths.root);
4477
4574
  const manifest = await readManifest7(paths);
4478
4575
  const taskId = await resolveTaskId2(paths, taskIdInput);
4479
4576
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
@@ -4528,8 +4625,8 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
4528
4625
  }
4529
4626
  const cwd = ctx.cwd ?? process.cwd();
4530
4627
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
4531
- const paths = basouPaths12(repositoryRoot);
4532
- await assertWorkspaceInitialized9(paths.root);
4628
+ const paths = basouPaths13(repositoryRoot);
4629
+ await assertWorkspaceInitialized10(paths.root);
4533
4630
  const manifest = await readManifest7(paths);
4534
4631
  const taskId = await resolveTaskId2(paths, taskIdInput);
4535
4632
  if (options.yes !== true) {
@@ -4573,8 +4670,8 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
4573
4670
  }
4574
4671
  const cwd = ctx.cwd ?? process.cwd();
4575
4672
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
4576
- const paths = basouPaths12(repositoryRoot);
4577
- await assertWorkspaceInitialized9(paths.root);
4673
+ const paths = basouPaths13(repositoryRoot);
4674
+ await assertWorkspaceInitialized10(paths.root);
4578
4675
  const manifest = await readManifest7(paths);
4579
4676
  const taskId = await resolveTaskId2(paths, taskIdInput);
4580
4677
  if (options.yes !== true) {
@@ -4689,10 +4786,10 @@ async function readDescriptionFile(path) {
4689
4786
  try {
4690
4787
  return await readFile3(path, "utf8");
4691
4788
  } catch (error) {
4692
- if (findErrorCode11(error, "ENOENT")) {
4789
+ if (findErrorCode12(error, "ENOENT")) {
4693
4790
  throw new Error("Description source not found", { cause: error });
4694
4791
  }
4695
- if (findErrorCode11(error, "EISDIR")) {
4792
+ if (findErrorCode12(error, "EISDIR")) {
4696
4793
  throw new Error("Description source is not a file", { cause: error });
4697
4794
  }
4698
4795
  throw new Error("Failed to read description source", { cause: error });
@@ -4700,7 +4797,7 @@ async function readDescriptionFile(path) {
4700
4797
  }
4701
4798
  async function resolveRepositoryRootForTask(cwd, subcmd) {
4702
4799
  try {
4703
- return await resolveRepositoryRoot13(cwd);
4800
+ return await resolveRepositoryRoot14(cwd);
4704
4801
  } catch (error) {
4705
4802
  if (error instanceof Error && error.message === "Not a git repository") {
4706
4803
  throw new Error(
@@ -4711,11 +4808,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
4711
4808
  throw error;
4712
4809
  }
4713
4810
  }
4714
- async function assertWorkspaceInitialized9(basouRoot) {
4811
+ async function assertWorkspaceInitialized10(basouRoot) {
4715
4812
  try {
4716
- await assertBasouRootSafe12(basouRoot);
4813
+ await assertBasouRootSafe13(basouRoot);
4717
4814
  } catch (error) {
4718
- if (findErrorCode11(error, "ENOENT")) {
4815
+ if (findErrorCode12(error, "ENOENT")) {
4719
4816
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4720
4817
  }
4721
4818
  throw error;
@@ -4803,18 +4900,16 @@ function maxLen3(values, floor) {
4803
4900
 
4804
4901
  // src/commands/verify.ts
4805
4902
  import {
4806
- assertBasouRootSafe as assertBasouRootSafe13,
4807
- basouPaths as basouPaths13,
4903
+ assertBasouRootSafe as assertBasouRootSafe14,
4904
+ basouPaths as basouPaths14,
4808
4905
  enumerateSessionDirs as enumerateSessionDirs3,
4809
- findErrorCode as findErrorCode12,
4810
- resolveRepositoryRoot as resolveRepositoryRoot14,
4906
+ findErrorCode as findErrorCode13,
4907
+ resolveRepositoryRoot as resolveRepositoryRoot15,
4811
4908
  resolveSessionId as resolveSessionId4,
4812
4909
  verifyEventsChain
4813
4910
  } from "@basou/core";
4814
4911
  function registerVerifyCommand(program) {
4815
- program.command("verify").description(
4816
- "Verify the tamper-evidence hash chain of imported sessions' event logs (read-only)"
4817
- ).option("--session <id>", "Verify a single session (unique id prefix accepted)").option("--all", "Verify every session (the default when --session is omitted)").option("--json", "Output the verdicts as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
4912
+ program.command("verify").description("Verify the tamper-evidence hash chain of sessions' event logs (read-only)").option("--session <id>", "Verify a single session (unique id prefix accepted)").option("--all", "Verify every session (the default when --session is omitted)").option("--json", "Output the verdicts as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
4818
4913
  await runVerify(opts);
4819
4914
  });
4820
4915
  }
@@ -4832,8 +4927,8 @@ async function doRunVerify(options, ctx) {
4832
4927
  }
4833
4928
  const cwd = ctx.cwd ?? process.cwd();
4834
4929
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
4835
- const paths = basouPaths13(repositoryRoot);
4836
- await assertWorkspaceInitialized10(paths.root);
4930
+ const paths = basouPaths14(repositoryRoot);
4931
+ await assertWorkspaceInitialized11(paths.root);
4837
4932
  const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
4838
4933
  const rows = [];
4839
4934
  for (const sessionId of sessionIds) {
@@ -4855,7 +4950,7 @@ async function doRunVerify(options, ctx) {
4855
4950
  }
4856
4951
  const tally = (status) => rows.filter((r) => r.status === status).length;
4857
4952
  console.log(
4858
- `Sessions: ${rows.length} total \u2014 ${tally("verified")} verified, ${tally("unchained")} unchained, ${tally("empty")} empty, ${tally("incomplete")} incomplete, ${tamperedCount} tampered`
4953
+ `Sessions: ${rows.length} total \u2014 ${tally("verified")} verified, ${tally("unchained")} unchained, ${tally("empty")} empty, ${tally("incomplete")} incomplete, ${tally("in_progress")} in_progress, ${tamperedCount} tampered`
4859
4954
  );
4860
4955
  }
4861
4956
  if (tamperedCount > 0) {
@@ -4870,15 +4965,17 @@ function renderVerdict(row) {
4870
4965
  return row.line !== void 0 ? `TAMPERED (${row.reason} at line ${row.line})` : `TAMPERED (${row.reason})`;
4871
4966
  case "incomplete":
4872
4967
  return "incomplete (session.yaml missing; re-import to repair)";
4968
+ case "in_progress":
4969
+ return `in_progress (${row.event_count} events; live session, anchor written at finalize)`;
4873
4970
  case "unchained":
4874
- return "unchained (not an imported session, or imported before chaining)";
4971
+ return "unchained (session created before event-log chaining)";
4875
4972
  case "empty":
4876
4973
  return "empty";
4877
4974
  }
4878
4975
  }
4879
4976
  async function resolveRepositoryRootForVerify(cwd) {
4880
4977
  try {
4881
- return await resolveRepositoryRoot14(cwd);
4978
+ return await resolveRepositoryRoot15(cwd);
4882
4979
  } catch (error) {
4883
4980
  if (error instanceof Error && error.message === "Not a git repository") {
4884
4981
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -4888,11 +4985,11 @@ async function resolveRepositoryRootForVerify(cwd) {
4888
4985
  throw error;
4889
4986
  }
4890
4987
  }
4891
- async function assertWorkspaceInitialized10(basouRoot) {
4988
+ async function assertWorkspaceInitialized11(basouRoot) {
4892
4989
  try {
4893
- await assertBasouRootSafe13(basouRoot);
4990
+ await assertBasouRootSafe14(basouRoot);
4894
4991
  } catch (error) {
4895
- if (findErrorCode12(error, "ENOENT")) {
4992
+ if (findErrorCode13(error, "ENOENT")) {
4896
4993
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4897
4994
  }
4898
4995
  throw error;
@@ -4901,7 +4998,7 @@ async function assertWorkspaceInitialized10(basouRoot) {
4901
4998
 
4902
4999
  // src/commands/view.ts
4903
5000
  import { spawn } from "child_process";
4904
- import { assertBasouRootSafe as assertBasouRootSafe14, basouPaths as basouPaths14, findErrorCode as findErrorCode14, resolveRepositoryRoot as resolveRepositoryRoot15 } from "@basou/core";
5001
+ import { assertBasouRootSafe as assertBasouRootSafe15, basouPaths as basouPaths15, findErrorCode as findErrorCode15, resolveRepositoryRoot as resolveRepositoryRoot16 } from "@basou/core";
4905
5002
  import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
4906
5003
 
4907
5004
  // src/lib/view-server.ts
@@ -4910,7 +5007,7 @@ import { join as join8 } from "path";
4910
5007
  import {
4911
5008
  computeWorkStats as computeWorkStats2,
4912
5009
  enumerateApprovals as enumerateApprovals2,
4913
- findErrorCode as findErrorCode13,
5010
+ findErrorCode as findErrorCode14,
4914
5011
  isLazyExpired as isLazyExpired2,
4915
5012
  loadApproval as loadApproval2,
4916
5013
  loadSessionEntries as loadSessionEntries3,
@@ -5383,7 +5480,7 @@ function startViewServer(opts) {
5383
5480
  };
5384
5481
  let boundPort = port;
5385
5482
  const getPort = () => boundPort;
5386
- return new Promise((resolve3, reject) => {
5483
+ return new Promise((resolve4, reject) => {
5387
5484
  const server = createServer((req, res) => {
5388
5485
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
5389
5486
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -5394,7 +5491,7 @@ function startViewServer(opts) {
5394
5491
  const address = server.address();
5395
5492
  boundPort = isAddressInfo(address) ? address.port : port;
5396
5493
  server.off("error", reject);
5397
- resolve3({
5494
+ resolve4({
5398
5495
  url: `http://${host}:${boundPort}`,
5399
5496
  port: boundPort,
5400
5497
  close: () => closeServer(server)
@@ -5406,8 +5503,8 @@ function isAddressInfo(value) {
5406
5503
  return value !== null && typeof value === "object";
5407
5504
  }
5408
5505
  function closeServer(server) {
5409
- return new Promise((resolve3) => {
5410
- server.close(() => resolve3());
5506
+ return new Promise((resolve4) => {
5507
+ server.close(() => resolve4());
5411
5508
  server.closeAllConnections();
5412
5509
  });
5413
5510
  }
@@ -5512,7 +5609,7 @@ async function overview(deps) {
5512
5609
  try {
5513
5610
  manifest = await readManifest8(deps.paths);
5514
5611
  } catch (error) {
5515
- if (findErrorCode13(error, "ENOENT")) {
5612
+ if (findErrorCode14(error, "ENOENT")) {
5516
5613
  return { initialized: false, repoRoot: deps.repoRoot };
5517
5614
  }
5518
5615
  throw error;
@@ -5727,8 +5824,8 @@ async function runView(options, ctx = {}) {
5727
5824
  async function doRunView(options, ctx) {
5728
5825
  const cwd = ctx.cwd ?? process.cwd();
5729
5826
  const repositoryRoot = await resolveRepositoryRootForView(cwd);
5730
- const paths = basouPaths14(repositoryRoot);
5731
- await assertWorkspaceInitialized11(paths.root);
5827
+ const paths = basouPaths15(repositoryRoot);
5828
+ await assertWorkspaceInitialized12(paths.root);
5732
5829
  const deps = {
5733
5830
  paths,
5734
5831
  repoRoot: repositoryRoot,
@@ -5759,7 +5856,7 @@ async function startListening(port, deps) {
5759
5856
  try {
5760
5857
  return await startViewServer({ port, deps });
5761
5858
  } catch (error) {
5762
- if (findErrorCode14(error, "EADDRINUSE")) {
5859
+ if (findErrorCode15(error, "EADDRINUSE")) {
5763
5860
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
5764
5861
  cause: error
5765
5862
  });
@@ -5782,7 +5879,7 @@ function openInBrowser(url, override) {
5782
5879
  }
5783
5880
  }
5784
5881
  function waitForShutdown(signal) {
5785
- return new Promise((resolve3) => {
5882
+ return new Promise((resolve4) => {
5786
5883
  const cleanup = () => {
5787
5884
  process.off("SIGINT", onSignal);
5788
5885
  process.off("SIGTERM", onSignal);
@@ -5790,18 +5887,18 @@ function waitForShutdown(signal) {
5790
5887
  };
5791
5888
  const onSignal = () => {
5792
5889
  cleanup();
5793
- resolve3();
5890
+ resolve4();
5794
5891
  };
5795
5892
  const onAbort = () => {
5796
5893
  cleanup();
5797
- resolve3();
5894
+ resolve4();
5798
5895
  };
5799
5896
  process.on("SIGINT", onSignal);
5800
5897
  process.on("SIGTERM", onSignal);
5801
5898
  if (signal !== void 0) {
5802
5899
  if (signal.aborted) {
5803
5900
  cleanup();
5804
- resolve3();
5901
+ resolve4();
5805
5902
  return;
5806
5903
  }
5807
5904
  signal.addEventListener("abort", onAbort);
@@ -5810,7 +5907,7 @@ function waitForShutdown(signal) {
5810
5907
  }
5811
5908
  async function resolveRepositoryRootForView(cwd) {
5812
5909
  try {
5813
- return await resolveRepositoryRoot15(cwd);
5910
+ return await resolveRepositoryRoot16(cwd);
5814
5911
  } catch (error) {
5815
5912
  if (error instanceof Error && error.message === "Not a git repository") {
5816
5913
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
@@ -5820,11 +5917,11 @@ async function resolveRepositoryRootForView(cwd) {
5820
5917
  throw error;
5821
5918
  }
5822
5919
  }
5823
- async function assertWorkspaceInitialized11(basouRoot) {
5920
+ async function assertWorkspaceInitialized12(basouRoot) {
5824
5921
  try {
5825
- await assertBasouRootSafe14(basouRoot);
5922
+ await assertBasouRootSafe15(basouRoot);
5826
5923
  } catch (error) {
5827
- if (findErrorCode14(error, "ENOENT")) {
5924
+ if (findErrorCode15(error, "ENOENT")) {
5828
5925
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5829
5926
  }
5830
5927
  throw error;
@@ -5853,6 +5950,7 @@ function buildProgram() {
5853
5950
  registerTaskCommand(program);
5854
5951
  registerHandoffCommand(program);
5855
5952
  registerDecisionsCommand(program);
5953
+ registerReportCommand(program);
5856
5954
  return program;
5857
5955
  }
5858
5956
  export {