@basou/core 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1720,6 +1720,8 @@ function splitLinesBytes2(buf) {
1720
1720
  }
1721
1721
 
1722
1722
  // src/git/snapshot.ts
1723
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
1724
+ import { join as join9 } from "path";
1723
1725
  import { simpleGit } from "simple-git";
1724
1726
 
1725
1727
  // src/storage/status.ts
@@ -1757,19 +1759,19 @@ var DIRECTORY_CHECKS = {
1757
1759
  tmp: (p) => p.tmp
1758
1760
  };
1759
1761
  async function assertBasouRootSafe(rootPath) {
1760
- let stat3;
1762
+ let stat4;
1761
1763
  try {
1762
- stat3 = await fsp.lstat(rootPath);
1764
+ stat4 = await fsp.lstat(rootPath);
1763
1765
  } catch (error) {
1764
1766
  if (hasErrorCode2(error) && error.code === "ENOENT") {
1765
1767
  throw new Error("Basou workspace not found", { cause: error });
1766
1768
  }
1767
1769
  throw new Error("Failed to inspect .basou root", { cause: error });
1768
1770
  }
1769
- if (stat3.isSymbolicLink()) {
1771
+ if (stat4.isSymbolicLink()) {
1770
1772
  throw new Error(".basou root is a symlink; refusing to operate");
1771
1773
  }
1772
- if (!stat3.isDirectory()) {
1774
+ if (!stat4.isDirectory()) {
1773
1775
  throw new Error(".basou root exists but is not a directory");
1774
1776
  }
1775
1777
  }
@@ -1849,6 +1851,14 @@ function isGitNotFound(error) {
1849
1851
  }
1850
1852
  return false;
1851
1853
  }
1854
+ function isNotAGitRepository(error) {
1855
+ let cur = error;
1856
+ for (let i = 0; i < 4 && cur instanceof Error; i++) {
1857
+ if (/not a git repository/i.test(cur.message)) return true;
1858
+ cur = cur.cause;
1859
+ }
1860
+ return false;
1861
+ }
1852
1862
  async function resolveRepositoryRoot(cwd) {
1853
1863
  const git = safeSimpleGit(cwd);
1854
1864
  try {
@@ -1864,8 +1874,53 @@ async function resolveRepositoryRoot(cwd) {
1864
1874
  if (error instanceof Error && error.message === "Not a git repository") {
1865
1875
  throw error;
1866
1876
  }
1867
- throw new Error("Not a git repository", { cause: error });
1877
+ if (isNotAGitRepository(error)) {
1878
+ throw new Error("Not a git repository", { cause: error });
1879
+ }
1880
+ throw new Error("Git command failed", { cause: error });
1881
+ }
1882
+ }
1883
+ async function resolveBasouRepositoryRoot(cwd, opts) {
1884
+ try {
1885
+ return await resolveRepositoryRoot(cwd);
1886
+ } catch (error) {
1887
+ if (!(error instanceof Error) || error.message !== "Not a git repository") throw error;
1888
+ const linked = await findLinkedBasouRepos(cwd);
1889
+ const only = linked[0];
1890
+ if (only !== void 0 && linked.length === 1) {
1891
+ opts?.onRedirect?.({ via: only.name, root: only.root });
1892
+ return only.root;
1893
+ }
1894
+ if (linked.length > 1) {
1895
+ const names = linked.map((l) => l.name).join(", ");
1896
+ throw new Error(
1897
+ `Ambiguous workspace view: ${linked.length} linked repos have a .basou store (${names}). cd into the one you want and re-run.`
1898
+ );
1899
+ }
1900
+ throw error;
1901
+ }
1902
+ }
1903
+ async function findLinkedBasouRepos(dir) {
1904
+ const entries = await readdir3(dir, { withFileTypes: true }).catch(() => null);
1905
+ if (entries === null) return [];
1906
+ const byRoot = /* @__PURE__ */ new Map();
1907
+ for (const entry of entries) {
1908
+ if (!entry.isSymbolicLink()) continue;
1909
+ let root;
1910
+ try {
1911
+ root = await resolveRepositoryRoot(join9(dir, entry.name));
1912
+ } catch {
1913
+ continue;
1914
+ }
1915
+ try {
1916
+ if (!(await stat2(join9(root, ".basou"))).isDirectory()) continue;
1917
+ } catch {
1918
+ continue;
1919
+ }
1920
+ const existing = byRoot.get(root);
1921
+ if (existing === void 0 || entry.name < existing) byRoot.set(root, entry.name);
1868
1922
  }
1923
+ return [...byRoot.entries()].map(([root, name]) => ({ name, root })).sort((a, b) => a.name.localeCompare(b.name));
1869
1924
  }
1870
1925
  async function tryRemoteUrl(repositoryRoot) {
1871
1926
  const git = safeSimpleGit(repositoryRoot);
@@ -2013,12 +2068,12 @@ function parseDiffNameStatus(raw) {
2013
2068
  }
2014
2069
 
2015
2070
  // src/handoff/handoff-renderer.ts
2016
- import { join as join12 } from "path";
2071
+ import { join as join13 } from "path";
2017
2072
 
2018
2073
  // src/storage/tasks.ts
2019
2074
  import { createHash as createHash2 } from "crypto";
2020
- import { mkdir as mkdir3, readdir as readdir3, readFile as readFile7, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
2021
- import { join as join11 } from "path";
2075
+ import { mkdir as mkdir3, readdir as readdir4, readFile as readFile7, rename as rename2, stat as stat3, unlink as unlink3 } from "fs/promises";
2076
+ import { join as join12 } from "path";
2022
2077
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2023
2078
  import { z as z8 } from "zod";
2024
2079
 
@@ -2062,7 +2117,7 @@ var TaskSchema = z6.object({
2062
2117
  // src/storage/ad-hoc-session.ts
2063
2118
  import { mkdir as mkdir2, rm } from "fs/promises";
2064
2119
  import { homedir } from "os";
2065
- import { join as join9 } from "path";
2120
+ import { join as join10 } from "path";
2066
2121
 
2067
2122
  // src/lib/path-sanitizer.ts
2068
2123
  import { posix as path } from "path";
@@ -2142,8 +2197,8 @@ async function createAdHocSessionWithEvent(input) {
2142
2197
  taskId: input.taskId ?? null
2143
2198
  })
2144
2199
  );
2145
- const sessionDir = join9(input.paths.sessions, sessionId);
2146
- const sessionYamlPath = join9(sessionDir, "session.yaml");
2200
+ const sessionDir = join10(input.paths.sessions, sessionId);
2201
+ const sessionYamlPath = join10(sessionDir, "session.yaml");
2147
2202
  const lock = await acquireLock(input.paths, "session", sessionId);
2148
2203
  let bulkResult = null;
2149
2204
  try {
@@ -2293,7 +2348,7 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
2293
2348
 
2294
2349
  // src/storage/task-index.ts
2295
2350
  import { readFile as readFile6 } from "fs/promises";
2296
- import { join as join10 } from "path";
2351
+ import { join as join11 } from "path";
2297
2352
 
2298
2353
  // src/schemas/task-index.schema.ts
2299
2354
  import { z as z7 } from "zod";
@@ -2312,7 +2367,7 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
2312
2367
 
2313
2368
  // src/storage/task-index.ts
2314
2369
  function taskIndexPath(paths) {
2315
- return join10(paths.tasks, "index.json");
2370
+ return join11(paths.tasks, "index.json");
2316
2371
  }
2317
2372
  async function readTaskIndex(paths) {
2318
2373
  const filePath = taskIndexPath(paths);
@@ -2426,7 +2481,7 @@ function splitFrontMatter(raw) {
2426
2481
  return { yamlText, body };
2427
2482
  }
2428
2483
  async function readTaskFile(paths, taskId) {
2429
- const filePath = join11(paths.tasks, `${taskId}.md`);
2484
+ const filePath = join12(paths.tasks, `${taskId}.md`);
2430
2485
  let raw;
2431
2486
  try {
2432
2487
  raw = await readFile7(filePath, "utf8");
@@ -2459,7 +2514,7 @@ async function readTaskFile(paths, taskId) {
2459
2514
  }
2460
2515
  async function writeTaskFile(paths, taskId, doc, options) {
2461
2516
  const validated = TaskSchema.parse(doc.task);
2462
- const filePath = join11(paths.tasks, `${taskId}.md`);
2517
+ const filePath = join12(paths.tasks, `${taskId}.md`);
2463
2518
  const yamlText = stringifyYaml(validated);
2464
2519
  const trimmedBody = doc.body.length === 0 ? "" : `
2465
2520
  ${doc.body.endsWith("\n") ? doc.body : `${doc.body}
@@ -2511,7 +2566,7 @@ async function enumerateTaskIds(paths) {
2511
2566
  async function enumerateTaskIdsFromDisk(paths) {
2512
2567
  let entries;
2513
2568
  try {
2514
- entries = (await readdir3(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2569
+ entries = (await readdir4(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2515
2570
  } catch (error) {
2516
2571
  if (findErrorCode(error, "ENOENT")) return [];
2517
2572
  throw new Error("Failed to enumerate tasks", { cause: error });
@@ -2544,12 +2599,12 @@ async function safeUpdateTaskIndex(paths, op) {
2544
2599
  }
2545
2600
  var ARCHIVE_DIR_NAME = "archive";
2546
2601
  function archiveTasksDir(paths) {
2547
- return join11(paths.tasks, ARCHIVE_DIR_NAME);
2602
+ return join12(paths.tasks, ARCHIVE_DIR_NAME);
2548
2603
  }
2549
2604
  async function enumerateArchivedTaskIds(paths) {
2550
2605
  let entries;
2551
2606
  try {
2552
- entries = (await readdir3(archiveTasksDir(paths), { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2607
+ entries = (await readdir4(archiveTasksDir(paths), { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2553
2608
  } catch (error) {
2554
2609
  if (findErrorCode(error, "ENOENT")) return [];
2555
2610
  throw new Error("Failed to enumerate archived tasks", { cause: error });
@@ -2574,7 +2629,7 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
2574
2629
  throw error;
2575
2630
  }
2576
2631
  }
2577
- const archiveFilePath = join11(archiveTasksDir(paths), `${taskId}.md`);
2632
+ const archiveFilePath = join12(archiveTasksDir(paths), `${taskId}.md`);
2578
2633
  let raw;
2579
2634
  try {
2580
2635
  raw = await readFile7(archiveFilePath, "utf8");
@@ -2868,7 +2923,7 @@ async function createTaskAttachLocked(input) {
2868
2923
  ...sessionDoc,
2869
2924
  session: { ...sessionDoc.session, task_id: input.taskId }
2870
2925
  };
2871
- await overwriteYamlFile(join11(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2926
+ await overwriteYamlFile(join12(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2872
2927
  } catch (error) {
2873
2928
  throw new TaskWriteAfterEventError({
2874
2929
  taskId: input.taskId,
@@ -3127,17 +3182,17 @@ function buildUpdatedDoc(input) {
3127
3182
  return { task: next, body: input.currentDoc.body };
3128
3183
  }
3129
3184
  async function computeTaskMdSnapshot(paths, taskId) {
3130
- const filePath = join11(paths.tasks, `${taskId}.md`);
3131
- const [stats, raw] = await Promise.all([stat2(filePath), readFile7(filePath)]);
3185
+ const filePath = join12(paths.tasks, `${taskId}.md`);
3186
+ const [stats, raw] = await Promise.all([stat3(filePath), readFile7(filePath)]);
3132
3187
  const hash = createHash2("sha256").update(raw).digest("hex");
3133
3188
  return { mtimeMs: stats.mtimeMs, hash };
3134
3189
  }
3135
3190
  async function readTaskFileWithSnapshot(paths, taskId) {
3136
- const filePath = join11(paths.tasks, `${taskId}.md`);
3191
+ const filePath = join12(paths.tasks, `${taskId}.md`);
3137
3192
  let rawBuffer;
3138
3193
  let stats;
3139
3194
  try {
3140
- [rawBuffer, stats] = await Promise.all([readFile7(filePath), stat2(filePath)]);
3195
+ [rawBuffer, stats] = await Promise.all([readFile7(filePath), stat3(filePath)]);
3141
3196
  } catch (error) {
3142
3197
  if (findErrorCode(error, "ENOENT")) {
3143
3198
  throw new Error("Task file not found", { cause: error });
@@ -3625,7 +3680,7 @@ async function deleteTaskLocked(input) {
3625
3680
  });
3626
3681
  const eventId = adHoc.targetEventIds[0];
3627
3682
  try {
3628
- await unlink3(join11(input.paths.tasks, `${input.taskId}.md`));
3683
+ await unlink3(join12(input.paths.tasks, `${input.taskId}.md`));
3629
3684
  } catch (error) {
3630
3685
  throw new TaskWriteAfterEventError({
3631
3686
  taskId: input.taskId,
@@ -3697,8 +3752,8 @@ async function archiveTaskLocked(input) {
3697
3752
  );
3698
3753
  await mkdir3(archiveTasksDir(input.paths), { recursive: true });
3699
3754
  await rename2(
3700
- join11(input.paths.tasks, `${input.taskId}.md`),
3701
- join11(archiveTasksDir(input.paths), `${input.taskId}.md`)
3755
+ join12(input.paths.tasks, `${input.taskId}.md`),
3756
+ join12(archiveTasksDir(input.paths), `${input.taskId}.md`)
3702
3757
  );
3703
3758
  } catch (error) {
3704
3759
  throw new TaskWriteAfterEventError({
@@ -3734,7 +3789,7 @@ async function renderHandoff(input) {
3734
3789
  const tasksCreated = [];
3735
3790
  const tasksStatusChanged = [];
3736
3791
  for (const entry of entries) {
3737
- const sessionDir = join12(input.paths.sessions, entry.sessionId);
3792
+ const sessionDir = join13(input.paths.sessions, entry.sessionId);
3738
3793
  try {
3739
3794
  for await (const ev of replayEvents(sessionDir, {
3740
3795
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -3850,11 +3905,11 @@ function formatHandoffBody(args) {
3850
3905
  if (args.latestSession !== void 0) {
3851
3906
  const status = args.latestSession.session.session.status;
3852
3907
  const label = args.latestSession.session.session.label;
3853
- const shortId = shortIdWithPrefix(args.latestSession.sessionId);
3908
+ const shortId2 = shortIdWithPrefix(args.latestSession.sessionId);
3854
3909
  if (label !== void 0 && label !== "") {
3855
- lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId}]`);
3910
+ lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId2}]`);
3856
3911
  } else {
3857
- lines.push(`- \u6700\u7D42 session: ${shortId} (${status})`);
3912
+ lines.push(`- \u6700\u7D42 session: ${shortId2} (${status})`);
3858
3913
  }
3859
3914
  } else {
3860
3915
  lines.push("- \u6700\u7D42 session: (no live sessions)");
@@ -4025,6 +4080,17 @@ function parseDuration(input) {
4025
4080
  return ms;
4026
4081
  }
4027
4082
 
4083
+ // src/lib/format-duration.ts
4084
+ function formatDurationMs(ms) {
4085
+ const totalSeconds = Math.round(ms / 1e3);
4086
+ const hours = Math.floor(totalSeconds / 3600);
4087
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
4088
+ const seconds = totalSeconds % 60;
4089
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
4090
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
4091
+ return `${seconds}s`;
4092
+ }
4093
+
4028
4094
  // src/lib/id-resolver.ts
4029
4095
  async function resolveSessionId(paths, input) {
4030
4096
  return resolveIdInternal(paths, input, "session");
@@ -4080,132 +4146,11 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
4080
4146
  return matches[0];
4081
4147
  }
4082
4148
 
4083
- // src/runtime/child-process-runner.ts
4084
- import { spawn as spawn2 } from "child_process";
4085
- var DEFAULT_KILL_GRACE_MS = 5e3;
4086
- var ChildProcessRunner = class {
4087
- async run(command, args, options) {
4088
- validateOptions(options);
4089
- if (options.signal?.aborted) {
4090
- throw new Error("Process aborted before spawn", {
4091
- cause: options.signal.reason
4092
- });
4093
- }
4094
- const snapshotCommand = command;
4095
- const snapshotArgs = [...args];
4096
- const snapshotCwd = options.cwd;
4097
- const captureMode = options.capture ?? "buffer";
4098
- const started_at = /* @__PURE__ */ new Date();
4099
- let child;
4100
- try {
4101
- child = spawn2(snapshotCommand, [...snapshotArgs], {
4102
- cwd: snapshotCwd,
4103
- env: options.env ?? process.env,
4104
- stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
4105
- shell: false,
4106
- detached: false
4107
- });
4108
- } catch (error) {
4109
- throw classifySpawnError(error);
4110
- }
4111
- if (options.onSpawn) {
4112
- try {
4113
- options.onSpawn(child);
4114
- } catch {
4115
- }
4116
- }
4117
- let timeoutTimer = null;
4118
- let killTimer = null;
4119
- let killed = false;
4120
- let settled = false;
4121
- const triggerKill = () => {
4122
- if (killed || child.exitCode !== null) return;
4123
- killed = true;
4124
- child.kill("SIGTERM");
4125
- killTimer = setTimeout(() => {
4126
- if (child.exitCode === null) {
4127
- child.kill("SIGKILL");
4128
- }
4129
- }, DEFAULT_KILL_GRACE_MS);
4130
- };
4131
- const onAbort = () => {
4132
- triggerKill();
4133
- };
4134
- options.signal?.addEventListener("abort", onAbort);
4135
- if (options.signal?.aborted) {
4136
- triggerKill();
4137
- }
4138
- let stdout = "";
4139
- let stderr = "";
4140
- if (captureMode === "buffer") {
4141
- child.stdout?.setEncoding("utf8");
4142
- child.stderr?.setEncoding("utf8");
4143
- child.stdout?.on("data", (chunk) => {
4144
- stdout += chunk;
4145
- });
4146
- child.stderr?.on("data", (chunk) => {
4147
- stderr += chunk;
4148
- });
4149
- if (options.stdin !== void 0) {
4150
- child.stdin?.end(options.stdin);
4151
- } else {
4152
- child.stdin?.end();
4153
- }
4154
- }
4155
- if (options.timeout_ms !== void 0) {
4156
- timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
4157
- }
4158
- const cleanup = () => {
4159
- if (timeoutTimer !== null) clearTimeout(timeoutTimer);
4160
- if (killTimer !== null) clearTimeout(killTimer);
4161
- options.signal?.removeEventListener("abort", onAbort);
4162
- };
4163
- return new Promise((resolve2, reject) => {
4164
- child.once("error", (error) => {
4165
- if (settled) return;
4166
- settled = true;
4167
- cleanup();
4168
- reject(classifySpawnError(error));
4169
- });
4170
- child.once("close", (code, signal) => {
4171
- if (settled) return;
4172
- settled = true;
4173
- cleanup();
4174
- const ended_at = /* @__PURE__ */ new Date();
4175
- resolve2({
4176
- command: snapshotCommand,
4177
- args: snapshotArgs,
4178
- cwd: snapshotCwd,
4179
- exit_code: code,
4180
- signal,
4181
- stdout,
4182
- stderr,
4183
- started_at: started_at.toISOString(),
4184
- ended_at: ended_at.toISOString(),
4185
- duration_ms: ended_at.getTime() - started_at.getTime(),
4186
- pid: child.pid ?? null
4187
- });
4188
- });
4189
- });
4190
- }
4191
- };
4192
- function validateOptions(options) {
4193
- if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
4194
- throw new Error("Invalid timeout_ms");
4195
- }
4196
- if (options.capture === "none" && options.stdin !== void 0) {
4197
- throw new Error('Combination of capture: "none" and stdin is not supported');
4198
- }
4199
- }
4200
- function classifySpawnError(error) {
4201
- if (findErrorCode(error, "ENOENT")) {
4202
- return new Error("Command not found", { cause: error });
4203
- }
4204
- return new Error("Failed to spawn child process", { cause: error });
4205
- }
4149
+ // src/orientation/orientation-renderer.ts
4150
+ import { join as join14 } from "path";
4206
4151
 
4207
- // src/schemas/json-schema.ts
4208
- import { z as z11 } from "zod";
4152
+ // src/storage/manifest.ts
4153
+ import { lstat as lstat3 } from "fs/promises";
4209
4154
 
4210
4155
  // src/schemas/manifest.schema.ts
4211
4156
  import { z as z9 } from "zod";
@@ -4256,126 +4201,400 @@ var ManifestSchema = z9.object({
4256
4201
  import: ImportConfigSchema.optional()
4257
4202
  });
4258
4203
 
4259
- // src/schemas/session-import.schema.ts
4260
- import { z as z10 } from "zod";
4261
- var SessionInnerImportSchema = z10.object({
4262
- id: SessionIdSchema.optional(),
4263
- label: z10.string().optional(),
4264
- task_id: TaskIdSchema.nullable().optional(),
4265
- workspace_id: WorkspaceIdSchema,
4266
- source: z10.object({
4267
- kind: SessionSourceKindSchema,
4268
- version: z10.literal("0.1.0"),
4269
- // Source-tool-native id (e.g. Claude Code session UUID), retained so
4270
- // re-imports of the same source can be deduplicated.
4271
- external_id: z10.string().optional(),
4272
- // Byte size of the source native log at import time. Declared here too
4273
- // (not only in session.schema.ts) because this inner `source` object is
4274
- // a plain z.object: zod strips keys it does not declare, so a field
4275
- // absent here would be dropped from the parsed payload before persist
4276
- // and the size could never be stored.
4277
- source_size_bytes: z10.number().int().nonnegative().optional()
4278
- }),
4279
- started_at: IsoTimestampSchema,
4280
- ended_at: IsoTimestampSchema.optional(),
4281
- status: SessionStatusSchema,
4282
- working_directory: z10.string().min(1),
4283
- invocation: z10.object({
4284
- command: z10.string().min(1),
4285
- args: z10.array(z10.string()),
4286
- exit_code: z10.number().int().nullable()
4287
- }),
4288
- related_files: z10.array(z10.string()).default([]),
4289
- events_log: z10.string().optional(),
4290
- summary: z10.string().nullable().optional(),
4291
- metrics: SessionMetricsSchema.optional(),
4292
- // Accepted so a payload assembled from an on-disk chained session.yaml
4293
- // round-trips, and DISCARDED by the importer (buildSessionRecord never
4294
- // copies it): the integrity anchor is computed at write time, never
4295
- // imported. Mirrors the accept-and-discard of `prev_hash` on events.
4296
- integrity: SessionIntegritySchema.optional()
4297
- }).strict();
4298
- var SessionImportPayloadSchema = z10.object({
4299
- schema_version: z10.string(),
4300
- session: SessionInnerImportSchema,
4301
- events: z10.array(EventSchema)
4302
- }).strict();
4303
-
4304
- // src/schemas/json-schema.ts
4305
- var JSON_SCHEMA_VERSION = "0.1.0";
4306
- var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
4307
- var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
4308
- var DOCUMENTS = [
4309
- {
4310
- name: "manifest",
4311
- schema: ManifestSchema,
4312
- title: "Basou Manifest",
4313
- description: "The `.basou/manifest.yaml` workspace manifest."
4314
- },
4315
- {
4316
- name: "session",
4317
- schema: SessionSchema,
4318
- title: "Basou Session",
4319
- description: "A `.basou/sessions/<id>/session.yaml` session record."
4320
- },
4321
- {
4322
- name: "event",
4323
- schema: EventSchema,
4324
- title: "Basou Event",
4325
- description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
4326
- },
4327
- {
4328
- name: "task",
4329
- schema: TaskSchema,
4330
- title: "Basou Task",
4331
- description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
4332
- },
4333
- {
4334
- name: "approval",
4335
- schema: ApprovalSchema,
4336
- title: "Basou Approval",
4337
- description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
4338
- },
4339
- {
4340
- name: "status",
4341
- schema: StatusSchema,
4342
- title: "Basou Status",
4343
- description: "The `.basou/status.json` workspace status snapshot."
4344
- },
4345
- {
4346
- name: "task-index",
4347
- schema: TaskIndexSchema,
4348
- title: "Basou Task Index",
4349
- description: "The `.basou/tasks/index.json` task lookup index."
4350
- },
4351
- {
4352
- name: "session-import",
4353
- schema: SessionImportPayloadSchema,
4354
- title: "Basou Session Import Payload",
4355
- description: "The portable session payload consumed by `basou session import`."
4204
+ // src/storage/manifest.ts
4205
+ function createManifest(input) {
4206
+ if (input.workspaceName.length === 0) {
4207
+ throw new Error("Workspace name is empty. Pass --name explicitly.");
4356
4208
  }
4357
- ];
4358
- function buildJsonSchemas() {
4359
- return DOCUMENTS.map((doc) => {
4360
- const generated = z11.toJSONSchema(doc.schema, { io: "input" });
4361
- const { $schema, ...rest } = generated;
4362
- const schema = {
4363
- $schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
4364
- $id: `${ID_BASE}/${doc.name}.schema.json`,
4365
- title: doc.title,
4366
- description: doc.description,
4367
- ...rest
4368
- };
4369
- return { name: doc.name, schema };
4209
+ const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
4210
+ const workspaceId = input.workspaceId ?? prefixedUlid("ws");
4211
+ const project = {
4212
+ ...input.projectName !== void 0 ? { name: input.projectName } : {},
4213
+ ...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
4214
+ ...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
4215
+ };
4216
+ const manifest = {
4217
+ schema_version: "0.1.0",
4218
+ basou_version: "0.1.0",
4219
+ workspace: {
4220
+ id: workspaceId,
4221
+ name: input.workspaceName,
4222
+ created_at: now,
4223
+ updated_at: now
4224
+ },
4225
+ project,
4226
+ capabilities: {
4227
+ enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
4228
+ },
4229
+ approval: {
4230
+ required_for: ["destructive_command", "external_send"],
4231
+ default_risk_level: "medium"
4232
+ },
4233
+ adapters: {
4234
+ "claude-code": { enabled: true }
4235
+ },
4236
+ git: { events_log: "ignore" },
4237
+ ...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
4238
+ };
4239
+ return ManifestSchema.parse(manifest);
4240
+ }
4241
+ async function writeManifest(paths, manifest, options) {
4242
+ const force = options?.force === true;
4243
+ const validated = ManifestSchema.parse(manifest);
4244
+ if (!force) {
4245
+ let existed = false;
4246
+ try {
4247
+ await lstat3(paths.files.manifest);
4248
+ existed = true;
4249
+ } catch (error) {
4250
+ if (!hasErrorCode3(error) || error.code !== "ENOENT") {
4251
+ throw new Error("Failed to inspect existing manifest", { cause: error });
4252
+ }
4253
+ }
4254
+ if (existed) {
4255
+ throw new Error("Already initialized. Use --force to overwrite.");
4256
+ }
4257
+ }
4258
+ await writeYamlFile(paths.files.manifest, validated);
4259
+ }
4260
+ async function readManifest(paths) {
4261
+ const raw = await readYamlFile(paths.files.manifest);
4262
+ return ManifestSchema.parse(raw);
4263
+ }
4264
+ function hasErrorCode3(error) {
4265
+ if (!(error instanceof Error)) return false;
4266
+ return typeof error.code === "string";
4267
+ }
4268
+
4269
+ // src/orientation/orientation-renderer.ts
4270
+ async function summarizeOrientation(input) {
4271
+ const limit = input.relatedFilesLimit ?? 10;
4272
+ const now = new Date(input.nowIso);
4273
+ const loadOpts = { now };
4274
+ if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
4275
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4276
+ const entries = await loadSessionEntries(input.paths, loadOpts);
4277
+ const decisions = [];
4278
+ for (const entry of entries) {
4279
+ const sessionDir = join14(input.paths.sessions, entry.sessionId);
4280
+ try {
4281
+ for await (const ev of replayEvents(sessionDir, {
4282
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4283
+ })) {
4284
+ if (ev.type === "decision_recorded") {
4285
+ decisions.push({
4286
+ decisionId: ev.decision_id,
4287
+ title: ev.title,
4288
+ occurredAt: ev.occurred_at
4289
+ });
4290
+ }
4291
+ }
4292
+ } catch {
4293
+ input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
4294
+ }
4295
+ }
4296
+ decisions.sort((a, b) => {
4297
+ const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
4298
+ return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
4370
4299
  });
4300
+ const latestDecision = decisions[decisions.length - 1];
4301
+ const taskLoadOpts = {};
4302
+ if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
4303
+ const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
4304
+ const inFlightTasks = taskEntries.filter((t) => t.task.task.status === "in_progress" || t.task.task.status === "planned").map((t) => ({
4305
+ id: t.task.task.id,
4306
+ title: t.task.task.title,
4307
+ status: t.task.task.status,
4308
+ linkedSessions: t.task.task.linked_sessions?.length ?? 0
4309
+ }));
4310
+ const plannedTasks = taskEntries.filter((t) => t.task.task.status === "planned").map((t) => ({ id: t.task.task.id, title: t.task.task.title }));
4311
+ const { pending: pendingIds } = await enumerateApprovals(input.paths);
4312
+ const pendingApprovals = [];
4313
+ for (const id of [...pendingIds].sort()) {
4314
+ const loaded = await loadApproval(input.paths, id);
4315
+ if (loaded === null) continue;
4316
+ const a = loaded.approval;
4317
+ pendingApprovals.push({
4318
+ id,
4319
+ risk: a.risk_level,
4320
+ kind: a.action.kind,
4321
+ reason: a.reason,
4322
+ sessionId: a.session_id,
4323
+ createdAt: a.created_at,
4324
+ expired: isLazyExpired(a, now)
4325
+ });
4326
+ }
4327
+ const suspects = entries.filter((e) => e.suspect).map((e) => ({
4328
+ sessionId: e.sessionId,
4329
+ status: e.session.session.status,
4330
+ reason: e.suspectReason
4331
+ }));
4332
+ const liveEntries = entries.filter(
4333
+ (e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
4334
+ );
4335
+ const latestEntry = [...liveEntries].sort(
4336
+ (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4337
+ )[0];
4338
+ const latestSession = latestEntry !== void 0 ? {
4339
+ sessionId: latestEntry.sessionId,
4340
+ label: latestEntry.session.session.label ?? null,
4341
+ status: latestEntry.session.session.status
4342
+ } : null;
4343
+ const activityEntries = entries.filter((e) => e.session.session.status !== "archived");
4344
+ const newest = [...activityEntries].sort(
4345
+ (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4346
+ )[0];
4347
+ const bySourceMap = /* @__PURE__ */ new Map();
4348
+ for (const e of entries) {
4349
+ const k = e.session.session.source.kind;
4350
+ bySourceMap.set(k, (bySourceMap.get(k) ?? 0) + 1);
4351
+ }
4352
+ const bySource = [...bySourceMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([kind, count]) => ({ kind, count }));
4353
+ let sourceRoots = null;
4354
+ try {
4355
+ const manifest = await readManifest(input.paths);
4356
+ sourceRoots = manifest.import?.source_roots ?? null;
4357
+ } catch {
4358
+ sourceRoots = null;
4359
+ }
4360
+ const latestFiles = latestEntry?.session.session.related_files ?? [];
4361
+ const uniqueFiles = new Set(latestFiles);
4362
+ const displayed = [...uniqueFiles].sort().slice(0, limit);
4363
+ const overflow = Math.max(0, uniqueFiles.size - limit);
4364
+ return {
4365
+ generatedAt: input.nowIso,
4366
+ sessionCount: entries.length,
4367
+ latestSession,
4368
+ latestDecision: latestDecision ?? null,
4369
+ decisionCount: decisions.length,
4370
+ relatedFiles: { displayed, overflow },
4371
+ inFlightTasks,
4372
+ plannedTasks,
4373
+ pendingApprovals,
4374
+ suspects,
4375
+ freshness: {
4376
+ newestStartedAt: newest?.session.session.started_at ?? null,
4377
+ newestSource: newest?.session.session.source.kind ?? null,
4378
+ bySource,
4379
+ sourceRoots
4380
+ }
4381
+ };
4371
4382
  }
4372
- function serializeJsonSchema(schema) {
4373
- return `${JSON.stringify(schema, null, 2)}
4374
- `;
4383
+ async function renderOrientation(input) {
4384
+ const summary = await summarizeOrientation(input);
4385
+ return {
4386
+ body: formatOrientationBody(summary, {
4387
+ staleness: input.staleness ?? null,
4388
+ verbose: input.verbose === true
4389
+ }),
4390
+ sessionCount: summary.sessionCount,
4391
+ pendingApprovalsCount: summary.pendingApprovals.length,
4392
+ suspectCount: summary.suspects.length,
4393
+ inFlightTaskCount: summary.inFlightTasks.length,
4394
+ decisionCount: summary.decisionCount
4395
+ };
4396
+ }
4397
+ function formatOrientationBody(summary, opts) {
4398
+ const lines = [];
4399
+ const now = new Date(summary.generatedAt);
4400
+ const newestRel = relativeAge(summary.freshness.newestStartedAt ?? void 0, now);
4401
+ lines.push("# Orientation");
4402
+ lines.push("");
4403
+ lines.push(
4404
+ `> Generated at ${summary.generatedAt} \xB7 sessions ${summary.sessionCount} \xB7 newest ${newestRel} \xB7 pending ${summary.pendingApprovals.length} \xB7 suspect ${summary.suspects.length}`
4405
+ );
4406
+ lines.push("");
4407
+ lines.push("## \u4ECA\u3069\u3053\u306B\u3044\u308B");
4408
+ lines.push("");
4409
+ if (summary.latestSession !== null) {
4410
+ const s = summary.latestSession;
4411
+ const sid = shortId(s.sessionId);
4412
+ if (s.label !== null && s.label !== "") {
4413
+ lines.push(`- \u6700\u7D42 session: ${s.label} (${s.status}) [${sid}]`);
4414
+ } else {
4415
+ lines.push(`- \u6700\u7D42 session: ${sid} (${s.status})`);
4416
+ }
4417
+ } else {
4418
+ lines.push("- \u6700\u7D42 session: (no live sessions)");
4419
+ }
4420
+ if (summary.latestDecision !== null) {
4421
+ lines.push(
4422
+ `- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}]`
4423
+ );
4424
+ if (summary.decisionCount > 1) {
4425
+ lines.push(` - ${summary.decisionCount} decisions total \u2014 see decisions.md`);
4426
+ }
4427
+ } else {
4428
+ lines.push("- \u76F4\u8FD1\u306E\u5224\u65AD: (no decisions recorded yet)");
4429
+ }
4430
+ if (summary.relatedFiles.displayed.length > 0) {
4431
+ const shown = summary.relatedFiles.displayed.join(", ");
4432
+ const more = summary.relatedFiles.overflow > 0 ? ` (... +${summary.relatedFiles.overflow} more)` : "";
4433
+ lines.push(`- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: ${shown}${more}`);
4434
+ } else {
4435
+ lines.push("- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: (none recorded)");
4436
+ }
4437
+ lines.push("");
4438
+ lines.push("## \u4F55\u304C\u52D5\u304F");
4439
+ lines.push("");
4440
+ lines.push(`### \u9032\u884C\u4E2D task (${summary.inFlightTasks.length})`);
4441
+ if (summary.inFlightTasks.length === 0) {
4442
+ lines.push("- (none)");
4443
+ } else {
4444
+ for (const t of summary.inFlightTasks) {
4445
+ const linkedSuffix = t.linkedSessions > 1 ? ` \u2014 linked_sessions: ${t.linkedSessions}` : "";
4446
+ lines.push(`- ${t.title} (${t.status}) [${shortId(t.id)}]${linkedSuffix}`);
4447
+ }
4448
+ }
4449
+ lines.push("");
4450
+ lines.push(`### \u627F\u8A8D\u5F85\u3061 (${summary.pendingApprovals.length})`);
4451
+ if (summary.pendingApprovals.length === 0) {
4452
+ lines.push("- (none)");
4453
+ } else {
4454
+ for (const a of summary.pendingApprovals) {
4455
+ const expired = a.expired ? " (expired)" : "";
4456
+ lines.push(
4457
+ `- [${a.risk}] ${a.kind}: ${a.reason} \u2014 session ${shortId(a.sessionId)}, since ${a.createdAt}${expired}`
4458
+ );
4459
+ }
4460
+ }
4461
+ lines.push("");
4462
+ lines.push(`### \u8981\u6CE8\u610F session (${summary.suspects.length})`);
4463
+ if (summary.suspects.length === 0) {
4464
+ lines.push("- (none)");
4465
+ } else {
4466
+ for (const e of summary.suspects) {
4467
+ lines.push(`- ${shortId(e.sessionId)} (${e.status}) \u2014 ${suspectText(e.reason)}`);
4468
+ }
4469
+ }
4470
+ lines.push("");
4471
+ lines.push("## \u3069\u3053\u3078\u5411\u304B\u3046");
4472
+ lines.push("");
4473
+ if (summary.plannedTasks.length === 0) {
4474
+ lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
4475
+ if (summary.latestDecision !== null) {
4476
+ lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title}`);
4477
+ }
4478
+ } else {
4479
+ for (const t of summary.plannedTasks) {
4480
+ lines.push(`- ${t.title} [${shortId(t.id)}]`);
4481
+ }
4482
+ }
4483
+ lines.push("");
4484
+ lines.push("## \u3053\u308C\u306F\u6700\u65B0\u304B");
4485
+ lines.push("");
4486
+ for (const line of freshnessVerdict(summary, opts.staleness, now)) lines.push(line);
4487
+ if (opts.verbose) {
4488
+ lines.push("");
4489
+ lines.push("<!-- verbose: raw freshness telemetry -->");
4490
+ if (summary.freshness.newestStartedAt !== null) {
4491
+ lines.push(`- newest captured session: ${summary.freshness.newestStartedAt} (${newestRel})`);
4492
+ } else {
4493
+ lines.push("- newest captured session: (no sessions captured yet)");
4494
+ }
4495
+ const sourceBreakdown = summary.freshness.bySource.map(({ kind, count }) => `${kind} ${count}`).join(", ");
4496
+ lines.push(
4497
+ `- sessions: ${summary.sessionCount}${sourceBreakdown !== "" ? ` (${sourceBreakdown})` : ""}`
4498
+ );
4499
+ if (summary.freshness.sourceRoots !== null && summary.freshness.sourceRoots.length > 0) {
4500
+ lines.push(`- source roots: ${summary.freshness.sourceRoots.join(", ")}`);
4501
+ } else {
4502
+ lines.push("- source roots: (single root)");
4503
+ }
4504
+ lines.push(`- suspect sessions: ${summary.suspects.length}`);
4505
+ const probe = opts.staleness === null ? "not run" : `new ${opts.staleness.newSessions}, updated ${opts.staleness.updatedSessions}, unverifiable ${opts.staleness.unverifiableSessions ?? 0}`;
4506
+ lines.push(`- staleness probe: ${probe}`);
4507
+ }
4508
+ return lines.join("\n");
4509
+ }
4510
+ function toolDisplayName(kind) {
4511
+ switch (kind) {
4512
+ case "claude-code-import":
4513
+ case "claude-code-adapter":
4514
+ return "Claude Code";
4515
+ case "codex-import":
4516
+ return "Codex";
4517
+ case "terminal":
4518
+ return "\u30BF\u30FC\u30DF\u30CA\u30EB";
4519
+ case "human":
4520
+ return "\u624B\u52D5\u30E1\u30E2";
4521
+ case "import":
4522
+ return "\u4ED6\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9";
4523
+ default:
4524
+ return kind ?? "\u4E0D\u660E";
4525
+ }
4526
+ }
4527
+ function freshnessVerdict(summary, staleness, now) {
4528
+ if (staleness !== null && (staleness.unverifiableSessions ?? 0) > 0) {
4529
+ return [
4530
+ `\u26A0\uFE0F \u6700\u65B0\u304B\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3002\u5909\u5316\u3057\u305F\u304C\u5B89\u5168\u306B\u53D6\u308A\u8FBC\u3081\u306A\u3044\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${staleness.unverifiableSessions} \u4EF6\u3042\u308A\u307E\u3059(\u30CF\u30C3\u30B7\u30E5\u30C1\u30A7\u30FC\u30F3\u7834\u640D\u30FB\u975E\u8FFD\u8A18\u5909\u66F4\u306A\u3069)\u3002`,
4531
+ "`basou verify` \u3067\u78BA\u8A8D\u3057\u3001`basou refresh --force` \u3067\u518D\u53D6\u308A\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4532
+ ];
4533
+ }
4534
+ if (staleness !== null && (staleness.newSessions > 0 || staleness.updatedSessions > 0)) {
4535
+ const parts = [];
4536
+ if (staleness.newSessions > 0) parts.push(`\u65B0\u898F ${staleness.newSessions} \u4EF6`);
4537
+ if (staleness.updatedSessions > 0) parts.push(`\u66F4\u65B0 ${staleness.updatedSessions} \u4EF6`);
4538
+ return [
4539
+ `\u26A0\uFE0F \u53E4\u3044\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002\u6700\u5F8C\u306E\u53D6\u308A\u8FBC\u307F\u4EE5\u964D\u306B\u672A\u53D6\u308A\u8FBC\u307F\u306E\u4F5C\u696D\u304C\u3042\u308A\u307E\u3059(${parts.join("\u30FB")})\u3002`,
4540
+ "`basou refresh` \u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4541
+ ];
4542
+ }
4543
+ if (summary.freshness.newestStartedAt === null) {
4544
+ return [
4545
+ "\u2139\uFE0F \u307E\u3060\u8A18\u9332\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
4546
+ "\u3053\u306E\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u3067\u4F5C\u696D\u3059\u308B\u3068\u3001\u3053\u3053\u306B\u73FE\u5728\u5730\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002"
4547
+ ];
4548
+ }
4549
+ const rel = relativeAgeJa(summary.freshness.newestStartedAt, now);
4550
+ const tool = toolDisplayName(summary.freshness.newestSource);
4551
+ const suspectCount = summary.suspects.length;
4552
+ const suspectClause = suspectCount > 0 ? `\u8981\u6CE8\u610F\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${suspectCount} \u4EF6\u3042\u308A\u307E\u3059\u3002` : "\u53D6\u308A\u3053\u307C\u3057\u30FB\u8981\u6CE8\u610F\u306A\u3057\u3002";
4553
+ if (staleness === null) {
4554
+ return [
4555
+ `\u2139\uFE0F \u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E\u72B6\u614B\u3092\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002`,
4556
+ "\u6700\u65B0\u304B\u78BA\u8A8D\u3059\u308B\u306B\u306F `basou refresh` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4557
+ ];
4558
+ }
4559
+ return [`\u2705 \u6700\u65B0\u3067\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002${suspectClause}`];
4560
+ }
4561
+ function relativeAgeJa(startedAt, now) {
4562
+ if (startedAt === null) return "(\u4E0D\u660E)";
4563
+ const ms = now.getTime() - Date.parse(startedAt);
4564
+ if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
4565
+ if (ms < 6e4) return "\u305F\u3063\u305F\u4ECA";
4566
+ const totalMin = Math.floor(ms / 6e4);
4567
+ const days = Math.floor(totalMin / 1440);
4568
+ const hours = Math.floor(totalMin % 1440 / 60);
4569
+ const mins = totalMin % 60;
4570
+ if (days > 0) return hours > 0 ? `${days}\u65E5${hours}\u6642\u9593\u524D` : `${days}\u65E5\u524D`;
4571
+ if (hours > 0) return mins > 0 ? `${hours}\u6642\u9593${mins}\u5206\u524D` : `${hours}\u6642\u9593\u524D`;
4572
+ return `${mins}\u5206\u524D`;
4573
+ }
4574
+ function relativeAge(startedAt, now) {
4575
+ if (startedAt === void 0) return "(unknown)";
4576
+ const ms = now.getTime() - Date.parse(startedAt);
4577
+ if (!Number.isFinite(ms)) return "(unknown)";
4578
+ if (ms < 0) return "just now";
4579
+ if (ms < 1e3) return "just now";
4580
+ return `${formatDurationMs(ms)} ago`;
4581
+ }
4582
+ function suspectText(reason) {
4583
+ if (reason === "events_say_ended_but_yaml_running") return "ended (yaml stale)";
4584
+ if (reason === "running_no_end_event") return "no end event";
4585
+ return "suspect";
4586
+ }
4587
+ function shortId(id) {
4588
+ const sep = id.indexOf("_");
4589
+ if (sep === -1) return id.slice(0, 10);
4590
+ return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
4375
4591
  }
4376
4592
 
4593
+ // src/report/report-renderer.ts
4594
+ import { join as join16 } from "path";
4595
+
4377
4596
  // src/stats/work-stats.ts
4378
- import { join as join13 } from "path";
4597
+ import { join as join15 } from "path";
4379
4598
  function resolveTimeZone(timeZone) {
4380
4599
  if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4381
4600
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -4406,7 +4625,7 @@ async function computeWorkStats(input) {
4406
4625
  const events = [];
4407
4626
  let eventsUnreadable = false;
4408
4627
  try {
4409
- for await (const ev of replayEvents(join13(input.paths.sessions, entry.sessionId), {
4628
+ for await (const ev of replayEvents(join15(input.paths.sessions, entry.sessionId), {
4410
4629
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4411
4630
  })) {
4412
4631
  events.push(ev);
@@ -4660,61 +4879,618 @@ function tzDate(ms, timeZone) {
4660
4879
  }).format(new Date(ms));
4661
4880
  }
4662
4881
 
4663
- // src/storage/basou-dir.ts
4664
- import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
4665
- import { join as join14 } from "path";
4666
- function basouPaths(repositoryRoot) {
4667
- const root = join14(repositoryRoot, ".basou");
4668
- const approvalsBase = join14(root, "approvals");
4669
- return {
4670
- root,
4671
- sessions: join14(root, "sessions"),
4672
- tasks: join14(root, "tasks"),
4673
- approvals: {
4674
- pending: join14(approvalsBase, "pending"),
4675
- resolved: join14(approvalsBase, "resolved")
4676
- },
4677
- locks: join14(root, "locks"),
4678
- logs: join14(root, "logs"),
4679
- raw: join14(root, "raw"),
4680
- tmp: join14(root, "tmp"),
4681
- files: {
4682
- manifest: join14(root, "manifest.yaml"),
4683
- status: join14(root, "status.json"),
4684
- handoff: join14(root, "handoff.md"),
4685
- decisions: join14(root, "decisions.md")
4686
- }
4882
+ // src/report/report-renderer.ts
4883
+ var CHANGED_FILES_MARKDOWN_LIMIT = 50;
4884
+ var DECISIONS_MARKDOWN_LIMIT = 20;
4885
+ var SESSIONS_MARKDOWN_LIMIT = 30;
4886
+ var TASKS_MARKDOWN_LIMIT = 30;
4887
+ var APPROVALS_MARKDOWN_LIMIT = 30;
4888
+ var SESSION_STATUS_ORDER = [
4889
+ "completed",
4890
+ "failed",
4891
+ "running",
4892
+ "waiting_approval",
4893
+ "interrupted",
4894
+ "initialized",
4895
+ "imported",
4896
+ "archived"
4897
+ ];
4898
+ var TASK_STATUS_ORDER = ["planned", "in_progress", "done", "cancelled"];
4899
+ async function renderReport(input) {
4900
+ const now = new Date(input.nowIso);
4901
+ const unreadableEmitted = /* @__PURE__ */ new Set();
4902
+ const wrappedSkip = (sid, reason) => {
4903
+ if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
4904
+ input.onSessionSkip?.(sid, reason);
4687
4905
  };
4688
- }
4689
- var PATH_LABELS = {
4690
- sessions: ".basou/sessions",
4691
- tasks: ".basou/tasks",
4692
- approvalsPending: ".basou/approvals/pending",
4693
- approvalsResolved: ".basou/approvals/resolved",
4694
- locks: ".basou/locks",
4695
- logs: ".basou/logs",
4696
- raw: ".basou/raw",
4697
- tmp: ".basou/tmp"
4698
- };
4699
- async function ensureBasouDirectory(repositoryRoot) {
4700
- const paths = basouPaths(repositoryRoot);
4701
- let existing;
4702
- try {
4703
- existing = await lstat3(paths.root);
4704
- } catch (error) {
4705
- if (!hasErrorCode3(error) || error.code !== "ENOENT") {
4706
- throw new Error("Failed to inspect .basou directory", { cause: error });
4906
+ const loadOpts = { now, onSkip: wrappedSkip };
4907
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4908
+ const entries = await loadSessionEntries(input.paths, loadOpts);
4909
+ const statsInput = { paths: input.paths, now };
4910
+ if (input.timeZone !== void 0) statsInput.timeZone = input.timeZone;
4911
+ const stats = await computeWorkStats(statsInput);
4912
+ const statsBySession = new Map(stats.sessions.map((s) => [s.sessionId, s]));
4913
+ const decisions = [];
4914
+ for (const entry of entries) {
4915
+ const sessionDir = join16(input.paths.sessions, entry.sessionId);
4916
+ try {
4917
+ for await (const ev of replayEvents(sessionDir, {
4918
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4919
+ })) {
4920
+ if (ev.type === "decision_recorded") {
4921
+ decisions.push({ id: ev.decision_id, title: ev.title, occurredAt: ev.occurred_at });
4922
+ }
4923
+ }
4924
+ } catch {
4925
+ if (!unreadableEmitted.has(entry.sessionId)) {
4926
+ wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4927
+ }
4707
4928
  }
4708
4929
  }
4709
- if (existing !== void 0 && !existing.isDirectory()) {
4710
- throw new Error("Basou root .basou exists but is not a directory");
4711
- }
4712
- await Promise.all([
4713
- mkdirLabeled(paths.sessions, PATH_LABELS.sessions),
4714
- mkdirLabeled(paths.tasks, PATH_LABELS.tasks),
4715
- mkdirLabeled(paths.approvals.pending, PATH_LABELS.approvalsPending),
4716
- mkdirLabeled(paths.approvals.resolved, PATH_LABELS.approvalsResolved),
4717
- mkdirLabeled(paths.locks, PATH_LABELS.locks),
4930
+ decisions.sort((a, b) => {
4931
+ const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
4932
+ return c !== 0 ? c : a.id.localeCompare(b.id);
4933
+ });
4934
+ const taskLoadOpts = {};
4935
+ if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
4936
+ const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
4937
+ const taskItems = taskEntries.map((t2) => ({
4938
+ id: t2.task.task.id,
4939
+ title: t2.task.task.title,
4940
+ status: t2.task.task.status
4941
+ }));
4942
+ const tasksByStatus = tallyTaskStatus(taskItems);
4943
+ const approvalIds = await enumerateApprovals(input.paths);
4944
+ const resolvedSet = new Set(approvalIds.resolved);
4945
+ const pendingIds = approvalIds.pending.filter((id) => !resolvedSet.has(id));
4946
+ const loadedApprovals = (await Promise.all(
4947
+ [...pendingIds, ...approvalIds.resolved].map((id) => loadApproval(input.paths, id))
4948
+ )).filter((a) => a !== null);
4949
+ const approvalItems = loadedApprovals.map((a) => ({
4950
+ id: a.approval.id,
4951
+ reason: a.approval.reason,
4952
+ status: a.approval.status,
4953
+ riskLevel: a.approval.risk_level
4954
+ }));
4955
+ const approvalCounts = { pending: 0, approved: 0, rejected: 0, expired: 0 };
4956
+ for (const a of approvalItems) approvalCounts[a.status] += 1;
4957
+ const changedSet = /* @__PURE__ */ new Set();
4958
+ for (const entry of entries) {
4959
+ if (entry.session.session.source.kind === "import") continue;
4960
+ for (const f of entry.session.session.related_files) changedSet.add(f);
4961
+ }
4962
+ const changedFiles = [...changedSet].sort();
4963
+ const integrity = {
4964
+ total: 0,
4965
+ verified: 0,
4966
+ unchained: 0,
4967
+ empty: 0,
4968
+ incomplete: 0,
4969
+ in_progress: 0,
4970
+ tampered: 0,
4971
+ tamperedSessions: []
4972
+ };
4973
+ for (const entry of entries) {
4974
+ const verdict = await verifyEventsChain(input.paths, entry.sessionId).catch(() => null);
4975
+ if (verdict === null) {
4976
+ if (!unreadableEmitted.has(entry.sessionId)) {
4977
+ wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4978
+ }
4979
+ continue;
4980
+ }
4981
+ integrity.total += 1;
4982
+ integrity[verdict.status] += 1;
4983
+ if (verdict.status === "tampered") integrity.tamperedSessions.push(entry.sessionId);
4984
+ }
4985
+ const sessionItems = [...entries].sort(
4986
+ (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4987
+ ).map((e) => {
4988
+ const w = statsBySession.get(e.sessionId);
4989
+ return {
4990
+ id: e.sessionId,
4991
+ label: e.session.session.label ?? null,
4992
+ status: e.session.session.status,
4993
+ source: e.session.session.source.kind,
4994
+ startedAt: e.session.session.started_at,
4995
+ activeMs: w?.activeTimeMs ?? 0,
4996
+ outputTokens: w?.tokens.output ?? 0
4997
+ };
4998
+ });
4999
+ const period = computePeriod(entries, input.nowIso);
5000
+ const t = stats.totals;
5001
+ const data = {
5002
+ generatedAt: input.nowIso,
5003
+ ...input.title !== void 0 ? { title: input.title } : {},
5004
+ period,
5005
+ sessions: { total: entries.length, byStatus: stats.byStatus, items: sessionItems },
5006
+ volume: {
5007
+ outputTokens: t.tokens.output,
5008
+ reasoningTokens: t.tokens.reasoning,
5009
+ commandCount: t.commandCount,
5010
+ fileChangedCount: t.fileChangedCount,
5011
+ decisionCount: t.decisionCount,
5012
+ tokensAvailable: t.tokensAvailable
5013
+ },
5014
+ time: {
5015
+ activeMs: t.billableActiveTimeMs,
5016
+ machineActiveMs: t.machineActiveTimeMs,
5017
+ machineAvailable: t.machineActiveAvailable,
5018
+ spanMs: t.sessionSpanMs,
5019
+ commandTimeMs: t.commandTimeMs,
5020
+ timeZone: stats.timeZone
5021
+ },
5022
+ decisions: { count: decisions.length, items: decisions },
5023
+ approvals: { ...approvalCounts, items: approvalItems },
5024
+ tasks: { total: taskEntries.length, byStatus: tasksByStatus, items: taskItems },
5025
+ changedFiles,
5026
+ integrity
5027
+ };
5028
+ return { body: formatReportBody(data), data };
5029
+ }
5030
+ function computePeriod(entries, nowIso) {
5031
+ if (entries.length === 0) return { from: null, to: null };
5032
+ let from = entries[0]?.session.session.started_at ?? nowIso;
5033
+ let to = nowIso;
5034
+ let sawEnd = false;
5035
+ for (const e of entries) {
5036
+ const s = e.session.session.started_at;
5037
+ if (Date.parse(s) < Date.parse(from)) from = s;
5038
+ const end = e.session.session.ended_at ?? nowIso;
5039
+ if (!sawEnd || Date.parse(end) > Date.parse(to)) {
5040
+ to = end;
5041
+ sawEnd = true;
5042
+ }
5043
+ }
5044
+ if (Date.parse(to) < Date.parse(from)) to = from;
5045
+ return { from, to };
5046
+ }
5047
+ function tallyTaskStatus(items) {
5048
+ const counts = /* @__PURE__ */ new Map();
5049
+ for (const i of items) counts.set(i.status, (counts.get(i.status) ?? 0) + 1);
5050
+ return TASK_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((status) => ({
5051
+ status,
5052
+ count: counts.get(status)
5053
+ }));
5054
+ }
5055
+ function formatReportBody(data) {
5056
+ const lines = [];
5057
+ const titleSuffix = data.title !== void 0 ? ` \u2014 ${data.title}` : "";
5058
+ lines.push(`# Report${titleSuffix}`);
5059
+ lines.push("");
5060
+ const periodSuffix = data.period.from !== null && data.period.to !== null ? ` (${data.period.from.slice(0, 10)}..${data.period.to.slice(0, 10)})` : "";
5061
+ lines.push(`> Generated at ${data.generatedAt}${periodSuffix}`);
5062
+ lines.push("");
5063
+ lines.push("## \u6982\u8981");
5064
+ lines.push("");
5065
+ lines.push(`- ${formatSessionsLine(data)}`);
5066
+ lines.push(
5067
+ `- Active time ${formatDurationMs(data.time.activeMs)}, ${formatInt(data.volume.outputTokens)} output tokens`
5068
+ );
5069
+ lines.push("");
5070
+ lines.push("## \u4F5C\u696D\u91CF");
5071
+ lines.push("");
5072
+ const tokenCaveat = data.volume.tokensAvailable ? "" : " (no token data captured)";
5073
+ lines.push(`- Output tokens: ${formatInt(data.volume.outputTokens)}${tokenCaveat}`);
5074
+ if (data.volume.reasoningTokens > 0) {
5075
+ lines.push(`- Reasoning tokens: ${formatInt(data.volume.reasoningTokens)} (Codex)`);
5076
+ }
5077
+ lines.push(
5078
+ `- Actions: ${data.volume.commandCount} commands, ${data.volume.fileChangedCount} files, ${data.volume.decisionCount} decisions`
5079
+ );
5080
+ lines.push(
5081
+ `- Active time: ${formatDurationMs(data.time.activeMs)} (union; idle gaps > 5m excluded; tz ${data.time.timeZone})`
5082
+ );
5083
+ if (data.time.machineAvailable) {
5084
+ lines.push(
5085
+ `- Model working: ${formatDurationMs(data.time.machineActiveMs)} (model compute, subset of active)`
5086
+ );
5087
+ }
5088
+ lines.push(`- Span: ${formatDurationMs(data.time.spanMs)} (total elapsed)`);
5089
+ lines.push("");
5090
+ lines.push("## \u5224\u65AD");
5091
+ lines.push("");
5092
+ if (data.decisions.items.length === 0) {
5093
+ lines.push("(no decisions recorded yet)");
5094
+ } else {
5095
+ const total = data.decisions.items.length;
5096
+ const shown = total > DECISIONS_MARKDOWN_LIMIT ? data.decisions.items.slice(-DECISIONS_MARKDOWN_LIMIT) : data.decisions.items;
5097
+ if (total > DECISIONS_MARKDOWN_LIMIT) {
5098
+ lines.push(`(showing the ${DECISIONS_MARKDOWN_LIMIT} most recent of ${total})`);
5099
+ lines.push("");
5100
+ }
5101
+ for (const d of shown) {
5102
+ lines.push(`- ${d.occurredAt.slice(0, 10)} \xB7 ${d.title}`);
5103
+ }
5104
+ }
5105
+ lines.push("");
5106
+ lines.push("## \u627F\u8A8D");
5107
+ lines.push("");
5108
+ if (data.approvals.items.length === 0) {
5109
+ lines.push("(none)");
5110
+ } else {
5111
+ const a = data.approvals;
5112
+ lines.push(
5113
+ `Pending ${a.pending} \xB7 Approved ${a.approved} \xB7 Rejected ${a.rejected} \xB7 Expired ${a.expired}`
5114
+ );
5115
+ lines.push("");
5116
+ for (const item of data.approvals.items.slice(0, APPROVALS_MARKDOWN_LIMIT)) {
5117
+ lines.push(`- ${item.reason} (${item.status}, ${item.riskLevel})`);
5118
+ }
5119
+ const overflow = data.approvals.items.length - APPROVALS_MARKDOWN_LIMIT;
5120
+ if (overflow > 0) lines.push(`- ... +${overflow} more`);
5121
+ }
5122
+ lines.push("");
5123
+ lines.push("## \u30BF\u30B9\u30AF");
5124
+ lines.push("");
5125
+ if (data.tasks.items.length === 0) {
5126
+ lines.push("(no tasks recorded yet)");
5127
+ } else {
5128
+ const breakdown = data.tasks.byStatus.map((s) => `${s.status} ${s.count}`).join(", ");
5129
+ lines.push(`Tasks: ${data.tasks.total} (${breakdown})`);
5130
+ lines.push("");
5131
+ for (const item of data.tasks.items.slice(0, TASKS_MARKDOWN_LIMIT)) {
5132
+ lines.push(`- ${item.title} (${item.status})`);
5133
+ }
5134
+ const overflow = data.tasks.items.length - TASKS_MARKDOWN_LIMIT;
5135
+ if (overflow > 0) lines.push(`- ... +${overflow} more`);
5136
+ }
5137
+ lines.push("");
5138
+ lines.push("## \u5909\u66F4\u30D5\u30A1\u30A4\u30EB");
5139
+ lines.push("");
5140
+ if (data.changedFiles.length === 0) {
5141
+ lines.push("(no related files recorded)");
5142
+ } else {
5143
+ for (const f of data.changedFiles.slice(0, CHANGED_FILES_MARKDOWN_LIMIT)) lines.push(`- ${f}`);
5144
+ const overflow = data.changedFiles.length - CHANGED_FILES_MARKDOWN_LIMIT;
5145
+ if (overflow > 0) lines.push(`- ... +${overflow} more`);
5146
+ }
5147
+ lines.push("");
5148
+ lines.push("## \u30BB\u30C3\u30B7\u30E7\u30F3\u4E00\u89A7");
5149
+ lines.push("");
5150
+ if (data.sessions.items.length === 0) {
5151
+ lines.push("(no sessions yet)");
5152
+ } else {
5153
+ lines.push("| started_at | source | status | active | out tok |");
5154
+ lines.push("|---|---|---|---|---|");
5155
+ for (const s of data.sessions.items.slice(0, SESSIONS_MARKDOWN_LIMIT)) {
5156
+ lines.push(
5157
+ `| ${s.startedAt} | ${s.source} | ${s.status} | ${formatDurationMs(s.activeMs)} | ${formatInt(s.outputTokens)} |`
5158
+ );
5159
+ }
5160
+ const overflow = data.sessions.items.length - SESSIONS_MARKDOWN_LIMIT;
5161
+ if (overflow > 0) {
5162
+ lines.push("");
5163
+ lines.push(`... +${overflow} more sessions`);
5164
+ }
5165
+ }
5166
+ lines.push("");
5167
+ lines.push("## \u6574\u5408\u6027");
5168
+ lines.push("");
5169
+ const i = data.integrity;
5170
+ lines.push(
5171
+ `Provenance internally tamper-checked: ${i.verified} verified, ${i.unchained} unchained, ${i.empty} empty, ${i.incomplete} incomplete, ${i.in_progress} in_progress, ${i.tampered} tampered (of ${i.total} sessions).`
5172
+ );
5173
+ lines.push("");
5174
+ lines.push(
5175
+ "This reflects internal consistency of the local event-log hash chain \u2014 not a third-party cryptographic proof."
5176
+ );
5177
+ if (i.tampered > 0) {
5178
+ lines.push("");
5179
+ for (const id of i.tamperedSessions) lines.push(`- Tampered: ${id}`);
5180
+ }
5181
+ return lines.join("\n");
5182
+ }
5183
+ function formatSessionsLine(data) {
5184
+ const counts = /* @__PURE__ */ new Map();
5185
+ for (const s of data.sessions.byStatus) counts.set(s.status, s.count);
5186
+ const breakdown = SESSION_STATUS_ORDER.filter((s) => (counts.get(s) ?? 0) > 0).map((s) => `${s} ${counts.get(s)}`).join(", ");
5187
+ return breakdown !== "" ? `Sessions: ${data.sessions.total} (${breakdown})` : `Sessions: ${data.sessions.total}`;
5188
+ }
5189
+ function formatInt(n) {
5190
+ return n.toLocaleString("en-US");
5191
+ }
5192
+
5193
+ // src/runtime/child-process-runner.ts
5194
+ import { spawn as spawn2 } from "child_process";
5195
+ var DEFAULT_KILL_GRACE_MS = 5e3;
5196
+ var ChildProcessRunner = class {
5197
+ async run(command, args, options) {
5198
+ validateOptions(options);
5199
+ if (options.signal?.aborted) {
5200
+ throw new Error("Process aborted before spawn", {
5201
+ cause: options.signal.reason
5202
+ });
5203
+ }
5204
+ const snapshotCommand = command;
5205
+ const snapshotArgs = [...args];
5206
+ const snapshotCwd = options.cwd;
5207
+ const captureMode = options.capture ?? "buffer";
5208
+ const started_at = /* @__PURE__ */ new Date();
5209
+ let child;
5210
+ try {
5211
+ child = spawn2(snapshotCommand, [...snapshotArgs], {
5212
+ cwd: snapshotCwd,
5213
+ env: options.env ?? process.env,
5214
+ stdio: captureMode === "none" ? ["inherit", "inherit", "inherit"] : ["pipe", "pipe", "pipe"],
5215
+ shell: false,
5216
+ detached: false
5217
+ });
5218
+ } catch (error) {
5219
+ throw classifySpawnError(error);
5220
+ }
5221
+ if (options.onSpawn) {
5222
+ try {
5223
+ options.onSpawn(child);
5224
+ } catch {
5225
+ }
5226
+ }
5227
+ let timeoutTimer = null;
5228
+ let killTimer = null;
5229
+ let killed = false;
5230
+ let settled = false;
5231
+ const triggerKill = () => {
5232
+ if (killed || child.exitCode !== null) return;
5233
+ killed = true;
5234
+ child.kill("SIGTERM");
5235
+ killTimer = setTimeout(() => {
5236
+ if (child.exitCode === null) {
5237
+ child.kill("SIGKILL");
5238
+ }
5239
+ }, DEFAULT_KILL_GRACE_MS);
5240
+ };
5241
+ const onAbort = () => {
5242
+ triggerKill();
5243
+ };
5244
+ options.signal?.addEventListener("abort", onAbort);
5245
+ if (options.signal?.aborted) {
5246
+ triggerKill();
5247
+ }
5248
+ let stdout = "";
5249
+ let stderr = "";
5250
+ if (captureMode === "buffer") {
5251
+ child.stdout?.setEncoding("utf8");
5252
+ child.stderr?.setEncoding("utf8");
5253
+ child.stdout?.on("data", (chunk) => {
5254
+ stdout += chunk;
5255
+ });
5256
+ child.stderr?.on("data", (chunk) => {
5257
+ stderr += chunk;
5258
+ });
5259
+ if (options.stdin !== void 0) {
5260
+ child.stdin?.end(options.stdin);
5261
+ } else {
5262
+ child.stdin?.end();
5263
+ }
5264
+ }
5265
+ if (options.timeout_ms !== void 0) {
5266
+ timeoutTimer = setTimeout(triggerKill, options.timeout_ms);
5267
+ }
5268
+ const cleanup = () => {
5269
+ if (timeoutTimer !== null) clearTimeout(timeoutTimer);
5270
+ if (killTimer !== null) clearTimeout(killTimer);
5271
+ options.signal?.removeEventListener("abort", onAbort);
5272
+ };
5273
+ return new Promise((resolve2, reject) => {
5274
+ child.once("error", (error) => {
5275
+ if (settled) return;
5276
+ settled = true;
5277
+ cleanup();
5278
+ reject(classifySpawnError(error));
5279
+ });
5280
+ child.once("close", (code, signal) => {
5281
+ if (settled) return;
5282
+ settled = true;
5283
+ cleanup();
5284
+ const ended_at = /* @__PURE__ */ new Date();
5285
+ resolve2({
5286
+ command: snapshotCommand,
5287
+ args: snapshotArgs,
5288
+ cwd: snapshotCwd,
5289
+ exit_code: code,
5290
+ signal,
5291
+ stdout,
5292
+ stderr,
5293
+ started_at: started_at.toISOString(),
5294
+ ended_at: ended_at.toISOString(),
5295
+ duration_ms: ended_at.getTime() - started_at.getTime(),
5296
+ pid: child.pid ?? null
5297
+ });
5298
+ });
5299
+ });
5300
+ }
5301
+ };
5302
+ function validateOptions(options) {
5303
+ if (options.timeout_ms !== void 0 && (!Number.isFinite(options.timeout_ms) || options.timeout_ms <= 0)) {
5304
+ throw new Error("Invalid timeout_ms");
5305
+ }
5306
+ if (options.capture === "none" && options.stdin !== void 0) {
5307
+ throw new Error('Combination of capture: "none" and stdin is not supported');
5308
+ }
5309
+ }
5310
+ function classifySpawnError(error) {
5311
+ if (findErrorCode(error, "ENOENT")) {
5312
+ return new Error("Command not found", { cause: error });
5313
+ }
5314
+ return new Error("Failed to spawn child process", { cause: error });
5315
+ }
5316
+
5317
+ // src/schemas/json-schema.ts
5318
+ import { z as z11 } from "zod";
5319
+
5320
+ // src/schemas/session-import.schema.ts
5321
+ import { z as z10 } from "zod";
5322
+ var SessionInnerImportSchema = z10.object({
5323
+ id: SessionIdSchema.optional(),
5324
+ label: z10.string().optional(),
5325
+ task_id: TaskIdSchema.nullable().optional(),
5326
+ workspace_id: WorkspaceIdSchema,
5327
+ source: z10.object({
5328
+ kind: SessionSourceKindSchema,
5329
+ version: z10.literal("0.1.0"),
5330
+ // Source-tool-native id (e.g. Claude Code session UUID), retained so
5331
+ // re-imports of the same source can be deduplicated.
5332
+ external_id: z10.string().optional(),
5333
+ // Byte size of the source native log at import time. Declared here too
5334
+ // (not only in session.schema.ts) because this inner `source` object is
5335
+ // a plain z.object: zod strips keys it does not declare, so a field
5336
+ // absent here would be dropped from the parsed payload before persist
5337
+ // and the size could never be stored.
5338
+ source_size_bytes: z10.number().int().nonnegative().optional()
5339
+ }),
5340
+ started_at: IsoTimestampSchema,
5341
+ ended_at: IsoTimestampSchema.optional(),
5342
+ status: SessionStatusSchema,
5343
+ working_directory: z10.string().min(1),
5344
+ invocation: z10.object({
5345
+ command: z10.string().min(1),
5346
+ args: z10.array(z10.string()),
5347
+ exit_code: z10.number().int().nullable()
5348
+ }),
5349
+ related_files: z10.array(z10.string()).default([]),
5350
+ events_log: z10.string().optional(),
5351
+ summary: z10.string().nullable().optional(),
5352
+ metrics: SessionMetricsSchema.optional(),
5353
+ // Accepted so a payload assembled from an on-disk chained session.yaml
5354
+ // round-trips, and DISCARDED by the importer (buildSessionRecord never
5355
+ // copies it): the integrity anchor is computed at write time, never
5356
+ // imported. Mirrors the accept-and-discard of `prev_hash` on events.
5357
+ integrity: SessionIntegritySchema.optional()
5358
+ }).strict();
5359
+ var SessionImportPayloadSchema = z10.object({
5360
+ schema_version: z10.string(),
5361
+ session: SessionInnerImportSchema,
5362
+ events: z10.array(EventSchema)
5363
+ }).strict();
5364
+
5365
+ // src/schemas/json-schema.ts
5366
+ var JSON_SCHEMA_VERSION = "0.1.0";
5367
+ var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
5368
+ var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
5369
+ var DOCUMENTS = [
5370
+ {
5371
+ name: "manifest",
5372
+ schema: ManifestSchema,
5373
+ title: "Basou Manifest",
5374
+ description: "The `.basou/manifest.yaml` workspace manifest."
5375
+ },
5376
+ {
5377
+ name: "session",
5378
+ schema: SessionSchema,
5379
+ title: "Basou Session",
5380
+ description: "A `.basou/sessions/<id>/session.yaml` session record."
5381
+ },
5382
+ {
5383
+ name: "event",
5384
+ schema: EventSchema,
5385
+ title: "Basou Event",
5386
+ description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
5387
+ },
5388
+ {
5389
+ name: "task",
5390
+ schema: TaskSchema,
5391
+ title: "Basou Task",
5392
+ description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
5393
+ },
5394
+ {
5395
+ name: "approval",
5396
+ schema: ApprovalSchema,
5397
+ title: "Basou Approval",
5398
+ description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
5399
+ },
5400
+ {
5401
+ name: "status",
5402
+ schema: StatusSchema,
5403
+ title: "Basou Status",
5404
+ description: "The `.basou/status.json` workspace status snapshot."
5405
+ },
5406
+ {
5407
+ name: "task-index",
5408
+ schema: TaskIndexSchema,
5409
+ title: "Basou Task Index",
5410
+ description: "The `.basou/tasks/index.json` task lookup index."
5411
+ },
5412
+ {
5413
+ name: "session-import",
5414
+ schema: SessionImportPayloadSchema,
5415
+ title: "Basou Session Import Payload",
5416
+ description: "The portable session payload consumed by `basou session import`."
5417
+ }
5418
+ ];
5419
+ function buildJsonSchemas() {
5420
+ return DOCUMENTS.map((doc) => {
5421
+ const generated = z11.toJSONSchema(doc.schema, { io: "input" });
5422
+ const { $schema, ...rest } = generated;
5423
+ const schema = {
5424
+ $schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
5425
+ $id: `${ID_BASE}/${doc.name}.schema.json`,
5426
+ title: doc.title,
5427
+ description: doc.description,
5428
+ ...rest
5429
+ };
5430
+ return { name: doc.name, schema };
5431
+ });
5432
+ }
5433
+ function serializeJsonSchema(schema) {
5434
+ return `${JSON.stringify(schema, null, 2)}
5435
+ `;
5436
+ }
5437
+
5438
+ // src/storage/basou-dir.ts
5439
+ import { lstat as lstat4, mkdir as mkdir4 } from "fs/promises";
5440
+ import { join as join17 } from "path";
5441
+ function basouPaths(repositoryRoot) {
5442
+ const root = join17(repositoryRoot, ".basou");
5443
+ const approvalsBase = join17(root, "approvals");
5444
+ return {
5445
+ root,
5446
+ sessions: join17(root, "sessions"),
5447
+ tasks: join17(root, "tasks"),
5448
+ approvals: {
5449
+ pending: join17(approvalsBase, "pending"),
5450
+ resolved: join17(approvalsBase, "resolved")
5451
+ },
5452
+ locks: join17(root, "locks"),
5453
+ logs: join17(root, "logs"),
5454
+ raw: join17(root, "raw"),
5455
+ tmp: join17(root, "tmp"),
5456
+ files: {
5457
+ manifest: join17(root, "manifest.yaml"),
5458
+ status: join17(root, "status.json"),
5459
+ handoff: join17(root, "handoff.md"),
5460
+ decisions: join17(root, "decisions.md"),
5461
+ orientation: join17(root, "orientation.md")
5462
+ }
5463
+ };
5464
+ }
5465
+ var PATH_LABELS = {
5466
+ sessions: ".basou/sessions",
5467
+ tasks: ".basou/tasks",
5468
+ approvalsPending: ".basou/approvals/pending",
5469
+ approvalsResolved: ".basou/approvals/resolved",
5470
+ locks: ".basou/locks",
5471
+ logs: ".basou/logs",
5472
+ raw: ".basou/raw",
5473
+ tmp: ".basou/tmp"
5474
+ };
5475
+ async function ensureBasouDirectory(repositoryRoot) {
5476
+ const paths = basouPaths(repositoryRoot);
5477
+ let existing;
5478
+ try {
5479
+ existing = await lstat4(paths.root);
5480
+ } catch (error) {
5481
+ if (!hasErrorCode4(error) || error.code !== "ENOENT") {
5482
+ throw new Error("Failed to inspect .basou directory", { cause: error });
5483
+ }
5484
+ }
5485
+ if (existing !== void 0 && !existing.isDirectory()) {
5486
+ throw new Error("Basou root .basou exists but is not a directory");
5487
+ }
5488
+ await Promise.all([
5489
+ mkdirLabeled(paths.sessions, PATH_LABELS.sessions),
5490
+ mkdirLabeled(paths.tasks, PATH_LABELS.tasks),
5491
+ mkdirLabeled(paths.approvals.pending, PATH_LABELS.approvalsPending),
5492
+ mkdirLabeled(paths.approvals.resolved, PATH_LABELS.approvalsResolved),
5493
+ mkdirLabeled(paths.locks, PATH_LABELS.locks),
4718
5494
  mkdirLabeled(paths.logs, PATH_LABELS.logs),
4719
5495
  mkdirLabeled(paths.raw, PATH_LABELS.raw),
4720
5496
  mkdirLabeled(paths.tmp, PATH_LABELS.tmp)
@@ -4725,13 +5501,13 @@ async function mkdirLabeled(target, label) {
4725
5501
  try {
4726
5502
  await mkdir4(target, { recursive: true });
4727
5503
  } catch (error) {
4728
- if (hasErrorCode3(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
5504
+ if (hasErrorCode4(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
4729
5505
  throw new Error(`${label} exists but is not a directory`, { cause: error });
4730
5506
  }
4731
5507
  throw new Error(`Failed to create ${label}`, { cause: error });
4732
5508
  }
4733
5509
  }
4734
- function hasErrorCode3(error) {
5510
+ function hasErrorCode4(error) {
4735
5511
  if (!(error instanceof Error)) return false;
4736
5512
  const codeProp = error.code;
4737
5513
  return typeof codeProp === "string";
@@ -4739,28 +5515,30 @@ function hasErrorCode3(error) {
4739
5515
 
4740
5516
  // src/storage/gitignore.ts
4741
5517
  import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
4742
- import { join as join15 } from "path";
5518
+ import { join as join18 } from "path";
4743
5519
  var MARKER = "# Basou - default ignore";
4744
- 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";
4745
- async function appendBasouGitignore(repositoryRoot) {
4746
- const gitignorePath = join15(repositoryRoot, ".gitignore");
5520
+ 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/orientation.md\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";
5521
+ var BASOU_GITIGNORE_BLOCK_LOCAL_ONLY = "# Basou - default ignore\n# Local-only: basou's trail is never committed (personal/local state,\n# regenerable by re-importing from the agents' own logs). Recommended for\n# monitored repos and any workspace kept out of version control.\n.basou/\n";
5522
+ async function appendBasouGitignore(repositoryRoot, options = {}) {
5523
+ const gitignorePath = join18(repositoryRoot, ".gitignore");
4747
5524
  let body;
4748
5525
  let existed;
4749
5526
  try {
4750
5527
  body = await readFile8(gitignorePath, "utf8");
4751
5528
  existed = true;
4752
5529
  } catch (error) {
4753
- if (hasErrorCode4(error) && error.code === "ENOENT") {
5530
+ if (hasErrorCode5(error) && error.code === "ENOENT") {
4754
5531
  body = "";
4755
5532
  existed = false;
4756
5533
  } else {
4757
5534
  throw new Error("Failed to read .gitignore", { cause: error });
4758
5535
  }
4759
5536
  }
4760
- if (existed && hasBasouMarker(body)) {
5537
+ if (existed && hasBasouGitignore(body)) {
4761
5538
  return { appended: false };
4762
5539
  }
4763
- const next = composeNextBody(body);
5540
+ const block = options.localOnly === true ? BASOU_GITIGNORE_BLOCK_LOCAL_ONLY : BASOU_GITIGNORE_BLOCK;
5541
+ const next = composeNextBody(body, block);
4764
5542
  try {
4765
5543
  await writeFile2(gitignorePath, next, { encoding: "utf8" });
4766
5544
  } catch (error) {
@@ -4768,84 +5546,20 @@ async function appendBasouGitignore(repositoryRoot) {
4768
5546
  }
4769
5547
  return { appended: true };
4770
5548
  }
4771
- function hasBasouMarker(body) {
5549
+ function hasBasouGitignore(body) {
4772
5550
  for (const rawLine of body.split("\n")) {
4773
- if (rawLine.trimEnd().startsWith(MARKER)) return true;
5551
+ const line = rawLine.trimEnd();
5552
+ if (line.startsWith(MARKER)) return true;
5553
+ if (line === ".basou/" || line === "/.basou/") return true;
4774
5554
  }
4775
5555
  return false;
4776
5556
  }
4777
- function composeNextBody(existing) {
4778
- if (existing.length === 0) return BASOU_GITIGNORE_BLOCK;
5557
+ function composeNextBody(existing, block) {
5558
+ if (existing.length === 0) return block;
4779
5559
  const normalized = existing.endsWith("\n") ? existing : `${existing}
4780
5560
  `;
4781
5561
  return `${normalized}
4782
- ${BASOU_GITIGNORE_BLOCK}`;
4783
- }
4784
- function hasErrorCode4(error) {
4785
- if (!(error instanceof Error)) return false;
4786
- return typeof error.code === "string";
4787
- }
4788
-
4789
- // src/storage/manifest.ts
4790
- import { lstat as lstat4 } from "fs/promises";
4791
- function createManifest(input) {
4792
- if (input.workspaceName.length === 0) {
4793
- throw new Error("Workspace name is empty. Pass --name explicitly.");
4794
- }
4795
- const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
4796
- const workspaceId = input.workspaceId ?? prefixedUlid("ws");
4797
- const project = {
4798
- ...input.projectName !== void 0 ? { name: input.projectName } : {},
4799
- ...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
4800
- ...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
4801
- };
4802
- const manifest = {
4803
- schema_version: "0.1.0",
4804
- basou_version: "0.1.0",
4805
- workspace: {
4806
- id: workspaceId,
4807
- name: input.workspaceName,
4808
- created_at: now,
4809
- updated_at: now
4810
- },
4811
- project,
4812
- capabilities: {
4813
- enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
4814
- },
4815
- approval: {
4816
- required_for: ["destructive_command", "external_send"],
4817
- default_risk_level: "medium"
4818
- },
4819
- adapters: {
4820
- "claude-code": { enabled: true }
4821
- },
4822
- git: { events_log: "ignore" },
4823
- ...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
4824
- };
4825
- return ManifestSchema.parse(manifest);
4826
- }
4827
- async function writeManifest(paths, manifest, options) {
4828
- const force = options?.force === true;
4829
- const validated = ManifestSchema.parse(manifest);
4830
- if (!force) {
4831
- let existed = false;
4832
- try {
4833
- await lstat4(paths.files.manifest);
4834
- existed = true;
4835
- } catch (error) {
4836
- if (!hasErrorCode5(error) || error.code !== "ENOENT") {
4837
- throw new Error("Failed to inspect existing manifest", { cause: error });
4838
- }
4839
- }
4840
- if (existed) {
4841
- throw new Error("Already initialized. Use --force to overwrite.");
4842
- }
4843
- }
4844
- await writeYamlFile(paths.files.manifest, validated);
4845
- }
4846
- async function readManifest(paths) {
4847
- const raw = await readYamlFile(paths.files.manifest);
4848
- return ManifestSchema.parse(raw);
5562
+ ${block}`;
4849
5563
  }
4850
5564
  function hasErrorCode5(error) {
4851
5565
  if (!(error instanceof Error)) return false;
@@ -4958,7 +5672,7 @@ function hasErrorCode6(error) {
4958
5672
  // src/storage/session-import.ts
4959
5673
  import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
4960
5674
  import { homedir as homedir2 } from "os";
4961
- import { join as join16 } from "path";
5675
+ import { join as join19 } from "path";
4962
5676
  async function importSessionFromJson(paths, manifest, payload, options) {
4963
5677
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
4964
5678
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -4983,7 +5697,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4983
5697
  pathSanitizeReport
4984
5698
  };
4985
5699
  }
4986
- const sessionDir = join16(paths.sessions, newSessionId);
5700
+ const sessionDir = join19(paths.sessions, newSessionId);
4987
5701
  try {
4988
5702
  await mkdir5(sessionDir, { recursive: true });
4989
5703
  } catch (error) {
@@ -4997,7 +5711,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4997
5711
  throw error;
4998
5712
  }
4999
5713
  try {
5000
- const sessionYamlPath = join16(sessionDir, "session.yaml");
5714
+ const sessionYamlPath = join19(sessionDir, "session.yaml");
5001
5715
  await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
5002
5716
  } catch (error) {
5003
5717
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
@@ -5165,7 +5879,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
5165
5879
  async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
5166
5880
  const sessionId = priorSessionId;
5167
5881
  const importSource = freshPayload.session.source.kind;
5168
- const sessionDir = join16(paths.sessions, priorSessionId);
5882
+ const sessionDir = join19(paths.sessions, priorSessionId);
5169
5883
  const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
5170
5884
  try {
5171
5885
  const priorVerdict = await verifyEventsChain(paths, priorSessionId);
@@ -5207,7 +5921,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5207
5921
  };
5208
5922
  const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
5209
5923
  if (options.dryRun !== true) {
5210
- const eventsPath = join16(sessionDir, "events.jsonl");
5924
+ const eventsPath = join19(sessionDir, "events.jsonl");
5211
5925
  let priorEventsRaw = null;
5212
5926
  try {
5213
5927
  priorEventsRaw = await readFile10(eventsPath);
@@ -5219,7 +5933,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5219
5933
  const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
5220
5934
  try {
5221
5935
  await overwriteYamlFile(
5222
- join16(sessionDir, "session.yaml"),
5936
+ join19(sessionDir, "session.yaml"),
5223
5937
  withIntegrity(updatedRecord, chainResult)
5224
5938
  );
5225
5939
  } catch (error) {
@@ -5243,7 +5957,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5243
5957
  }
5244
5958
  }
5245
5959
  async function rechainSessionInPlace(paths, sessionId, options = {}) {
5246
- const sessionDir = join16(paths.sessions, sessionId);
5960
+ const sessionDir = join19(paths.sessions, sessionId);
5247
5961
  let lock;
5248
5962
  try {
5249
5963
  lock = await acquireLock(paths, "session", sessionId);
@@ -5276,7 +5990,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5276
5990
  if (verdict.status !== "unchained") {
5277
5991
  return { status: "skipped", reason: "tampered" };
5278
5992
  }
5279
- const eventsPath = join16(sessionDir, "events.jsonl");
5993
+ const eventsPath = join19(sessionDir, "events.jsonl");
5280
5994
  let priorRaw;
5281
5995
  try {
5282
5996
  priorRaw = await readFile10(eventsPath);
@@ -5324,7 +6038,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5324
6038
  }
5325
6039
  try {
5326
6040
  await overwriteYamlFile(
5327
- join16(sessionDir, "session.yaml"),
6041
+ join19(sessionDir, "session.yaml"),
5328
6042
  withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
5329
6043
  );
5330
6044
  } catch (error) {
@@ -5406,6 +6120,7 @@ export {
5406
6120
  enumerateTaskIds,
5407
6121
  finalizeSessionYaml,
5408
6122
  findErrorCode,
6123
+ formatDurationMs,
5409
6124
  genesisHash,
5410
6125
  getDiff,
5411
6126
  getSnapshot,
@@ -5438,8 +6153,11 @@ export {
5438
6153
  reimportPreservingId,
5439
6154
  renderDecisions,
5440
6155
  renderHandoff,
6156
+ renderOrientation,
6157
+ renderReport,
5441
6158
  renderWithMarkers,
5442
6159
  replayEvents,
6160
+ resolveBasouRepositoryRoot,
5443
6161
  resolveClaudeCodeCommand,
5444
6162
  resolveRepositoryRoot,
5445
6163
  resolveSessionId,
@@ -5451,6 +6169,7 @@ export {
5451
6169
  serializeJsonSchema,
5452
6170
  sessionWorkStatsFromEvents,
5453
6171
  summarizeAdapterOutput,
6172
+ summarizeOrientation,
5454
6173
  tryRemoteUrl,
5455
6174
  ulid,
5456
6175
  updateTaskStatusWithEvent,