@basou/core 0.11.0 → 0.13.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
@@ -102,6 +102,14 @@ function sumDurations(intervals) {
102
102
  return total;
103
103
  }
104
104
 
105
+ // src/adapters/session-label.ts
106
+ function sessionLabelDateSpan(startIso, endIso) {
107
+ const a = startIso.slice(0, 10);
108
+ const b = endIso.slice(0, 10);
109
+ if (a === b) return a;
110
+ return a < b ? `${a}..${b}` : `${b}..${a}`;
111
+ }
112
+
105
113
  // src/adapters/claude-code/transcript-importer.ts
106
114
  var CLAUDE_IMPORT_SOURCE = "claude-code-import";
107
115
  function claudeTranscriptToImportPayload(records, options) {
@@ -201,8 +209,7 @@ function claudeTranscriptToImportPayload(records, options) {
201
209
  const externalId = options.externalId ?? claudeSessionId;
202
210
  const commandCount = derived.reduce((n, e) => e.type === "command_executed" ? n + 1 : n, 0);
203
211
  const fileCount = relatedFiles.size;
204
- const date = minTs.slice(0, 10);
205
- const label = `claude-code ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
212
+ const label = `claude-code ${sessionLabelDateSpan(minTs, maxTs)}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
206
213
  const active = engagementTsMs.length >= 2 ? activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS) : void 0;
207
214
  const metricsFields = {
208
215
  ...outputTokens > 0 ? { output_tokens: outputTokens } : {},
@@ -414,8 +421,7 @@ function codexRolloutToImportPayload(records, options) {
414
421
  ];
415
422
  const externalId = options.externalId ?? codexSessionId;
416
423
  const commandCount = derived.length;
417
- const date = minTs.slice(0, 10);
418
- const label = `codex ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
424
+ const label = `codex ${sessionLabelDateSpan(minTs, maxTs)}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
419
425
  const minTsMs = Date.parse(minTs);
420
426
  const turnIntervals = [];
421
427
  let machineActiveMs = 0;
@@ -905,7 +911,12 @@ var TaskArchivedEventSchema = BaseEventSchema.extend({
905
911
  }).strict();
906
912
  var NoteAddedEventSchema = BaseEventSchema.extend({
907
913
  type: z3.literal("note_added"),
908
- body: z3.string()
914
+ body: z3.string(),
915
+ // `next_step` marks a note authored by `basou note` as the operator's resume
916
+ // hint, which orientation surfaces as the next starting point. Absent (the
917
+ // `basou session note` default) is a plain annotation orientation does not
918
+ // surface. Optional so pre-existing note_added events remain valid.
919
+ kind: z3.enum(["note", "next_step"]).optional()
909
920
  });
910
921
  var AdapterOutputEventSchema = BaseEventSchema.extend({
911
922
  type: z3.literal("adapter_output"),
@@ -1720,6 +1731,8 @@ function splitLinesBytes2(buf) {
1720
1731
  }
1721
1732
 
1722
1733
  // src/git/snapshot.ts
1734
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
1735
+ import { join as join9 } from "path";
1723
1736
  import { simpleGit } from "simple-git";
1724
1737
 
1725
1738
  // src/storage/status.ts
@@ -1757,19 +1770,19 @@ var DIRECTORY_CHECKS = {
1757
1770
  tmp: (p) => p.tmp
1758
1771
  };
1759
1772
  async function assertBasouRootSafe(rootPath) {
1760
- let stat3;
1773
+ let stat4;
1761
1774
  try {
1762
- stat3 = await fsp.lstat(rootPath);
1775
+ stat4 = await fsp.lstat(rootPath);
1763
1776
  } catch (error) {
1764
1777
  if (hasErrorCode2(error) && error.code === "ENOENT") {
1765
1778
  throw new Error("Basou workspace not found", { cause: error });
1766
1779
  }
1767
1780
  throw new Error("Failed to inspect .basou root", { cause: error });
1768
1781
  }
1769
- if (stat3.isSymbolicLink()) {
1782
+ if (stat4.isSymbolicLink()) {
1770
1783
  throw new Error(".basou root is a symlink; refusing to operate");
1771
1784
  }
1772
- if (!stat3.isDirectory()) {
1785
+ if (!stat4.isDirectory()) {
1773
1786
  throw new Error(".basou root exists but is not a directory");
1774
1787
  }
1775
1788
  }
@@ -1849,6 +1862,14 @@ function isGitNotFound(error) {
1849
1862
  }
1850
1863
  return false;
1851
1864
  }
1865
+ function isNotAGitRepository(error) {
1866
+ let cur = error;
1867
+ for (let i = 0; i < 4 && cur instanceof Error; i++) {
1868
+ if (/not a git repository/i.test(cur.message)) return true;
1869
+ cur = cur.cause;
1870
+ }
1871
+ return false;
1872
+ }
1852
1873
  async function resolveRepositoryRoot(cwd) {
1853
1874
  const git = safeSimpleGit(cwd);
1854
1875
  try {
@@ -1864,8 +1885,53 @@ async function resolveRepositoryRoot(cwd) {
1864
1885
  if (error instanceof Error && error.message === "Not a git repository") {
1865
1886
  throw error;
1866
1887
  }
1867
- throw new Error("Not a git repository", { cause: error });
1888
+ if (isNotAGitRepository(error)) {
1889
+ throw new Error("Not a git repository", { cause: error });
1890
+ }
1891
+ throw new Error("Git command failed", { cause: error });
1892
+ }
1893
+ }
1894
+ async function resolveBasouRepositoryRoot(cwd, opts) {
1895
+ try {
1896
+ return await resolveRepositoryRoot(cwd);
1897
+ } catch (error) {
1898
+ if (!(error instanceof Error) || error.message !== "Not a git repository") throw error;
1899
+ const linked = await findLinkedBasouRepos(cwd);
1900
+ const only = linked[0];
1901
+ if (only !== void 0 && linked.length === 1) {
1902
+ opts?.onRedirect?.({ via: only.name, root: only.root });
1903
+ return only.root;
1904
+ }
1905
+ if (linked.length > 1) {
1906
+ const names = linked.map((l) => l.name).join(", ");
1907
+ throw new Error(
1908
+ `Ambiguous workspace view: ${linked.length} linked repos have a .basou store (${names}). cd into the one you want and re-run.`
1909
+ );
1910
+ }
1911
+ throw error;
1912
+ }
1913
+ }
1914
+ async function findLinkedBasouRepos(dir) {
1915
+ const entries = await readdir3(dir, { withFileTypes: true }).catch(() => null);
1916
+ if (entries === null) return [];
1917
+ const byRoot = /* @__PURE__ */ new Map();
1918
+ for (const entry of entries) {
1919
+ if (!entry.isSymbolicLink()) continue;
1920
+ let root;
1921
+ try {
1922
+ root = await resolveRepositoryRoot(join9(dir, entry.name));
1923
+ } catch {
1924
+ continue;
1925
+ }
1926
+ try {
1927
+ if (!(await stat2(join9(root, ".basou"))).isDirectory()) continue;
1928
+ } catch {
1929
+ continue;
1930
+ }
1931
+ const existing = byRoot.get(root);
1932
+ if (existing === void 0 || entry.name < existing) byRoot.set(root, entry.name);
1868
1933
  }
1934
+ return [...byRoot.entries()].map(([root, name]) => ({ name, root })).sort((a, b) => a.name.localeCompare(b.name));
1869
1935
  }
1870
1936
  async function tryRemoteUrl(repositoryRoot) {
1871
1937
  const git = safeSimpleGit(repositoryRoot);
@@ -2013,12 +2079,12 @@ function parseDiffNameStatus(raw) {
2013
2079
  }
2014
2080
 
2015
2081
  // src/handoff/handoff-renderer.ts
2016
- import { join as join12 } from "path";
2082
+ import { join as join13 } from "path";
2017
2083
 
2018
2084
  // src/storage/tasks.ts
2019
2085
  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";
2086
+ import { mkdir as mkdir3, readdir as readdir4, readFile as readFile7, rename as rename2, stat as stat3, unlink as unlink3 } from "fs/promises";
2087
+ import { join as join12 } from "path";
2022
2088
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2023
2089
  import { z as z8 } from "zod";
2024
2090
 
@@ -2062,7 +2128,7 @@ var TaskSchema = z6.object({
2062
2128
  // src/storage/ad-hoc-session.ts
2063
2129
  import { mkdir as mkdir2, rm } from "fs/promises";
2064
2130
  import { homedir } from "os";
2065
- import { join as join9 } from "path";
2131
+ import { join as join10 } from "path";
2066
2132
 
2067
2133
  // src/lib/path-sanitizer.ts
2068
2134
  import { posix as path } from "path";
@@ -2142,8 +2208,8 @@ async function createAdHocSessionWithEvent(input) {
2142
2208
  taskId: input.taskId ?? null
2143
2209
  })
2144
2210
  );
2145
- const sessionDir = join9(input.paths.sessions, sessionId);
2146
- const sessionYamlPath = join9(sessionDir, "session.yaml");
2211
+ const sessionDir = join10(input.paths.sessions, sessionId);
2212
+ const sessionYamlPath = join10(sessionDir, "session.yaml");
2147
2213
  const lock = await acquireLock(input.paths, "session", sessionId);
2148
2214
  let bulkResult = null;
2149
2215
  try {
@@ -2293,7 +2359,7 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
2293
2359
 
2294
2360
  // src/storage/task-index.ts
2295
2361
  import { readFile as readFile6 } from "fs/promises";
2296
- import { join as join10 } from "path";
2362
+ import { join as join11 } from "path";
2297
2363
 
2298
2364
  // src/schemas/task-index.schema.ts
2299
2365
  import { z as z7 } from "zod";
@@ -2312,7 +2378,7 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
2312
2378
 
2313
2379
  // src/storage/task-index.ts
2314
2380
  function taskIndexPath(paths) {
2315
- return join10(paths.tasks, "index.json");
2381
+ return join11(paths.tasks, "index.json");
2316
2382
  }
2317
2383
  async function readTaskIndex(paths) {
2318
2384
  const filePath = taskIndexPath(paths);
@@ -2426,7 +2492,7 @@ function splitFrontMatter(raw) {
2426
2492
  return { yamlText, body };
2427
2493
  }
2428
2494
  async function readTaskFile(paths, taskId) {
2429
- const filePath = join11(paths.tasks, `${taskId}.md`);
2495
+ const filePath = join12(paths.tasks, `${taskId}.md`);
2430
2496
  let raw;
2431
2497
  try {
2432
2498
  raw = await readFile7(filePath, "utf8");
@@ -2459,7 +2525,7 @@ async function readTaskFile(paths, taskId) {
2459
2525
  }
2460
2526
  async function writeTaskFile(paths, taskId, doc, options) {
2461
2527
  const validated = TaskSchema.parse(doc.task);
2462
- const filePath = join11(paths.tasks, `${taskId}.md`);
2528
+ const filePath = join12(paths.tasks, `${taskId}.md`);
2463
2529
  const yamlText = stringifyYaml(validated);
2464
2530
  const trimmedBody = doc.body.length === 0 ? "" : `
2465
2531
  ${doc.body.endsWith("\n") ? doc.body : `${doc.body}
@@ -2511,7 +2577,7 @@ async function enumerateTaskIds(paths) {
2511
2577
  async function enumerateTaskIdsFromDisk(paths) {
2512
2578
  let entries;
2513
2579
  try {
2514
- entries = (await readdir3(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2580
+ entries = (await readdir4(paths.tasks, { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2515
2581
  } catch (error) {
2516
2582
  if (findErrorCode(error, "ENOENT")) return [];
2517
2583
  throw new Error("Failed to enumerate tasks", { cause: error });
@@ -2544,12 +2610,12 @@ async function safeUpdateTaskIndex(paths, op) {
2544
2610
  }
2545
2611
  var ARCHIVE_DIR_NAME = "archive";
2546
2612
  function archiveTasksDir(paths) {
2547
- return join11(paths.tasks, ARCHIVE_DIR_NAME);
2613
+ return join12(paths.tasks, ARCHIVE_DIR_NAME);
2548
2614
  }
2549
2615
  async function enumerateArchivedTaskIds(paths) {
2550
2616
  let entries;
2551
2617
  try {
2552
- entries = (await readdir3(archiveTasksDir(paths), { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2618
+ entries = (await readdir4(archiveTasksDir(paths), { withFileTypes: true })).filter((d) => d.isFile()).map((d) => d.name);
2553
2619
  } catch (error) {
2554
2620
  if (findErrorCode(error, "ENOENT")) return [];
2555
2621
  throw new Error("Failed to enumerate archived tasks", { cause: error });
@@ -2574,7 +2640,7 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
2574
2640
  throw error;
2575
2641
  }
2576
2642
  }
2577
- const archiveFilePath = join11(archiveTasksDir(paths), `${taskId}.md`);
2643
+ const archiveFilePath = join12(archiveTasksDir(paths), `${taskId}.md`);
2578
2644
  let raw;
2579
2645
  try {
2580
2646
  raw = await readFile7(archiveFilePath, "utf8");
@@ -2868,7 +2934,7 @@ async function createTaskAttachLocked(input) {
2868
2934
  ...sessionDoc,
2869
2935
  session: { ...sessionDoc.session, task_id: input.taskId }
2870
2936
  };
2871
- await overwriteYamlFile(join11(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2937
+ await overwriteYamlFile(join12(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2872
2938
  } catch (error) {
2873
2939
  throw new TaskWriteAfterEventError({
2874
2940
  taskId: input.taskId,
@@ -3127,17 +3193,17 @@ function buildUpdatedDoc(input) {
3127
3193
  return { task: next, body: input.currentDoc.body };
3128
3194
  }
3129
3195
  async function computeTaskMdSnapshot(paths, taskId) {
3130
- const filePath = join11(paths.tasks, `${taskId}.md`);
3131
- const [stats, raw] = await Promise.all([stat2(filePath), readFile7(filePath)]);
3196
+ const filePath = join12(paths.tasks, `${taskId}.md`);
3197
+ const [stats, raw] = await Promise.all([stat3(filePath), readFile7(filePath)]);
3132
3198
  const hash = createHash2("sha256").update(raw).digest("hex");
3133
3199
  return { mtimeMs: stats.mtimeMs, hash };
3134
3200
  }
3135
3201
  async function readTaskFileWithSnapshot(paths, taskId) {
3136
- const filePath = join11(paths.tasks, `${taskId}.md`);
3202
+ const filePath = join12(paths.tasks, `${taskId}.md`);
3137
3203
  let rawBuffer;
3138
3204
  let stats;
3139
3205
  try {
3140
- [rawBuffer, stats] = await Promise.all([readFile7(filePath), stat2(filePath)]);
3206
+ [rawBuffer, stats] = await Promise.all([readFile7(filePath), stat3(filePath)]);
3141
3207
  } catch (error) {
3142
3208
  if (findErrorCode(error, "ENOENT")) {
3143
3209
  throw new Error("Task file not found", { cause: error });
@@ -3625,7 +3691,7 @@ async function deleteTaskLocked(input) {
3625
3691
  });
3626
3692
  const eventId = adHoc.targetEventIds[0];
3627
3693
  try {
3628
- await unlink3(join11(input.paths.tasks, `${input.taskId}.md`));
3694
+ await unlink3(join12(input.paths.tasks, `${input.taskId}.md`));
3629
3695
  } catch (error) {
3630
3696
  throw new TaskWriteAfterEventError({
3631
3697
  taskId: input.taskId,
@@ -3697,8 +3763,8 @@ async function archiveTaskLocked(input) {
3697
3763
  );
3698
3764
  await mkdir3(archiveTasksDir(input.paths), { recursive: true });
3699
3765
  await rename2(
3700
- join11(input.paths.tasks, `${input.taskId}.md`),
3701
- join11(archiveTasksDir(input.paths), `${input.taskId}.md`)
3766
+ join12(input.paths.tasks, `${input.taskId}.md`),
3767
+ join12(archiveTasksDir(input.paths), `${input.taskId}.md`)
3702
3768
  );
3703
3769
  } catch (error) {
3704
3770
  throw new TaskWriteAfterEventError({
@@ -3734,7 +3800,7 @@ async function renderHandoff(input) {
3734
3800
  const tasksCreated = [];
3735
3801
  const tasksStatusChanged = [];
3736
3802
  for (const entry of entries) {
3737
- const sessionDir = join12(input.paths.sessions, entry.sessionId);
3803
+ const sessionDir = join13(input.paths.sessions, entry.sessionId);
3738
3804
  try {
3739
3805
  for await (const ev of replayEvents(sessionDir, {
3740
3806
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -3850,11 +3916,11 @@ function formatHandoffBody(args) {
3850
3916
  if (args.latestSession !== void 0) {
3851
3917
  const status = args.latestSession.session.session.status;
3852
3918
  const label = args.latestSession.session.session.label;
3853
- const shortId = shortIdWithPrefix(args.latestSession.sessionId);
3919
+ const shortId2 = shortIdWithPrefix(args.latestSession.sessionId);
3854
3920
  if (label !== void 0 && label !== "") {
3855
- lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId}]`);
3921
+ lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId2}]`);
3856
3922
  } else {
3857
- lines.push(`- \u6700\u7D42 session: ${shortId} (${status})`);
3923
+ lines.push(`- \u6700\u7D42 session: ${shortId2} (${status})`);
3858
3924
  }
3859
3925
  } else {
3860
3926
  lines.push("- \u6700\u7D42 session: (no live sessions)");
@@ -4091,94 +4157,1194 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
4091
4157
  return matches[0];
4092
4158
  }
4093
4159
 
4094
- // src/report/report-renderer.ts
4160
+ // src/orientation/orientation-renderer.ts
4095
4161
  import { join as join14 } from "path";
4096
4162
 
4097
- // src/stats/work-stats.ts
4098
- import { join as join13 } from "path";
4099
- function resolveTimeZone(timeZone) {
4100
- if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4101
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
4163
+ // src/storage/manifest.ts
4164
+ import { lstat as lstat3 } from "fs/promises";
4165
+
4166
+ // src/schemas/manifest.schema.ts
4167
+ import { z as z9 } from "zod";
4168
+ var ProjectSchema = z9.looseObject({
4169
+ name: z9.string().optional(),
4170
+ description: z9.string().optional(),
4171
+ repository_url: z9.string().nullable().optional()
4172
+ });
4173
+ var CapabilitiesSchema = z9.looseObject({
4174
+ enabled: z9.array(z9.string())
4175
+ });
4176
+ var ApprovalConfigSchema = z9.looseObject({
4177
+ required_for: z9.array(z9.string()).optional(),
4178
+ default_risk_level: z9.enum(["low", "medium", "high", "critical"])
4179
+ });
4180
+ var ClaudeCodeAdapterConfigSchema = z9.looseObject({
4181
+ enabled: z9.boolean(),
4182
+ config_path: z9.string().optional()
4183
+ });
4184
+ var AdaptersSchema = z9.looseObject({
4185
+ "claude-code": ClaudeCodeAdapterConfigSchema
4186
+ });
4187
+ var GitConfigSchema = z9.looseObject({
4188
+ events_log: z9.enum(["ignore", "commit"]).default("ignore")
4189
+ });
4190
+ var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)(?!\s)[^\0\\]*[^\0\\\s]$/;
4191
+ var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
4192
+ message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
4193
+ });
4194
+ var ImportConfigSchema = z9.looseObject({
4195
+ source_roots: z9.array(SourceRootSchema).min(1).optional()
4196
+ });
4197
+ var RepoVisibilitySchema = z9.enum(["public", "private", "future-public"]);
4198
+ var RepoLanguageSchema = z9.enum(["en", "ja", "en+ja"]);
4199
+ var PublishKindSchema = z9.enum(["web", "npm"]);
4200
+ var PublishTargetSchema = z9.looseObject({
4201
+ kind: PublishKindSchema,
4202
+ visibility: RepoVisibilitySchema.optional(),
4203
+ language: RepoLanguageSchema.optional()
4204
+ });
4205
+ var RepoEntrySchema = z9.looseObject({
4206
+ path: SourceRootSchema,
4207
+ visibility: RepoVisibilitySchema.optional(),
4208
+ language: RepoLanguageSchema.optional(),
4209
+ publishes: z9.array(PublishTargetSchema).optional()
4210
+ });
4211
+ var WorkspaceMetaSchema = z9.looseObject({
4212
+ id: WorkspaceIdSchema,
4213
+ name: z9.string().min(1),
4214
+ created_at: IsoTimestampSchema,
4215
+ updated_at: IsoTimestampSchema,
4216
+ /**
4217
+ * The generated workspace view: a throwaway directory that aggregates the
4218
+ * roster repos via symlinks (one `<repo-basename>` symlink per repo). A path
4219
+ * relative to the manifest root, reusing the machine-portable source-root
4220
+ * constraint. Absent for a solo project (no view needed); `basou project
4221
+ * workspace` reconciles the view's symlinks to the declared roster.
4222
+ */
4223
+ view: SourceRootSchema.optional()
4224
+ });
4225
+ var ManifestSchema = z9.looseObject({
4226
+ schema_version: SchemaVersionSchema,
4227
+ basou_version: z9.literal("0.1.0"),
4228
+ workspace: WorkspaceMetaSchema,
4229
+ project: ProjectSchema,
4230
+ capabilities: CapabilitiesSchema,
4231
+ approval: ApprovalConfigSchema,
4232
+ adapters: AdaptersSchema,
4233
+ git: GitConfigSchema,
4234
+ import: ImportConfigSchema.optional(),
4235
+ repos: z9.array(RepoEntrySchema).min(1).optional()
4236
+ });
4237
+ var KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(ManifestSchema.shape));
4238
+ function unknownManifestKeys(manifest) {
4239
+ return Object.keys(manifest).filter((k) => !KNOWN_TOP_LEVEL_KEYS.has(k)).sort();
4102
4240
  }
4103
- var STATUS_ORDER = [
4104
- "completed",
4105
- "failed",
4106
- "running",
4107
- "interrupted",
4108
- "waiting_approval",
4109
- "initialized",
4110
- "imported",
4111
- "archived"
4112
- ];
4113
- async function computeWorkStats(input) {
4114
- const { now } = input;
4115
- const timeZone = resolveTimeZone(input.timeZone);
4116
- const unreadableEmitted = /* @__PURE__ */ new Set();
4117
- const wrappedSkip = (sid, reason) => {
4118
- if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
4119
- input.onSessionSkip?.(sid, reason);
4241
+
4242
+ // src/storage/manifest.ts
4243
+ function createManifest(input) {
4244
+ if (input.workspaceName.length === 0) {
4245
+ throw new Error("Workspace name is empty. Pass --name explicitly.");
4246
+ }
4247
+ const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
4248
+ const workspaceId = input.workspaceId ?? prefixedUlid("ws");
4249
+ const project = {
4250
+ ...input.projectName !== void 0 ? { name: input.projectName } : {},
4251
+ ...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
4252
+ ...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
4120
4253
  };
4121
- const loadOpts = { now, onSkip: wrappedSkip };
4254
+ const manifest = {
4255
+ schema_version: "0.1.0",
4256
+ basou_version: "0.1.0",
4257
+ workspace: {
4258
+ id: workspaceId,
4259
+ name: input.workspaceName,
4260
+ created_at: now,
4261
+ updated_at: now
4262
+ },
4263
+ project,
4264
+ capabilities: {
4265
+ enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
4266
+ },
4267
+ approval: {
4268
+ required_for: ["destructive_command", "external_send"],
4269
+ default_risk_level: "medium"
4270
+ },
4271
+ adapters: {
4272
+ "claude-code": { enabled: true }
4273
+ },
4274
+ git: { events_log: "ignore" },
4275
+ ...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
4276
+ };
4277
+ return ManifestSchema.parse(manifest);
4278
+ }
4279
+ async function writeManifest(paths, manifest, options) {
4280
+ const force = options?.force === true;
4281
+ const validated = ManifestSchema.parse(manifest);
4282
+ if (!force) {
4283
+ let existed = false;
4284
+ try {
4285
+ await lstat3(paths.files.manifest);
4286
+ existed = true;
4287
+ } catch (error) {
4288
+ if (!hasErrorCode3(error) || error.code !== "ENOENT") {
4289
+ throw new Error("Failed to inspect existing manifest", { cause: error });
4290
+ }
4291
+ }
4292
+ if (existed) {
4293
+ throw new Error("Already initialized. Use --force to overwrite.");
4294
+ }
4295
+ }
4296
+ await writeYamlFile(paths.files.manifest, validated);
4297
+ }
4298
+ async function readManifest(paths) {
4299
+ const raw = await readYamlFile(paths.files.manifest);
4300
+ return ManifestSchema.parse(raw);
4301
+ }
4302
+ function hasErrorCode3(error) {
4303
+ if (!(error instanceof Error)) return false;
4304
+ return typeof error.code === "string";
4305
+ }
4306
+
4307
+ // src/orientation/orientation-renderer.ts
4308
+ var DECISION_TRAILING_ACTIVITY_GAP_MS = 60 * 60 * 1e3;
4309
+ async function summarizeOrientation(input) {
4310
+ const limit = input.relatedFilesLimit ?? 10;
4311
+ const now = new Date(input.nowIso);
4312
+ const loadOpts = { now };
4313
+ if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
4122
4314
  if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4123
4315
  const entries = await loadSessionEntries(input.paths, loadOpts);
4124
- const sessions = [];
4316
+ const decisions = [];
4317
+ let latestActivityAt = null;
4318
+ let latestNote = null;
4319
+ const noteActivity = (iso) => {
4320
+ if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
4321
+ latestActivityAt = iso;
4322
+ }
4323
+ };
4125
4324
  for (const entry of entries) {
4126
- const events = [];
4127
- let eventsUnreadable = false;
4325
+ const sessionDir = join14(input.paths.sessions, entry.sessionId);
4326
+ const counted = entry.session.session.status !== "archived";
4327
+ if (counted) noteActivity(entry.session.session.ended_at ?? entry.session.session.started_at);
4128
4328
  try {
4129
- for await (const ev of replayEvents(join13(input.paths.sessions, entry.sessionId), {
4329
+ for await (const ev of replayEvents(sessionDir, {
4130
4330
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4131
4331
  })) {
4132
- events.push(ev);
4332
+ if (ev.type === "decision_recorded") {
4333
+ decisions.push({
4334
+ decisionId: ev.decision_id,
4335
+ title: ev.title,
4336
+ occurredAt: ev.occurred_at
4337
+ });
4338
+ }
4339
+ if (counted && ev.type === "note_added" && ev.kind === "next_step") {
4340
+ if (latestNote === null || Date.parse(ev.occurred_at) > Date.parse(latestNote.occurredAt)) {
4341
+ latestNote = { body: ev.body, sessionId: entry.sessionId, occurredAt: ev.occurred_at };
4342
+ }
4343
+ }
4344
+ if (counted) noteActivity(ev.occurred_at);
4133
4345
  }
4134
4346
  } catch {
4135
- eventsUnreadable = true;
4136
- if (!unreadableEmitted.has(entry.sessionId)) {
4137
- wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4138
- }
4347
+ input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
4139
4348
  }
4140
- sessions.push(
4141
- sessionWorkStatsFromEvents(
4142
- entry.sessionId,
4143
- entry.session.session,
4144
- events,
4145
- now,
4146
- eventsUnreadable
4147
- )
4148
- );
4149
4349
  }
4150
- const allIntervals = [];
4151
- for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
4152
- const union = unionDurationMs(allIntervals);
4350
+ decisions.sort((a, b) => {
4351
+ const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
4352
+ return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
4353
+ });
4354
+ const latestDecision = decisions[decisions.length - 1];
4355
+ const taskLoadOpts = {};
4356
+ if (input.onTaskSkip !== void 0) taskLoadOpts.onSkip = input.onTaskSkip;
4357
+ const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
4358
+ const inFlightTasks = taskEntries.filter((t) => t.task.task.status === "in_progress" || t.task.task.status === "planned").map((t) => ({
4359
+ id: t.task.task.id,
4360
+ title: t.task.task.title,
4361
+ status: t.task.task.status,
4362
+ linkedSessions: t.task.task.linked_sessions?.length ?? 0
4363
+ }));
4364
+ const plannedTasks = taskEntries.filter((t) => t.task.task.status === "planned").map((t) => ({ id: t.task.task.id, title: t.task.task.title }));
4365
+ const { pending: pendingIds } = await enumerateApprovals(input.paths);
4366
+ const pendingApprovals = [];
4367
+ for (const id of [...pendingIds].sort()) {
4368
+ const loaded = await loadApproval(input.paths, id);
4369
+ if (loaded === null) continue;
4370
+ const a = loaded.approval;
4371
+ pendingApprovals.push({
4372
+ id,
4373
+ risk: a.risk_level,
4374
+ kind: a.action.kind,
4375
+ reason: a.reason,
4376
+ sessionId: a.session_id,
4377
+ createdAt: a.created_at,
4378
+ expired: isLazyExpired(a, now)
4379
+ });
4380
+ }
4381
+ const suspects = entries.filter((e) => e.suspect).map((e) => ({
4382
+ sessionId: e.sessionId,
4383
+ status: e.session.session.status,
4384
+ reason: e.suspectReason
4385
+ }));
4386
+ const liveEntries = entries.filter(
4387
+ (e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
4388
+ );
4389
+ const latestEntry = [...liveEntries].sort(
4390
+ (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4391
+ )[0];
4392
+ const latestSession = latestEntry !== void 0 ? {
4393
+ sessionId: latestEntry.sessionId,
4394
+ label: latestEntry.session.session.label ?? null,
4395
+ status: latestEntry.session.session.status
4396
+ } : null;
4397
+ const activityEntries = entries.filter((e) => e.session.session.status !== "archived");
4398
+ const newest = [...activityEntries].sort(
4399
+ (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
4400
+ )[0];
4401
+ const bySourceMap = /* @__PURE__ */ new Map();
4402
+ for (const e of entries) {
4403
+ const k = e.session.session.source.kind;
4404
+ bySourceMap.set(k, (bySourceMap.get(k) ?? 0) + 1);
4405
+ }
4406
+ const bySource = [...bySourceMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([kind, count]) => ({ kind, count }));
4407
+ let sourceRoots = null;
4408
+ try {
4409
+ const manifest = await readManifest(input.paths);
4410
+ sourceRoots = manifest.import?.source_roots ?? null;
4411
+ } catch {
4412
+ sourceRoots = null;
4413
+ }
4414
+ const latestFiles = latestEntry?.session.session.related_files ?? [];
4415
+ const uniqueFiles = new Set(latestFiles);
4416
+ const displayed = [...uniqueFiles].sort().slice(0, limit);
4417
+ const overflow = Math.max(0, uniqueFiles.size - limit);
4153
4418
  return {
4154
- generatedAt: now.toISOString(),
4155
- activeGapCapMs: ACTIVE_GAP_CAP_MS,
4156
- timeZone,
4157
- totals: computeTotals(sessions, union.ms),
4158
- sessions,
4159
- bySource: computeBySource(sessions),
4160
- byStatus: computeByStatus(sessions),
4161
- byDay: computeByDay(sessions, union.merged, timeZone)
4419
+ generatedAt: input.nowIso,
4420
+ sessionCount: entries.length,
4421
+ latestSession,
4422
+ latestDecision: latestDecision ?? null,
4423
+ decisionCount: decisions.length,
4424
+ latestNote,
4425
+ relatedFiles: { displayed, overflow },
4426
+ inFlightTasks,
4427
+ plannedTasks,
4428
+ pendingApprovals,
4429
+ suspects,
4430
+ freshness: {
4431
+ newestStartedAt: newest?.session.session.started_at ?? null,
4432
+ newestSource: newest?.session.session.source.kind ?? null,
4433
+ latestActivityAt,
4434
+ bySource,
4435
+ sourceRoots
4436
+ }
4162
4437
  };
4163
4438
  }
4164
- function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
4165
- let commandCount = 0;
4166
- let fileChangedCount = 0;
4167
- let decisionCount = 0;
4168
- let commandTimeMs = 0;
4169
- const timestamps = [];
4170
- for (const ev of events) {
4171
- const t = Date.parse(ev.occurred_at);
4172
- if (Number.isFinite(t)) timestamps.push(t);
4173
- if (ev.type === "command_executed") {
4174
- commandCount++;
4175
- commandTimeMs += ev.duration_ms;
4176
- } else if (ev.type === "file_changed") {
4177
- fileChangedCount++;
4178
- } else if (ev.type === "decision_recorded") {
4179
- decisionCount++;
4180
- }
4181
- }
4439
+ async function renderOrientation(input) {
4440
+ const summary = await summarizeOrientation(input);
4441
+ return {
4442
+ body: formatOrientationBody(summary, {
4443
+ staleness: input.staleness ?? null,
4444
+ verbose: input.verbose === true
4445
+ }),
4446
+ sessionCount: summary.sessionCount,
4447
+ pendingApprovalsCount: summary.pendingApprovals.length,
4448
+ suspectCount: summary.suspects.length,
4449
+ inFlightTaskCount: summary.inFlightTasks.length,
4450
+ decisionCount: summary.decisionCount
4451
+ };
4452
+ }
4453
+ function formatOrientationBody(summary, opts) {
4454
+ const lines = [];
4455
+ const now = new Date(summary.generatedAt);
4456
+ const newestRel = relativeAge(summary.freshness.newestStartedAt ?? void 0, now);
4457
+ lines.push("# Orientation");
4458
+ lines.push("");
4459
+ lines.push(
4460
+ `> Generated at ${summary.generatedAt} \xB7 sessions ${summary.sessionCount} \xB7 newest ${newestRel} \xB7 pending ${summary.pendingApprovals.length} \xB7 suspect ${summary.suspects.length}`
4461
+ );
4462
+ lines.push("");
4463
+ lines.push("## \u4ECA\u3069\u3053\u306B\u3044\u308B");
4464
+ lines.push("");
4465
+ if (summary.latestSession !== null) {
4466
+ const s = summary.latestSession;
4467
+ const sid = shortId(s.sessionId);
4468
+ if (s.label !== null && s.label !== "") {
4469
+ lines.push(`- \u6700\u7D42 session: ${s.label} (${s.status}) [${sid}]`);
4470
+ } else {
4471
+ lines.push(`- \u6700\u7D42 session: ${sid} (${s.status})`);
4472
+ }
4473
+ } else {
4474
+ lines.push("- \u6700\u7D42 session: (no live sessions)");
4475
+ }
4476
+ if (summary.latestDecision !== null) {
4477
+ const decAge = relativeAgeJa(summary.latestDecision.occurredAt, now);
4478
+ lines.push(
4479
+ `- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}] (${decAge})`
4480
+ );
4481
+ const activityAt = summary.freshness.latestActivityAt;
4482
+ if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestDecision.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
4483
+ lines.push(
4484
+ ` - \u6CE8: \u3053\u308C\u306F\u6700\u5F8C\u306B\u300C\u8A18\u9332\u3055\u308C\u305F\u300D\u5224\u65AD\u3067\u3059\u3002\u6700\u7D42\u6D3B\u52D5 (${relativeAgeJa(activityAt, now)}) \u306F\u3053\u308C\u3088\u308A\u5F8C\u306E\u305F\u3081\u3001\u73FE\u5728\u306E\u65B9\u91DD\u304C\u53CD\u6620\u3055\u308C\u3066\u3044\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059(\u4F1A\u8A71\u3067\u306E\u610F\u601D\u6C7A\u5B9A\u306F\u81EA\u52D5\u8A18\u9332\u3055\u308C\u307E\u305B\u3093)\u3002`
4485
+ );
4486
+ }
4487
+ if (summary.decisionCount > 1) {
4488
+ lines.push(` - ${summary.decisionCount} decisions total \u2014 see decisions.md`);
4489
+ }
4490
+ } else {
4491
+ lines.push("- \u76F4\u8FD1\u306E\u5224\u65AD: (no decisions recorded yet)");
4492
+ }
4493
+ if (summary.relatedFiles.displayed.length > 0) {
4494
+ const shown = summary.relatedFiles.displayed.join(", ");
4495
+ const more = summary.relatedFiles.overflow > 0 ? ` (... +${summary.relatedFiles.overflow} more)` : "";
4496
+ lines.push(`- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: ${shown}${more}`);
4497
+ } else {
4498
+ lines.push("- \u76F4\u8FD1\u306E\u5909\u66F4\u30D5\u30A1\u30A4\u30EB: (none recorded)");
4499
+ }
4500
+ lines.push("");
4501
+ lines.push("## \u4F55\u304C\u52D5\u304F");
4502
+ lines.push("");
4503
+ lines.push(`### \u9032\u884C\u4E2D task (${summary.inFlightTasks.length})`);
4504
+ if (summary.inFlightTasks.length === 0) {
4505
+ lines.push("- (none)");
4506
+ } else {
4507
+ for (const t of summary.inFlightTasks) {
4508
+ const linkedSuffix = t.linkedSessions > 1 ? ` \u2014 linked_sessions: ${t.linkedSessions}` : "";
4509
+ lines.push(`- ${t.title} (${t.status}) [${shortId(t.id)}]${linkedSuffix}`);
4510
+ }
4511
+ }
4512
+ lines.push("");
4513
+ lines.push(`### \u627F\u8A8D\u5F85\u3061 (${summary.pendingApprovals.length})`);
4514
+ if (summary.pendingApprovals.length === 0) {
4515
+ lines.push("- (none)");
4516
+ } else {
4517
+ for (const a of summary.pendingApprovals) {
4518
+ const expired = a.expired ? " (expired)" : "";
4519
+ lines.push(
4520
+ `- [${a.risk}] ${a.kind}: ${a.reason} \u2014 session ${shortId(a.sessionId)}, since ${a.createdAt}${expired}`
4521
+ );
4522
+ }
4523
+ }
4524
+ lines.push("");
4525
+ lines.push(`### \u8981\u6CE8\u610F session (${summary.suspects.length})`);
4526
+ if (summary.suspects.length === 0) {
4527
+ lines.push("- (none)");
4528
+ } else {
4529
+ for (const e of summary.suspects) {
4530
+ lines.push(`- ${shortId(e.sessionId)} (${e.status}) \u2014 ${suspectText(e.reason)}`);
4531
+ }
4532
+ }
4533
+ lines.push("");
4534
+ lines.push("## \u3069\u3053\u3078\u5411\u304B\u3046");
4535
+ lines.push("");
4536
+ if (summary.latestNote !== null) {
4537
+ const noteAge = relativeAgeJa(summary.latestNote.occurredAt, now);
4538
+ lines.push(
4539
+ `- \u6B21\u306E\u8D77\u70B9 (\u8A18\u9332\u6E08\u307F, ${noteAge}): ${noteSummary(summary.latestNote.body)} [session ${shortId(summary.latestNote.sessionId)}]`
4540
+ );
4541
+ const activityAt = summary.freshness.latestActivityAt;
4542
+ if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestNote.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
4543
+ lines.push(
4544
+ ` - \u6CE8: \u3053\u306E\u8D77\u70B9\u306E\u8A18\u9332\u5F8C (\u6700\u7D42\u6D3B\u52D5 ${relativeAgeJa(activityAt, now)}) \u3082\u4F5C\u696D\u304C\u7D9A\u3044\u3066\u3044\u307E\u3059\u3002\u518D\u958B\u70B9\u304C\u53E4\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`
4545
+ );
4546
+ }
4547
+ }
4548
+ for (const t of summary.plannedTasks) {
4549
+ lines.push(`- ${t.title} [${shortId(t.id)}]`);
4550
+ }
4551
+ if (summary.latestNote === null && summary.plannedTasks.length === 0) {
4552
+ lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
4553
+ if (summary.latestDecision !== null) {
4554
+ lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title}`);
4555
+ }
4556
+ }
4557
+ lines.push("");
4558
+ lines.push("## \u3053\u308C\u306F\u6700\u65B0\u304B");
4559
+ lines.push("");
4560
+ for (const line of freshnessVerdict(summary, opts.staleness, now)) lines.push(line);
4561
+ if (opts.verbose) {
4562
+ lines.push("");
4563
+ lines.push("<!-- verbose: raw freshness telemetry -->");
4564
+ if (summary.freshness.newestStartedAt !== null) {
4565
+ lines.push(`- newest captured session: ${summary.freshness.newestStartedAt} (${newestRel})`);
4566
+ } else {
4567
+ lines.push("- newest captured session: (no sessions captured yet)");
4568
+ }
4569
+ if (summary.freshness.latestActivityAt !== null) {
4570
+ lines.push(
4571
+ `- latest activity: ${summary.freshness.latestActivityAt} (${relativeAge(summary.freshness.latestActivityAt, now)})`
4572
+ );
4573
+ }
4574
+ const sourceBreakdown = summary.freshness.bySource.map(({ kind, count }) => `${kind} ${count}`).join(", ");
4575
+ lines.push(
4576
+ `- sessions: ${summary.sessionCount}${sourceBreakdown !== "" ? ` (${sourceBreakdown})` : ""}`
4577
+ );
4578
+ if (summary.freshness.sourceRoots !== null && summary.freshness.sourceRoots.length > 0) {
4579
+ lines.push(`- source roots: ${summary.freshness.sourceRoots.join(", ")}`);
4580
+ } else {
4581
+ lines.push("- source roots: (single root)");
4582
+ }
4583
+ lines.push(`- suspect sessions: ${summary.suspects.length}`);
4584
+ const probe = opts.staleness === null ? "not run" : `new ${opts.staleness.newSessions}, updated ${opts.staleness.updatedSessions}, unverifiable ${opts.staleness.unverifiableSessions ?? 0}`;
4585
+ lines.push(`- staleness probe: ${probe}`);
4586
+ }
4587
+ return lines.join("\n");
4588
+ }
4589
+ function toolDisplayName(kind) {
4590
+ switch (kind) {
4591
+ case "claude-code-import":
4592
+ case "claude-code-adapter":
4593
+ return "Claude Code";
4594
+ case "codex-import":
4595
+ return "Codex";
4596
+ case "terminal":
4597
+ return "\u30BF\u30FC\u30DF\u30CA\u30EB";
4598
+ case "human":
4599
+ return "\u624B\u52D5\u30E1\u30E2";
4600
+ case "import":
4601
+ return "\u4ED6\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9";
4602
+ default:
4603
+ return kind ?? "\u4E0D\u660E";
4604
+ }
4605
+ }
4606
+ function freshnessVerdict(summary, staleness, now) {
4607
+ if (staleness !== null && (staleness.unverifiableSessions ?? 0) > 0) {
4608
+ return [
4609
+ `\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`,
4610
+ "`basou verify` \u3067\u78BA\u8A8D\u3057\u3001`basou refresh --force` \u3067\u518D\u53D6\u308A\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4611
+ ];
4612
+ }
4613
+ if (staleness !== null && (staleness.newSessions > 0 || staleness.updatedSessions > 0)) {
4614
+ const parts = [];
4615
+ if (staleness.newSessions > 0) parts.push(`\u65B0\u898F ${staleness.newSessions} \u4EF6`);
4616
+ if (staleness.updatedSessions > 0) parts.push(`\u66F4\u65B0 ${staleness.updatedSessions} \u4EF6`);
4617
+ return [
4618
+ `\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`,
4619
+ "`basou refresh` \u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4620
+ ];
4621
+ }
4622
+ if (summary.freshness.newestStartedAt === null) {
4623
+ return [
4624
+ "\u2139\uFE0F \u307E\u3060\u8A18\u9332\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
4625
+ "\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"
4626
+ ];
4627
+ }
4628
+ const rel = relativeAgeJa(summary.freshness.newestStartedAt, now);
4629
+ const tool = toolDisplayName(summary.freshness.newestSource);
4630
+ const suspectCount = summary.suspects.length;
4631
+ if (staleness === null) {
4632
+ return [
4633
+ `\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`,
4634
+ "\u6700\u65B0\u304B\u78BA\u8A8D\u3059\u308B\u306B\u306F `basou refresh` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4635
+ ];
4636
+ }
4637
+ const lines = [
4638
+ `\u2705 \u53D6\u308A\u8FBC\u307F\u306F\u6700\u65B0\u3067\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002\u672A\u53D6\u308A\u8FBC\u307F\u306E native \u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093\u3002`
4639
+ ];
4640
+ if (suspectCount > 0) {
4641
+ lines.push(`\u305F\u3060\u3057\u8981\u6CE8\u610F\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${suspectCount} \u4EF6\u3042\u308A\u307E\u3059(\u4E0A\u8A18\u300C\u8981\u6CE8\u610F session\u300D\u53C2\u7167)\u3002`);
4642
+ }
4643
+ lines.push(
4644
+ "\u6CE8: \u3053\u306E\u5224\u5B9A\u306F\u53D6\u308A\u8FBC\u307F\u6E08\u307F native \u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u9BAE\u5EA6\u3068 suspect \u306E\u6709\u7121\u3060\u3051\u3092\u898B\u307E\u3059\u3002\u8A08\u753B\u2194\u5B9F\u88C5\u306E\u30C9\u30EA\u30D5\u30C8\u3084\u672A\u8A18\u9332\u306E\u610F\u601D\u6C7A\u5B9A\u307E\u3067\u306F\u691C\u77E5\u3057\u307E\u305B\u3093\u3002"
4645
+ );
4646
+ return lines;
4647
+ }
4648
+ function relativeAgeJa(startedAt, now) {
4649
+ if (startedAt === null) return "(\u4E0D\u660E)";
4650
+ const ms = now.getTime() - Date.parse(startedAt);
4651
+ if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
4652
+ if (ms < 6e4) return "\u305F\u3063\u305F\u4ECA";
4653
+ const totalMin = Math.floor(ms / 6e4);
4654
+ const days = Math.floor(totalMin / 1440);
4655
+ const hours = Math.floor(totalMin % 1440 / 60);
4656
+ const mins = totalMin % 60;
4657
+ if (days > 0) return hours > 0 ? `${days}\u65E5${hours}\u6642\u9593\u524D` : `${days}\u65E5\u524D`;
4658
+ if (hours > 0) return mins > 0 ? `${hours}\u6642\u9593${mins}\u5206\u524D` : `${hours}\u6642\u9593\u524D`;
4659
+ return `${mins}\u5206\u524D`;
4660
+ }
4661
+ function relativeAge(startedAt, now) {
4662
+ if (startedAt === void 0) return "(unknown)";
4663
+ const ms = now.getTime() - Date.parse(startedAt);
4664
+ if (!Number.isFinite(ms)) return "(unknown)";
4665
+ if (ms < 0) return "just now";
4666
+ if (ms < 1e3) return "just now";
4667
+ return `${formatDurationMs(ms)} ago`;
4668
+ }
4669
+ var NOTE_SUMMARY_MAX = 200;
4670
+ function noteSummary(body) {
4671
+ const oneLine = body.replace(/\s+/g, " ").trim();
4672
+ return oneLine.length > NOTE_SUMMARY_MAX ? `${oneLine.slice(0, NOTE_SUMMARY_MAX - 1)}\u2026` : oneLine;
4673
+ }
4674
+ function suspectText(reason) {
4675
+ if (reason === "events_say_ended_but_yaml_running") return "ended (yaml stale)";
4676
+ if (reason === "running_no_end_event") return "no end event";
4677
+ return "suspect";
4678
+ }
4679
+ function shortId(id) {
4680
+ const sep = id.indexOf("_");
4681
+ if (sep === -1) return id.slice(0, 10);
4682
+ return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
4683
+ }
4684
+
4685
+ // src/project/relative-path.ts
4686
+ function normalizeRelativePath(p) {
4687
+ const trimmed = p.trim();
4688
+ const absolute = trimmed.startsWith("/");
4689
+ const out = [];
4690
+ for (const seg of trimmed.split("/")) {
4691
+ if (seg === "" || seg === ".") continue;
4692
+ if (seg === "..") {
4693
+ const top = out[out.length - 1];
4694
+ if (top !== void 0 && top !== "..") {
4695
+ out.pop();
4696
+ } else if (!absolute) {
4697
+ out.push("..");
4698
+ }
4699
+ continue;
4700
+ }
4701
+ out.push(seg);
4702
+ }
4703
+ const joined = out.join("/");
4704
+ if (absolute) return `/${joined}`;
4705
+ return joined.length === 0 ? "." : joined;
4706
+ }
4707
+
4708
+ // src/project/archive.ts
4709
+ function planArchive(input) {
4710
+ const target = normalizeRelativePath(input.target);
4711
+ const repos = input.repos ?? [];
4712
+ const isAnchor = input.targetIsAnchor === true || target === ".";
4713
+ const matched = repos.filter((r) => normalizeRelativePath(r.path) === target);
4714
+ const found = matched.length > 0;
4715
+ if (isAnchor || !found) {
4716
+ return {
4717
+ target,
4718
+ found,
4719
+ isAnchor,
4720
+ nextRepos: repos,
4721
+ reposEmptied: false,
4722
+ remainingCount: repos.length,
4723
+ becomesSolo: false
4724
+ };
4725
+ }
4726
+ const nextRepos = repos.filter((r) => normalizeRelativePath(r.path) !== target);
4727
+ const remainingCount = nextRepos.length;
4728
+ let sourceRootRemoval;
4729
+ let nextSourceRoots;
4730
+ if (input.sourceRoots !== void 0) {
4731
+ const pruned = input.sourceRoots.filter((s) => normalizeRelativePath(s) !== target);
4732
+ if (pruned.length !== input.sourceRoots.length) {
4733
+ sourceRootRemoval = target;
4734
+ nextSourceRoots = pruned;
4735
+ }
4736
+ }
4737
+ return {
4738
+ target,
4739
+ found: true,
4740
+ isAnchor: false,
4741
+ rosterEntry: matched[matched.length - 1],
4742
+ nextRepos,
4743
+ reposEmptied: remainingCount === 0,
4744
+ ...sourceRootRemoval !== void 0 ? { sourceRootRemoval } : {},
4745
+ ...nextSourceRoots !== void 0 ? { nextSourceRoots } : {},
4746
+ remainingCount,
4747
+ becomesSolo: remainingCount === 1
4748
+ };
4749
+ }
4750
+
4751
+ // src/project/gitignore-plan.ts
4752
+ function isPublicFacing(v) {
4753
+ return v === "public" || v === "future-public";
4754
+ }
4755
+ function planGitignore(input) {
4756
+ const plans = [];
4757
+ const unknown = [];
4758
+ const unreachable = [];
4759
+ for (const repo of input.repos) {
4760
+ if (!repo.reachable) {
4761
+ unreachable.push(repo.path);
4762
+ continue;
4763
+ }
4764
+ if (repo.visibility === void 0) {
4765
+ unknown.push(repo.path);
4766
+ continue;
4767
+ }
4768
+ if (!isPublicFacing(repo.visibility)) continue;
4769
+ const present = /* @__PURE__ */ new Set();
4770
+ for (const line of repo.currentLines) {
4771
+ const trimmed = line.trim();
4772
+ present.add(trimmed);
4773
+ if (trimmed.startsWith("/")) present.add(trimmed.slice(1));
4774
+ }
4775
+ const toAdd = input.required.filter((p) => !present.has(p));
4776
+ if (toAdd.length > 0) plans.push({ path: repo.path, toAdd });
4777
+ }
4778
+ return {
4779
+ plans,
4780
+ unknown,
4781
+ unreachable,
4782
+ ok: plans.length === 0 && unknown.length === 0 && unreachable.length === 0
4783
+ };
4784
+ }
4785
+
4786
+ // src/project/preset.ts
4787
+ function visibilityLabel(v) {
4788
+ switch (v) {
4789
+ case "public":
4790
+ return "public(git \u5C65\u6B74\u306F\u516C\u958B)";
4791
+ case "private":
4792
+ return "private(git \u5C65\u6B74\u306F\u975E\u516C\u958B)";
4793
+ case "future-public":
4794
+ return "future-public(\u73FE\u5728\u306F\u975E\u516C\u958B\u30FB\u5C06\u6765\u516C\u958B\u4E88\u5B9A)";
4795
+ default:
4796
+ return "\u672A\u8A2D\u5B9A";
4797
+ }
4798
+ }
4799
+ function sourceLanguageLabel(l) {
4800
+ switch (l) {
4801
+ case "en":
4802
+ return "en(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u82F1\u8A9E)";
4803
+ case "ja":
4804
+ return "ja(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u65E5\u672C\u8A9E)";
4805
+ case "en+ja":
4806
+ return "en+ja(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u65E5\u82F1)";
4807
+ default:
4808
+ return "\u672A\u8A2D\u5B9A";
4809
+ }
4810
+ }
4811
+ function publishKindLabel(k) {
4812
+ return k === "web" ? "web(\u30C7\u30D7\u30ED\u30A4)" : "npm(\u30D1\u30C3\u30B1\u30FC\u30B8)";
4813
+ }
4814
+ function publishVisibilityLabel(v) {
4815
+ switch (v) {
4816
+ case "public":
4817
+ return "\u516C\u958B";
4818
+ case "private":
4819
+ return "\u975E\u516C\u958B";
4820
+ case "future-public":
4821
+ return "\u5C06\u6765\u516C\u958B";
4822
+ default:
4823
+ return "\u53EF\u8996\u6027\u672A\u8A2D\u5B9A";
4824
+ }
4825
+ }
4826
+ function contentLanguageLabel(l) {
4827
+ return l ?? "\u8A00\u8A9E\u672A\u8A2D\u5B9A";
4828
+ }
4829
+ function isRenderable(repo) {
4830
+ return repo.visibility !== void 0 || repo.language !== void 0 || repo.publishes !== void 0 && repo.publishes.length > 0;
4831
+ }
4832
+ function renderPresetBlock(repo) {
4833
+ const lines = [];
4834
+ lines.push("## \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210(basou \u304C\u751F\u6210 \u2014 manifest \u304C\u6B63\u672C)");
4835
+ lines.push("");
4836
+ lines.push(
4837
+ "\u3053\u306E\u30BB\u30AF\u30B7\u30E7\u30F3\u306F `.basou/manifest.yaml` \u306E\u5BA3\u8A00\u304B\u3089 `basou project preset` \u304C\u751F\u6210\u3057\u307E\u3059\u3002\u7DE8\u96C6\u306F manifest \u5074\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044(\u30DE\u30FC\u30AB\u30FC\u5916\u306E\u8A18\u8FF0\u306F\u4FDD\u6301\u3055\u308C\u307E\u3059)\u3002"
4838
+ );
4839
+ lines.push("");
4840
+ lines.push(`- \u30BD\u30FC\u30B9\u53EF\u8996\u6027: ${visibilityLabel(repo.visibility)}`);
4841
+ lines.push(`- \u30BD\u30FC\u30B9\u8A00\u8A9E: ${sourceLanguageLabel(repo.language)}`);
4842
+ const publishes = repo.publishes ?? [];
4843
+ if (publishes.length === 0) {
4844
+ lines.push("- \u914D\u4FE1\u7269: \u306A\u3057");
4845
+ } else {
4846
+ lines.push("- \u914D\u4FE1\u7269:");
4847
+ for (const p of publishes) {
4848
+ lines.push(
4849
+ ` - ${publishKindLabel(p.kind)} \u2014 ${publishVisibilityLabel(p.visibility)} / ${contentLanguageLabel(p.language)}`
4850
+ );
4851
+ }
4852
+ }
4853
+ return lines.join("\n");
4854
+ }
4855
+ function normalizeBlock(s) {
4856
+ return s.replace(/\r\n/g, "\n").replace(/\n+$/, "");
4857
+ }
4858
+ function summarizePresetPlan(facts) {
4859
+ const deduped = [];
4860
+ const seenPath = /* @__PURE__ */ new Set();
4861
+ for (const f of facts) {
4862
+ const key = normalizeRelativePath(f.path);
4863
+ if (seenPath.has(key)) continue;
4864
+ seenPath.add(key);
4865
+ deduped.push(f);
4866
+ }
4867
+ const byCanonical = /* @__PURE__ */ new Map();
4868
+ for (const f of deduped) {
4869
+ if (f.isAnchor || !f.reachable || f.canonicalName === void 0 || !isRenderable(f)) continue;
4870
+ const repos = byCanonical.get(f.canonicalName) ?? [];
4871
+ repos.push(f.path);
4872
+ byCanonical.set(f.canonicalName, repos);
4873
+ }
4874
+ const collisions = [];
4875
+ const collidingPaths = /* @__PURE__ */ new Set();
4876
+ for (const [canonicalName, repos] of byCanonical) {
4877
+ if (repos.length > 1) {
4878
+ collisions.push({ canonicalName, repos });
4879
+ for (const r of repos) collidingPaths.add(r);
4880
+ }
4881
+ }
4882
+ const plans = [];
4883
+ const inSync = [];
4884
+ const undeclared = [];
4885
+ const markerConflicts = [];
4886
+ const unreadable = [];
4887
+ const anchors = [];
4888
+ const unreachable = [];
4889
+ for (const f of deduped) {
4890
+ if (f.isAnchor) {
4891
+ anchors.push(f.path);
4892
+ continue;
4893
+ }
4894
+ if (!f.reachable) {
4895
+ unreachable.push(f.path);
4896
+ continue;
4897
+ }
4898
+ if (!isRenderable(f)) {
4899
+ undeclared.push(f.path);
4900
+ continue;
4901
+ }
4902
+ if (collidingPaths.has(f.path)) continue;
4903
+ if (f.canonicalName === void 0) {
4904
+ unreachable.push(f.path);
4905
+ continue;
4906
+ }
4907
+ const desiredBlock = renderPresetBlock(f);
4908
+ if (!f.canonicalPresent) {
4909
+ plans.push({ path: f.path, canonicalName: f.canonicalName, action: "create", desiredBlock });
4910
+ continue;
4911
+ }
4912
+ if (f.canonicalReadable === false) {
4913
+ unreadable.push(f.path);
4914
+ continue;
4915
+ }
4916
+ if (f.markerKind === "ok") {
4917
+ if (normalizeBlock(f.currentBlock ?? "") === normalizeBlock(desiredBlock)) {
4918
+ inSync.push(f.path);
4919
+ } else {
4920
+ plans.push({
4921
+ path: f.path,
4922
+ canonicalName: f.canonicalName,
4923
+ action: "update",
4924
+ desiredBlock
4925
+ });
4926
+ }
4927
+ continue;
4928
+ }
4929
+ markerConflicts.push({ repo: f.path, reason: f.markerKind ?? "no_markers" });
4930
+ }
4931
+ return {
4932
+ plans,
4933
+ inSync,
4934
+ undeclared,
4935
+ markerConflicts,
4936
+ unreadable,
4937
+ collisions,
4938
+ anchors,
4939
+ unreachable,
4940
+ ok: plans.length === 0 && markerConflicts.length === 0 && unreadable.length === 0 && collisions.length === 0 && unreachable.length === 0 && undeclared.length === 0
4941
+ };
4942
+ }
4943
+
4944
+ // src/project/rename.ts
4945
+ function pathBasename(p) {
4946
+ const parts = normalizeRelativePath(p).split("/");
4947
+ return parts[parts.length - 1];
4948
+ }
4949
+ function dedupRepos(entries) {
4950
+ const seen = /* @__PURE__ */ new Set();
4951
+ const out = [];
4952
+ for (const e of entries) {
4953
+ const k = normalizeRelativePath(e.path);
4954
+ if (seen.has(k)) continue;
4955
+ seen.add(k);
4956
+ out.push(e);
4957
+ }
4958
+ return out;
4959
+ }
4960
+ function dedupNorm(items) {
4961
+ const seen = /* @__PURE__ */ new Set();
4962
+ const out = [];
4963
+ for (const s of items) {
4964
+ const k = normalizeRelativePath(s);
4965
+ if (seen.has(k)) continue;
4966
+ seen.add(k);
4967
+ out.push(s);
4968
+ }
4969
+ return out;
4970
+ }
4971
+ function planRename(input) {
4972
+ const oldTarget = normalizeRelativePath(input.oldPath);
4973
+ const newTarget = normalizeRelativePath(input.newPath);
4974
+ const repos = input.repos ?? [];
4975
+ const basenameChanged = pathBasename(oldTarget) !== pathBasename(newTarget);
4976
+ const noop = oldTarget === newTarget;
4977
+ const isAnchor = input.oldIsAnchor === true || oldTarget === ".";
4978
+ const found = repos.some((r) => normalizeRelativePath(r.path) === oldTarget);
4979
+ const collision = !noop && repos.some((r) => normalizeRelativePath(r.path) === newTarget);
4980
+ if (noop || isAnchor || !found || collision) {
4981
+ return {
4982
+ oldTarget,
4983
+ newTarget,
4984
+ noop,
4985
+ isAnchor,
4986
+ found,
4987
+ collision,
4988
+ nextRepos: repos,
4989
+ reposChanged: false,
4990
+ basenameChanged
4991
+ };
4992
+ }
4993
+ const rosterEntry = repos.find((r) => normalizeRelativePath(r.path) === oldTarget);
4994
+ const nextRepos = dedupRepos(
4995
+ repos.map((r) => normalizeRelativePath(r.path) === oldTarget ? { ...r, path: newTarget } : r)
4996
+ );
4997
+ let sourceRootRenamed;
4998
+ let nextSourceRoots;
4999
+ if (input.sourceRoots !== void 0 && input.sourceRoots.some((s) => normalizeRelativePath(s) === oldTarget)) {
5000
+ nextSourceRoots = dedupNorm(
5001
+ input.sourceRoots.map((s) => normalizeRelativePath(s) === oldTarget ? newTarget : s)
5002
+ );
5003
+ sourceRootRenamed = oldTarget;
5004
+ }
5005
+ return {
5006
+ oldTarget,
5007
+ newTarget,
5008
+ noop: false,
5009
+ isAnchor: false,
5010
+ found: true,
5011
+ collision: false,
5012
+ rosterEntry,
5013
+ nextRepos,
5014
+ reposChanged: true,
5015
+ ...sourceRootRenamed !== void 0 ? { sourceRootRenamed } : {},
5016
+ ...nextSourceRoots !== void 0 ? { nextSourceRoots } : {},
5017
+ basenameChanged
5018
+ };
5019
+ }
5020
+
5021
+ // src/project/roster.ts
5022
+ function summarizeRosterDrift(input) {
5023
+ const captured = new Set((input.sourceRoots ?? []).map(normalizeRelativePath));
5024
+ const declared = /* @__PURE__ */ new Map();
5025
+ for (const r of input.repos ?? []) declared.set(normalizeRelativePath(r.path), r);
5026
+ const gaps = [];
5027
+ const matched = [];
5028
+ for (const [norm, entry] of declared) {
5029
+ if (captured.has(norm)) matched.push(norm);
5030
+ else gaps.push(entry);
5031
+ }
5032
+ const extra = [...captured].filter((c) => !declared.has(c)).sort();
5033
+ return {
5034
+ declaredCount: declared.size,
5035
+ capturedCount: captured.size,
5036
+ gaps,
5037
+ extra,
5038
+ matched: matched.sort(),
5039
+ ok: gaps.length === 0
5040
+ };
5041
+ }
5042
+ function reconcileSourceRoots(input) {
5043
+ const current = input.sourceRoots ?? [];
5044
+ const seen = new Set(current.map(normalizeRelativePath));
5045
+ const added = [];
5046
+ for (const r of input.repos ?? []) {
5047
+ const norm = normalizeRelativePath(r.path);
5048
+ if (seen.has(norm)) continue;
5049
+ seen.add(norm);
5050
+ added.push(norm);
5051
+ }
5052
+ return {
5053
+ next: [...current, ...added],
5054
+ added,
5055
+ unchanged: added.length === 0
5056
+ };
5057
+ }
5058
+ function planRosterAdoption(candidates) {
5059
+ const repos = [];
5060
+ const excluded = [];
5061
+ const seen = /* @__PURE__ */ new Set();
5062
+ for (const c of candidates) {
5063
+ const norm = normalizeRelativePath(c.path);
5064
+ if (seen.has(norm)) continue;
5065
+ seen.add(norm);
5066
+ if (c.kind === "repo") repos.push({ path: c.path });
5067
+ else excluded.push({ path: c.path, kind: c.kind });
5068
+ }
5069
+ return { repos, excluded };
5070
+ }
5071
+
5072
+ // src/project/symlinks.ts
5073
+ function summarizeSymlinkPlan(facts) {
5074
+ const deduped = [];
5075
+ const seenPath = /* @__PURE__ */ new Set();
5076
+ for (const f of facts) {
5077
+ const key = normalizeRelativePath(f.path);
5078
+ if (seenPath.has(key)) continue;
5079
+ seenPath.add(key);
5080
+ deduped.push(f);
5081
+ }
5082
+ const byCanonical = /* @__PURE__ */ new Map();
5083
+ for (const f of deduped) {
5084
+ if (f.isAnchor || !f.reachable || !f.canonicalPresent || f.canonicalName === void 0) {
5085
+ continue;
5086
+ }
5087
+ const repos = byCanonical.get(f.canonicalName) ?? [];
5088
+ repos.push(f.path);
5089
+ byCanonical.set(f.canonicalName, repos);
5090
+ }
5091
+ const collisions = [];
5092
+ const collidingPaths = /* @__PURE__ */ new Set();
5093
+ for (const [canonicalName, repos] of byCanonical) {
5094
+ if (repos.length > 1) {
5095
+ collisions.push({ canonicalName, repos });
5096
+ for (const r of repos) collidingPaths.add(r);
5097
+ }
5098
+ }
5099
+ const plans = [];
5100
+ const conflicts = [];
5101
+ const missingCanonical = [];
5102
+ const unreachable = [];
5103
+ for (const f of deduped) {
5104
+ if (f.isAnchor) continue;
5105
+ if (!f.reachable) {
5106
+ unreachable.push(f.path);
5107
+ continue;
5108
+ }
5109
+ if (!f.canonicalPresent) {
5110
+ missingCanonical.push(f.path);
5111
+ continue;
5112
+ }
5113
+ if (collidingPaths.has(f.path)) continue;
5114
+ const toCreate = [];
5115
+ for (const file of f.files) {
5116
+ if (file.state === "missing") {
5117
+ toCreate.push({ name: file.name, target: file.expectedTarget });
5118
+ } else if (file.state === "mismatch") {
5119
+ conflicts.push({
5120
+ repo: f.path,
5121
+ file: file.name,
5122
+ reason: "mismatch",
5123
+ ...file.actualTarget !== void 0 ? { actualTarget: file.actualTarget } : {}
5124
+ });
5125
+ } else if (file.state === "occupied") {
5126
+ conflicts.push({ repo: f.path, file: file.name, reason: "occupied" });
5127
+ } else if (file.state === "blocked") {
5128
+ conflicts.push({ repo: f.path, file: file.name, reason: "blocked" });
5129
+ }
5130
+ }
5131
+ if (toCreate.length > 0) plans.push({ path: f.path, toCreate });
5132
+ }
5133
+ return {
5134
+ plans,
5135
+ conflicts,
5136
+ missingCanonical,
5137
+ unreachable,
5138
+ collisions,
5139
+ ok: plans.length === 0 && conflicts.length === 0 && missingCanonical.length === 0 && unreachable.length === 0 && collisions.length === 0
5140
+ };
5141
+ }
5142
+
5143
+ // src/project/wiring.ts
5144
+ function isPublicFacing2(v) {
5145
+ return v === "public" || v === "future-public";
5146
+ }
5147
+ function summarizeWiring(facts) {
5148
+ const risks = [];
5149
+ const unknown = [];
5150
+ const incomplete = [];
5151
+ const unreachable = [];
5152
+ for (const f of facts) {
5153
+ if (!f.reachable) {
5154
+ unreachable.push(f.path);
5155
+ continue;
5156
+ }
5157
+ if (isPublicFacing2(f.visibility)) {
5158
+ for (const file of f.instructionFiles) {
5159
+ if (file.tracked) risks.push({ repo: f.path, visibility: f.visibility, file: file.name });
5160
+ }
5161
+ } else if (f.visibility === void 0) {
5162
+ unknown.push(f.path);
5163
+ }
5164
+ const missing = f.instructionFiles.filter((file) => !file.present).map((file) => file.name);
5165
+ if (missing.length > 0) incomplete.push({ repo: f.path, missing });
5166
+ }
5167
+ return {
5168
+ repos: facts,
5169
+ risks,
5170
+ unknown,
5171
+ incomplete,
5172
+ unreachable,
5173
+ ok: risks.length === 0 && unknown.length === 0 && unreachable.length === 0
5174
+ };
5175
+ }
5176
+
5177
+ // src/project/workspace-view.ts
5178
+ function planWorkspaceView(facts, existing = [], rosterNames = []) {
5179
+ const deduped = [];
5180
+ const seenPath = /* @__PURE__ */ new Set();
5181
+ for (const f of facts) {
5182
+ const key = normalizeRelativePath(f.path);
5183
+ if (seenPath.has(key)) continue;
5184
+ seenPath.add(key);
5185
+ deduped.push(f);
5186
+ }
5187
+ const byLinkName = /* @__PURE__ */ new Map();
5188
+ for (const f of deduped) {
5189
+ if (!f.reachable || f.linkName === void 0) continue;
5190
+ const repos = byLinkName.get(f.linkName) ?? [];
5191
+ repos.push(f.path);
5192
+ byLinkName.set(f.linkName, repos);
5193
+ }
5194
+ const collisions = [];
5195
+ const collidingPaths = /* @__PURE__ */ new Set();
5196
+ for (const [linkName, repos] of byLinkName) {
5197
+ if (repos.length > 1) {
5198
+ collisions.push({ linkName, repos });
5199
+ for (const r of repos) collidingPaths.add(r);
5200
+ }
5201
+ }
5202
+ const toCreate = [];
5203
+ const conflicts = [];
5204
+ const unreachable = [];
5205
+ let correctCount = 0;
5206
+ for (const f of deduped) {
5207
+ if (!f.reachable) {
5208
+ unreachable.push(f.path);
5209
+ continue;
5210
+ }
5211
+ if (collidingPaths.has(f.path)) continue;
5212
+ if (f.linkName === void 0 || f.expectedTarget === void 0 || f.state === void 0) {
5213
+ continue;
5214
+ }
5215
+ if (f.state === "missing") {
5216
+ toCreate.push({ name: f.linkName, target: f.expectedTarget });
5217
+ } else if (f.state === "mismatch") {
5218
+ conflicts.push({
5219
+ name: f.linkName,
5220
+ reason: "mismatch",
5221
+ ...f.actualTarget !== void 0 ? { actualTarget: f.actualTarget } : {}
5222
+ });
5223
+ } else if (f.state === "occupied") {
5224
+ conflicts.push({ name: f.linkName, reason: "occupied" });
5225
+ } else if (f.state === "blocked") {
5226
+ conflicts.push({ name: f.linkName, reason: "blocked" });
5227
+ } else {
5228
+ correctCount += 1;
5229
+ }
5230
+ }
5231
+ const ownedNames = new Set(rosterNames);
5232
+ for (const f of deduped) {
5233
+ if (f.reachable && f.linkName !== void 0) ownedNames.add(f.linkName);
5234
+ }
5235
+ const toPrune = [];
5236
+ const strayUnknown = [];
5237
+ const seenExisting = /* @__PURE__ */ new Set();
5238
+ for (const e of existing) {
5239
+ if (ownedNames.has(e.name)) continue;
5240
+ if (seenExisting.has(e.name)) continue;
5241
+ seenExisting.add(e.name);
5242
+ if (e.kind === "repo") {
5243
+ toPrune.push({ name: e.name, target: e.target });
5244
+ } else {
5245
+ strayUnknown.push({ name: e.name, target: e.target, reason: e.kind });
5246
+ }
5247
+ }
5248
+ return {
5249
+ toCreate,
5250
+ conflicts,
5251
+ collisions,
5252
+ unreachable,
5253
+ toPrune,
5254
+ strayUnknown,
5255
+ correctCount,
5256
+ ok: toCreate.length === 0 && conflicts.length === 0 && collisions.length === 0 && unreachable.length === 0 && toPrune.length === 0 && strayUnknown.length === 0
5257
+ };
5258
+ }
5259
+
5260
+ // src/report/report-renderer.ts
5261
+ import { join as join16 } from "path";
5262
+
5263
+ // src/stats/work-stats.ts
5264
+ import { join as join15 } from "path";
5265
+ function resolveTimeZone(timeZone) {
5266
+ if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
5267
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
5268
+ }
5269
+ var STATUS_ORDER = [
5270
+ "completed",
5271
+ "failed",
5272
+ "running",
5273
+ "interrupted",
5274
+ "waiting_approval",
5275
+ "initialized",
5276
+ "imported",
5277
+ "archived"
5278
+ ];
5279
+ async function computeWorkStats(input) {
5280
+ const { now } = input;
5281
+ const timeZone = resolveTimeZone(input.timeZone);
5282
+ const unreadableEmitted = /* @__PURE__ */ new Set();
5283
+ const wrappedSkip = (sid, reason) => {
5284
+ if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
5285
+ input.onSessionSkip?.(sid, reason);
5286
+ };
5287
+ const loadOpts = { now, onSkip: wrappedSkip };
5288
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
5289
+ const entries = await loadSessionEntries(input.paths, loadOpts);
5290
+ const sessions = [];
5291
+ for (const entry of entries) {
5292
+ const events = [];
5293
+ let eventsUnreadable = false;
5294
+ try {
5295
+ for await (const ev of replayEvents(join15(input.paths.sessions, entry.sessionId), {
5296
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
5297
+ })) {
5298
+ events.push(ev);
5299
+ }
5300
+ } catch {
5301
+ eventsUnreadable = true;
5302
+ if (!unreadableEmitted.has(entry.sessionId)) {
5303
+ wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
5304
+ }
5305
+ }
5306
+ sessions.push(
5307
+ sessionWorkStatsFromEvents(
5308
+ entry.sessionId,
5309
+ entry.session.session,
5310
+ events,
5311
+ now,
5312
+ eventsUnreadable
5313
+ )
5314
+ );
5315
+ }
5316
+ const allIntervals = [];
5317
+ for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
5318
+ const union = unionDurationMs(allIntervals);
5319
+ return {
5320
+ generatedAt: now.toISOString(),
5321
+ activeGapCapMs: ACTIVE_GAP_CAP_MS,
5322
+ timeZone,
5323
+ totals: computeTotals(sessions, union.ms),
5324
+ sessions,
5325
+ bySource: computeBySource(sessions),
5326
+ byStatus: computeByStatus(sessions),
5327
+ byDay: computeByDay(sessions, union.merged, timeZone)
5328
+ };
5329
+ }
5330
+ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
5331
+ let commandCount = 0;
5332
+ let fileChangedCount = 0;
5333
+ let decisionCount = 0;
5334
+ let commandTimeMs = 0;
5335
+ const timestamps = [];
5336
+ for (const ev of events) {
5337
+ const t = Date.parse(ev.occurred_at);
5338
+ if (Number.isFinite(t)) timestamps.push(t);
5339
+ if (ev.type === "command_executed") {
5340
+ commandCount++;
5341
+ commandTimeMs += ev.duration_ms;
5342
+ } else if (ev.type === "file_changed") {
5343
+ fileChangedCount++;
5344
+ } else if (ev.type === "decision_recorded") {
5345
+ decisionCount++;
5346
+ }
5347
+ }
4182
5348
  const span = computeSpan(inner.started_at, inner.ended_at, now);
4183
5349
  const tokens = readTokens(inner.metrics);
4184
5350
  const active = resolveActiveTime(inner.metrics, timestamps);
@@ -4413,7 +5579,7 @@ async function renderReport(input) {
4413
5579
  const statsBySession = new Map(stats.sessions.map((s) => [s.sessionId, s]));
4414
5580
  const decisions = [];
4415
5581
  for (const entry of entries) {
4416
- const sessionDir = join14(input.paths.sessions, entry.sessionId);
5582
+ const sessionDir = join16(input.paths.sessions, entry.sessionId);
4417
5583
  try {
4418
5584
  for await (const ev of replayEvents(sessionDir, {
4419
5585
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -4691,6 +5857,231 @@ function formatInt(n) {
4691
5857
  return n.toLocaleString("en-US");
4692
5858
  }
4693
5859
 
5860
+ // src/review/review-gaps.ts
5861
+ import { existsSync, realpathSync } from "fs";
5862
+ import { homedir as homedir2 } from "os";
5863
+ import { basename as basename2, isAbsolute, join as join17 } from "path";
5864
+ function stripQuotes(s) {
5865
+ if (s.length >= 2 && (s[0] === '"' && s.at(-1) === '"' || s[0] === "'" && s.at(-1) === "'")) {
5866
+ return s.slice(1, -1);
5867
+ }
5868
+ return s;
5869
+ }
5870
+ var realpathCache = /* @__PURE__ */ new Map();
5871
+ function resolveRealpath(absPath) {
5872
+ const cached = realpathCache.get(absPath);
5873
+ if (cached !== void 0) return cached;
5874
+ let resolved;
5875
+ try {
5876
+ resolved = realpathSync(absPath);
5877
+ } catch {
5878
+ resolved = null;
5879
+ }
5880
+ realpathCache.set(absPath, resolved);
5881
+ return resolved;
5882
+ }
5883
+ var repoRootCache = /* @__PURE__ */ new Map();
5884
+ function isRepoRoot(realPath) {
5885
+ const cached = repoRootCache.get(realPath);
5886
+ if (cached !== void 0) return cached;
5887
+ const result = existsSync(join17(realPath, ".git"));
5888
+ repoRootCache.set(realPath, result);
5889
+ return result;
5890
+ }
5891
+ function normalizeRepoPath(p) {
5892
+ if (!p) return null;
5893
+ let s = stripQuotes(p.trim()).replace(/\/+$/, "");
5894
+ if (s.length === 0 || s === "~") return null;
5895
+ if (s.startsWith("~/")) s = homedir2() + s.slice(1);
5896
+ if (isAbsolute(s)) {
5897
+ const real = resolveRealpath(s);
5898
+ if (real !== null) {
5899
+ return isRepoRoot(real) ? real : null;
5900
+ }
5901
+ }
5902
+ s = s.replace(/\/[^/]*-workspace\/([^/]+)/, "/$1");
5903
+ const seg = s.split("/").filter((x) => x.length > 0).pop();
5904
+ if (seg === void 0) return null;
5905
+ if (/-workspace$/.test(seg) || seg.includes("$")) return null;
5906
+ return s;
5907
+ }
5908
+ function normalizeRepoKey(p) {
5909
+ const full = normalizeRepoPath(p);
5910
+ return full === null ? null : basename2(full);
5911
+ }
5912
+ function inspectCommand(args) {
5913
+ const a = args.join(" ");
5914
+ const files = /* @__PURE__ */ new Set();
5915
+ const examinedDiff = /\bgit\s+(?:diff|show|log\s+-p|add\s+-p)\b/.test(a);
5916
+ for (const re of [
5917
+ /\b(?:cat|less|bat|head|tail)\s+([^\s|&;<>]+)/g,
5918
+ /\bsed\s+-n\s+'[^']*'\s+([^\s|&;<>]+)/g,
5919
+ /\b(?:rg|grep)\b[^|&;]*?\s([^\s|&;<>]+\.[A-Za-z0-9]+)(?:\s|$)/g
5920
+ ]) {
5921
+ let m;
5922
+ while ((m = re.exec(a)) !== null) {
5923
+ const f = m[1];
5924
+ if (f !== void 0) files.add(basename2(f));
5925
+ }
5926
+ }
5927
+ return { files: [...files], examinedDiff };
5928
+ }
5929
+ function commandRepo(args, cwd) {
5930
+ const cd = args.join(" ").match(/\bcd\s+("[^"]+"|'[^']+'|[^\s&]+)\s*&&/);
5931
+ if (cd) return normalizeRepoPath(cd[1]);
5932
+ return normalizeRepoPath(cwd);
5933
+ }
5934
+ function commandFailed(exitCode) {
5935
+ return exitCode !== null && exitCode !== 0;
5936
+ }
5937
+ function commitFiles(args) {
5938
+ const a = args.join(" ");
5939
+ const add = a.match(/git add\s+([^&|;]+)/);
5940
+ if (!add?.[1]) return [];
5941
+ return add[1].split(/\s+/).filter((t) => /\.[A-Za-z]/.test(t) && !t.startsWith("-")).map((t) => basename2(t));
5942
+ }
5943
+ var REVIEW_SOURCE = "codex-import";
5944
+ var DEFAULT_WINDOW_HOURS = 24;
5945
+ async function findReviewGaps(input) {
5946
+ const now = new Date(input.nowIso);
5947
+ const windowHours = input.windowHours ?? DEFAULT_WINDOW_HOURS;
5948
+ const scope = input.scope && input.scope.length > 0 ? input.scope : null;
5949
+ const loadOpts = { now };
5950
+ if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
5951
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
5952
+ const entries = await loadSessionEntries(input.paths, loadOpts);
5953
+ const reviews = [];
5954
+ const workUnits = /* @__PURE__ */ new Map();
5955
+ const unknownCommits = /* @__PURE__ */ new Map();
5956
+ for (const entry of entries) {
5957
+ const sessionDir = join17(input.paths.sessions, entry.sessionId);
5958
+ const isReview = entry.session.session.source.kind === REVIEW_SOURCE;
5959
+ const reviewRepos = /* @__PURE__ */ new Map();
5960
+ let reviewEnd = null;
5961
+ try {
5962
+ for await (const ev of replayEvents(sessionDir, {
5963
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
5964
+ })) {
5965
+ if (ev.type !== "command_executed") continue;
5966
+ if (commandFailed(ev.exit_code)) continue;
5967
+ const at = Date.parse(ev.occurred_at);
5968
+ if (isReview) {
5969
+ const repo2 = commandRepo(ev.args, ev.cwd);
5970
+ if (repo2 === null) continue;
5971
+ const ins = inspectCommand(ev.args);
5972
+ const slot = reviewRepos.get(repo2) ?? { examinedDiff: false, files: /* @__PURE__ */ new Set() };
5973
+ if (ins.examinedDiff) slot.examinedDiff = true;
5974
+ for (const f of ins.files) slot.files.add(f);
5975
+ reviewRepos.set(repo2, slot);
5976
+ if (!Number.isNaN(at)) reviewEnd = reviewEnd === null ? at : Math.max(reviewEnd, at);
5977
+ continue;
5978
+ }
5979
+ if (!ev.args.join(" ").includes("git commit")) continue;
5980
+ const repo = commandRepo(ev.args, ev.cwd);
5981
+ if (repo === null || Number.isNaN(at)) {
5982
+ const list2 = unknownCommits.get(entry.sessionId) ?? [];
5983
+ list2.push(Number.isNaN(at) ? null : at);
5984
+ unknownCommits.set(entry.sessionId, list2);
5985
+ continue;
5986
+ }
5987
+ const byRepo = workUnits.get(entry.sessionId) ?? /* @__PURE__ */ new Map();
5988
+ const list = byRepo.get(repo) ?? [];
5989
+ list.push({ repo, at, files: commitFiles(ev.args) });
5990
+ byRepo.set(repo, list);
5991
+ workUnits.set(entry.sessionId, byRepo);
5992
+ }
5993
+ } catch {
5994
+ input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
5995
+ continue;
5996
+ }
5997
+ if (isReview && reviewRepos.size > 0) {
5998
+ reviews.push({ sessionId: entry.sessionId, endedAt: reviewEnd, repos: reviewRepos });
5999
+ }
6000
+ }
6001
+ const windowMs = windowHours * 3600 * 1e3;
6002
+ const units = [];
6003
+ let newestCommit = null;
6004
+ for (const [sessionId, byRepo] of workUnits) {
6005
+ for (const [repoPath, commits] of byRepo) {
6006
+ const label = basename2(repoPath);
6007
+ if (scope !== null && !scope.includes(label)) continue;
6008
+ const times = commits.map((c) => c.at).sort((a, b) => a - b);
6009
+ const first = times[0] ?? null;
6010
+ const last = times[times.length - 1] ?? null;
6011
+ if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
6012
+ const changedFiles = new Set(commits.flatMap((c) => c.files));
6013
+ const before = first ?? last ?? 0;
6014
+ const nearby = reviews.filter((r) => {
6015
+ if (!r.repos.has(repoPath) || r.endedAt === null) return false;
6016
+ return r.endedAt <= before && r.endedAt >= before - windowMs;
6017
+ });
6018
+ const bound = nearby.filter((r) => {
6019
+ const touched = r.repos.get(repoPath);
6020
+ if (touched === void 0) return false;
6021
+ if (touched.examinedDiff) return true;
6022
+ for (const f of changedFiles) if (touched.files.has(f)) return true;
6023
+ return false;
6024
+ });
6025
+ const verdict = bound.length > 0 ? "candidate" : nearby.length > 0 ? "near_unbound" : "omission";
6026
+ const cited = verdict === "candidate" ? bound : verdict === "near_unbound" ? nearby : [];
6027
+ units.push({
6028
+ repo: label,
6029
+ sessionId,
6030
+ commitCount: commits.length,
6031
+ firstCommitAt: first === null ? null : new Date(first).toISOString(),
6032
+ lastCommitAt: last === null ? null : new Date(last).toISOString(),
6033
+ verdict,
6034
+ reviews: cited.map((r) => ({
6035
+ sessionId: r.sessionId,
6036
+ examinedDiff: r.repos.get(repoPath)?.examinedDiff ?? false,
6037
+ files: [...r.repos.get(repoPath)?.files ?? []].slice(0, 8),
6038
+ endedAt: r.endedAt === null ? null : new Date(r.endedAt).toISOString()
6039
+ }))
6040
+ });
6041
+ }
6042
+ }
6043
+ if (scope === null) {
6044
+ for (const [sessionId, times] of unknownCommits) {
6045
+ const valid = times.filter((t) => t !== null).sort((a, b) => a - b);
6046
+ const first = valid[0] ?? null;
6047
+ const last = valid[valid.length - 1] ?? null;
6048
+ if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
6049
+ units.push({
6050
+ repo: "(unknown)",
6051
+ sessionId,
6052
+ commitCount: times.length,
6053
+ firstCommitAt: first === null ? null : new Date(first).toISOString(),
6054
+ lastCommitAt: last === null ? null : new Date(last).toISOString(),
6055
+ verdict: "unknown",
6056
+ reviews: []
6057
+ });
6058
+ }
6059
+ }
6060
+ const recentFirst = (a, b) => (Date.parse(b.lastCommitAt ?? "") || 0) - (Date.parse(a.lastCommitAt ?? "") || 0);
6061
+ const repoKeys = [...new Set(units.map((u) => u.repo))].sort();
6062
+ const repos = repoKeys.map((repo) => {
6063
+ const us = units.filter((u) => u.repo === repo);
6064
+ return {
6065
+ repo,
6066
+ units: us.length,
6067
+ omissionUnits: us.filter((u) => u.verdict === "omission").length,
6068
+ nearUnboundUnits: us.filter((u) => u.verdict === "near_unbound").length,
6069
+ candidateUnits: us.filter((u) => u.verdict === "candidate").length,
6070
+ unknownUnits: us.filter((u) => u.verdict === "unknown").length
6071
+ };
6072
+ });
6073
+ return {
6074
+ generatedAt: input.nowIso,
6075
+ windowHours,
6076
+ scope,
6077
+ repos,
6078
+ gaps: units.filter((u) => u.verdict === "omission" || u.verdict === "near_unbound").sort(recentFirst),
6079
+ candidates: units.filter((u) => u.verdict === "candidate").sort(recentFirst),
6080
+ unknowns: units.filter((u) => u.verdict === "unknown").sort(recentFirst),
6081
+ newestCommitAt: newestCommit === null ? null : new Date(newestCommit).toISOString()
6082
+ };
6083
+ }
6084
+
4694
6085
  // src/runtime/child-process-runner.ts
4695
6086
  import { spawn as spawn2 } from "child_process";
4696
6087
  var DEFAULT_KILL_GRACE_MS = 5e3;
@@ -4818,55 +6209,6 @@ function classifySpawnError(error) {
4818
6209
  // src/schemas/json-schema.ts
4819
6210
  import { z as z11 } from "zod";
4820
6211
 
4821
- // src/schemas/manifest.schema.ts
4822
- import { z as z9 } from "zod";
4823
- var ProjectSchema = z9.object({
4824
- name: z9.string().optional(),
4825
- description: z9.string().optional(),
4826
- repository_url: z9.string().nullable().optional()
4827
- });
4828
- var CapabilitiesSchema = z9.object({
4829
- enabled: z9.array(z9.string())
4830
- });
4831
- var ApprovalConfigSchema = z9.object({
4832
- required_for: z9.array(z9.string()).optional(),
4833
- default_risk_level: z9.enum(["low", "medium", "high", "critical"])
4834
- });
4835
- var ClaudeCodeAdapterConfigSchema = z9.object({
4836
- enabled: z9.boolean(),
4837
- config_path: z9.string().optional()
4838
- });
4839
- var AdaptersSchema = z9.object({
4840
- "claude-code": ClaudeCodeAdapterConfigSchema
4841
- });
4842
- var GitConfigSchema = z9.object({
4843
- events_log: z9.enum(["ignore", "commit"]).default("ignore")
4844
- });
4845
- var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
4846
- var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
4847
- message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
4848
- });
4849
- var ImportConfigSchema = z9.object({
4850
- source_roots: z9.array(SourceRootSchema).min(1).optional()
4851
- });
4852
- var WorkspaceMetaSchema = z9.object({
4853
- id: WorkspaceIdSchema,
4854
- name: z9.string().min(1),
4855
- created_at: IsoTimestampSchema,
4856
- updated_at: IsoTimestampSchema
4857
- });
4858
- var ManifestSchema = z9.object({
4859
- schema_version: SchemaVersionSchema,
4860
- basou_version: z9.literal("0.1.0"),
4861
- workspace: WorkspaceMetaSchema,
4862
- project: ProjectSchema,
4863
- capabilities: CapabilitiesSchema,
4864
- approval: ApprovalConfigSchema,
4865
- adapters: AdaptersSchema,
4866
- git: GitConfigSchema,
4867
- import: ImportConfigSchema.optional()
4868
- });
4869
-
4870
6212
  // src/schemas/session-import.schema.ts
4871
6213
  import { z as z10 } from "zod";
4872
6214
  var SessionInnerImportSchema = z10.object({
@@ -4986,28 +6328,29 @@ function serializeJsonSchema(schema) {
4986
6328
  }
4987
6329
 
4988
6330
  // src/storage/basou-dir.ts
4989
- import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
4990
- import { join as join15 } from "path";
6331
+ import { lstat as lstat4, mkdir as mkdir4 } from "fs/promises";
6332
+ import { join as join18 } from "path";
4991
6333
  function basouPaths(repositoryRoot) {
4992
- const root = join15(repositoryRoot, ".basou");
4993
- const approvalsBase = join15(root, "approvals");
6334
+ const root = join18(repositoryRoot, ".basou");
6335
+ const approvalsBase = join18(root, "approvals");
4994
6336
  return {
4995
6337
  root,
4996
- sessions: join15(root, "sessions"),
4997
- tasks: join15(root, "tasks"),
6338
+ sessions: join18(root, "sessions"),
6339
+ tasks: join18(root, "tasks"),
4998
6340
  approvals: {
4999
- pending: join15(approvalsBase, "pending"),
5000
- resolved: join15(approvalsBase, "resolved")
6341
+ pending: join18(approvalsBase, "pending"),
6342
+ resolved: join18(approvalsBase, "resolved")
5001
6343
  },
5002
- locks: join15(root, "locks"),
5003
- logs: join15(root, "logs"),
5004
- raw: join15(root, "raw"),
5005
- tmp: join15(root, "tmp"),
6344
+ locks: join18(root, "locks"),
6345
+ logs: join18(root, "logs"),
6346
+ raw: join18(root, "raw"),
6347
+ tmp: join18(root, "tmp"),
5006
6348
  files: {
5007
- manifest: join15(root, "manifest.yaml"),
5008
- status: join15(root, "status.json"),
5009
- handoff: join15(root, "handoff.md"),
5010
- decisions: join15(root, "decisions.md")
6349
+ manifest: join18(root, "manifest.yaml"),
6350
+ status: join18(root, "status.json"),
6351
+ handoff: join18(root, "handoff.md"),
6352
+ decisions: join18(root, "decisions.md"),
6353
+ orientation: join18(root, "orientation.md")
5011
6354
  }
5012
6355
  };
5013
6356
  }
@@ -5025,9 +6368,9 @@ async function ensureBasouDirectory(repositoryRoot) {
5025
6368
  const paths = basouPaths(repositoryRoot);
5026
6369
  let existing;
5027
6370
  try {
5028
- existing = await lstat3(paths.root);
6371
+ existing = await lstat4(paths.root);
5029
6372
  } catch (error) {
5030
- if (!hasErrorCode3(error) || error.code !== "ENOENT") {
6373
+ if (!hasErrorCode4(error) || error.code !== "ENOENT") {
5031
6374
  throw new Error("Failed to inspect .basou directory", { cause: error });
5032
6375
  }
5033
6376
  }
@@ -5050,13 +6393,13 @@ async function mkdirLabeled(target, label) {
5050
6393
  try {
5051
6394
  await mkdir4(target, { recursive: true });
5052
6395
  } catch (error) {
5053
- if (hasErrorCode3(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
6396
+ if (hasErrorCode4(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
5054
6397
  throw new Error(`${label} exists but is not a directory`, { cause: error });
5055
6398
  }
5056
6399
  throw new Error(`Failed to create ${label}`, { cause: error });
5057
6400
  }
5058
6401
  }
5059
- function hasErrorCode3(error) {
6402
+ function hasErrorCode4(error) {
5060
6403
  if (!(error instanceof Error)) return false;
5061
6404
  const codeProp = error.code;
5062
6405
  return typeof codeProp === "string";
@@ -5064,28 +6407,30 @@ function hasErrorCode3(error) {
5064
6407
 
5065
6408
  // src/storage/gitignore.ts
5066
6409
  import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
5067
- import { join as join16 } from "path";
6410
+ import { join as join19 } from "path";
5068
6411
  var MARKER = "# Basou - default ignore";
5069
- var BASOU_GITIGNORE_BLOCK = "# Basou - default ignore\n.basou/logs/\n.basou/raw/\n.basou/tmp/\n.basou/locks/\n.basou/status.json\n.basou/sessions/*/events.jsonl\n.basou/sessions/*/artifacts/\n.basou/approvals/pending/\n.basou/approvals/resolved/\n\n# Basou - default commit\n# .basou/manifest.yaml\n# .basou/handoff.md\n# .basou/decisions.md\n# .basou/tasks/\n# .basou/sessions/*/session.yaml\n# .basou/sessions/*/transcript.md\n# .basou/sessions/*/changed-files.json\n";
5070
- async function appendBasouGitignore(repositoryRoot) {
5071
- const gitignorePath = join16(repositoryRoot, ".gitignore");
6412
+ 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";
6413
+ 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";
6414
+ async function appendBasouGitignore(repositoryRoot, options = {}) {
6415
+ const gitignorePath = join19(repositoryRoot, ".gitignore");
5072
6416
  let body;
5073
6417
  let existed;
5074
6418
  try {
5075
6419
  body = await readFile8(gitignorePath, "utf8");
5076
6420
  existed = true;
5077
6421
  } catch (error) {
5078
- if (hasErrorCode4(error) && error.code === "ENOENT") {
6422
+ if (hasErrorCode5(error) && error.code === "ENOENT") {
5079
6423
  body = "";
5080
6424
  existed = false;
5081
6425
  } else {
5082
6426
  throw new Error("Failed to read .gitignore", { cause: error });
5083
6427
  }
5084
6428
  }
5085
- if (existed && hasBasouMarker(body)) {
6429
+ if (existed && hasBasouGitignore(body)) {
5086
6430
  return { appended: false };
5087
6431
  }
5088
- const next = composeNextBody(body);
6432
+ const block = options.localOnly === true ? BASOU_GITIGNORE_BLOCK_LOCAL_ONLY : BASOU_GITIGNORE_BLOCK;
6433
+ const next = composeNextBody(body, block);
5089
6434
  try {
5090
6435
  await writeFile2(gitignorePath, next, { encoding: "utf8" });
5091
6436
  } catch (error) {
@@ -5093,84 +6438,20 @@ async function appendBasouGitignore(repositoryRoot) {
5093
6438
  }
5094
6439
  return { appended: true };
5095
6440
  }
5096
- function hasBasouMarker(body) {
6441
+ function hasBasouGitignore(body) {
5097
6442
  for (const rawLine of body.split("\n")) {
5098
- if (rawLine.trimEnd().startsWith(MARKER)) return true;
6443
+ const line = rawLine.trimEnd();
6444
+ if (line.startsWith(MARKER)) return true;
6445
+ if (line === ".basou/" || line === "/.basou/") return true;
5099
6446
  }
5100
6447
  return false;
5101
6448
  }
5102
- function composeNextBody(existing) {
5103
- if (existing.length === 0) return BASOU_GITIGNORE_BLOCK;
6449
+ function composeNextBody(existing, block) {
6450
+ if (existing.length === 0) return block;
5104
6451
  const normalized = existing.endsWith("\n") ? existing : `${existing}
5105
6452
  `;
5106
6453
  return `${normalized}
5107
- ${BASOU_GITIGNORE_BLOCK}`;
5108
- }
5109
- function hasErrorCode4(error) {
5110
- if (!(error instanceof Error)) return false;
5111
- return typeof error.code === "string";
5112
- }
5113
-
5114
- // src/storage/manifest.ts
5115
- import { lstat as lstat4 } from "fs/promises";
5116
- function createManifest(input) {
5117
- if (input.workspaceName.length === 0) {
5118
- throw new Error("Workspace name is empty. Pass --name explicitly.");
5119
- }
5120
- const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
5121
- const workspaceId = input.workspaceId ?? prefixedUlid("ws");
5122
- const project = {
5123
- ...input.projectName !== void 0 ? { name: input.projectName } : {},
5124
- ...input.projectDescription !== void 0 ? { description: input.projectDescription } : {},
5125
- ...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
5126
- };
5127
- const manifest = {
5128
- schema_version: "0.1.0",
5129
- basou_version: "0.1.0",
5130
- workspace: {
5131
- id: workspaceId,
5132
- name: input.workspaceName,
5133
- created_at: now,
5134
- updated_at: now
5135
- },
5136
- project,
5137
- capabilities: {
5138
- enabled: ["core", "claude-code-adapter", "terminal-recording", "git-capability", "approval"]
5139
- },
5140
- approval: {
5141
- required_for: ["destructive_command", "external_send"],
5142
- default_risk_level: "medium"
5143
- },
5144
- adapters: {
5145
- "claude-code": { enabled: true }
5146
- },
5147
- git: { events_log: "ignore" },
5148
- ...input.sourceRoots !== void 0 && input.sourceRoots.length > 0 ? { import: { source_roots: input.sourceRoots } } : {}
5149
- };
5150
- return ManifestSchema.parse(manifest);
5151
- }
5152
- async function writeManifest(paths, manifest, options) {
5153
- const force = options?.force === true;
5154
- const validated = ManifestSchema.parse(manifest);
5155
- if (!force) {
5156
- let existed = false;
5157
- try {
5158
- await lstat4(paths.files.manifest);
5159
- existed = true;
5160
- } catch (error) {
5161
- if (!hasErrorCode5(error) || error.code !== "ENOENT") {
5162
- throw new Error("Failed to inspect existing manifest", { cause: error });
5163
- }
5164
- }
5165
- if (existed) {
5166
- throw new Error("Already initialized. Use --force to overwrite.");
5167
- }
5168
- }
5169
- await writeYamlFile(paths.files.manifest, validated);
5170
- }
5171
- async function readManifest(paths) {
5172
- const raw = await readYamlFile(paths.files.manifest);
5173
- return ManifestSchema.parse(raw);
6454
+ ${block}`;
5174
6455
  }
5175
6456
  function hasErrorCode5(error) {
5176
6457
  if (!(error instanceof Error)) return false;
@@ -5282,8 +6563,8 @@ function hasErrorCode6(error) {
5282
6563
 
5283
6564
  // src/storage/session-import.ts
5284
6565
  import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
5285
- import { homedir as homedir2 } from "os";
5286
- import { join as join17 } from "path";
6566
+ import { homedir as homedir3 } from "os";
6567
+ import { join as join20 } from "path";
5287
6568
  async function importSessionFromJson(paths, manifest, payload, options) {
5288
6569
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
5289
6570
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -5308,7 +6589,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
5308
6589
  pathSanitizeReport
5309
6590
  };
5310
6591
  }
5311
- const sessionDir = join17(paths.sessions, newSessionId);
6592
+ const sessionDir = join20(paths.sessions, newSessionId);
5312
6593
  try {
5313
6594
  await mkdir5(sessionDir, { recursive: true });
5314
6595
  } catch (error) {
@@ -5322,7 +6603,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
5322
6603
  throw error;
5323
6604
  }
5324
6605
  try {
5325
- const sessionYamlPath = join17(sessionDir, "session.yaml");
6606
+ const sessionYamlPath = join20(sessionDir, "session.yaml");
5326
6607
  await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
5327
6608
  } catch (error) {
5328
6609
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
@@ -5391,7 +6672,7 @@ function withIntegrity(record, chainResult) {
5391
6672
  };
5392
6673
  }
5393
6674
  function buildSessionRecord(input, manifest, newSessionId, options) {
5394
- const home = homedir2();
6675
+ const home = homedir3();
5395
6676
  const workingDirectoryRaw = input.working_directory;
5396
6677
  const workingDirectorySanitized = sanitizeWorkingDirectory(workingDirectoryRaw, {
5397
6678
  homedir: home
@@ -5490,7 +6771,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
5490
6771
  async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
5491
6772
  const sessionId = priorSessionId;
5492
6773
  const importSource = freshPayload.session.source.kind;
5493
- const sessionDir = join17(paths.sessions, priorSessionId);
6774
+ const sessionDir = join20(paths.sessions, priorSessionId);
5494
6775
  const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
5495
6776
  try {
5496
6777
  const priorVerdict = await verifyEventsChain(paths, priorSessionId);
@@ -5532,7 +6813,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5532
6813
  };
5533
6814
  const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
5534
6815
  if (options.dryRun !== true) {
5535
- const eventsPath = join17(sessionDir, "events.jsonl");
6816
+ const eventsPath = join20(sessionDir, "events.jsonl");
5536
6817
  let priorEventsRaw = null;
5537
6818
  try {
5538
6819
  priorEventsRaw = await readFile10(eventsPath);
@@ -5544,7 +6825,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5544
6825
  const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
5545
6826
  try {
5546
6827
  await overwriteYamlFile(
5547
- join17(sessionDir, "session.yaml"),
6828
+ join20(sessionDir, "session.yaml"),
5548
6829
  withIntegrity(updatedRecord, chainResult)
5549
6830
  );
5550
6831
  } catch (error) {
@@ -5568,7 +6849,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5568
6849
  }
5569
6850
  }
5570
6851
  async function rechainSessionInPlace(paths, sessionId, options = {}) {
5571
- const sessionDir = join17(paths.sessions, sessionId);
6852
+ const sessionDir = join20(paths.sessions, sessionId);
5572
6853
  let lock;
5573
6854
  try {
5574
6855
  lock = await acquireLock(paths, "session", sessionId);
@@ -5601,7 +6882,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5601
6882
  if (verdict.status !== "unchained") {
5602
6883
  return { status: "skipped", reason: "tampered" };
5603
6884
  }
5604
- const eventsPath = join17(sessionDir, "events.jsonl");
6885
+ const eventsPath = join20(sessionDir, "events.jsonl");
5605
6886
  let priorRaw;
5606
6887
  try {
5607
6888
  priorRaw = await readFile10(eventsPath);
@@ -5649,7 +6930,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5649
6930
  }
5650
6931
  try {
5651
6932
  await overwriteYamlFile(
5652
- join17(sessionDir, "session.yaml"),
6933
+ join20(sessionDir, "session.yaml"),
5653
6934
  withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
5654
6935
  );
5655
6936
  } catch (error) {
@@ -5731,23 +7012,34 @@ export {
5731
7012
  enumerateTaskIds,
5732
7013
  finalizeSessionYaml,
5733
7014
  findErrorCode,
7015
+ findReviewGaps,
5734
7016
  formatDurationMs,
5735
7017
  genesisHash,
5736
7018
  getDiff,
5737
7019
  getSnapshot,
5738
7020
  importSessionFromJson,
5739
7021
  inspectChainTail,
7022
+ isGitNotFound,
5740
7023
  isImportDerivedSource,
5741
7024
  isLazyExpired,
7025
+ isRenderable,
5742
7026
  isValidPrefixedId,
5743
7027
  lineHash,
5744
7028
  linkYamlFile,
5745
7029
  loadApproval,
5746
7030
  loadSessionEntries,
5747
7031
  loadTaskEntries,
7032
+ normalizeRepoKey,
7033
+ normalizeRepoPath,
5748
7034
  overwriteYamlFile,
5749
7035
  parseDuration,
5750
7036
  parseMarkers,
7037
+ pathBasename,
7038
+ planArchive,
7039
+ planGitignore,
7040
+ planRename,
7041
+ planRosterAdoption,
7042
+ planWorkspaceView,
5751
7043
  prefixedUlid,
5752
7044
  readAllEvents,
5753
7045
  readManifest,
@@ -5759,18 +7051,23 @@ export {
5759
7051
  readYamlFile,
5760
7052
  rechainSessionInPlace,
5761
7053
  reconcileAllTasks,
7054
+ reconcileSourceRoots,
5762
7055
  reconcileTask,
5763
7056
  refreshTaskLinkedSessions,
5764
7057
  reimportPreservingId,
5765
7058
  renderDecisions,
5766
7059
  renderHandoff,
7060
+ renderOrientation,
7061
+ renderPresetBlock,
5767
7062
  renderReport,
5768
7063
  renderWithMarkers,
5769
7064
  replayEvents,
7065
+ resolveBasouRepositoryRoot,
5770
7066
  resolveClaudeCodeCommand,
5771
7067
  resolveRepositoryRoot,
5772
7068
  resolveSessionId,
5773
7069
  resolveTaskId,
7070
+ safeSimpleGit,
5774
7071
  sanitizePath,
5775
7072
  sanitizeRelatedFiles,
5776
7073
  sanitizeWorkingDirectory,
@@ -5778,8 +7075,14 @@ export {
5778
7075
  serializeJsonSchema,
5779
7076
  sessionWorkStatsFromEvents,
5780
7077
  summarizeAdapterOutput,
7078
+ summarizeOrientation,
7079
+ summarizePresetPlan,
7080
+ summarizeRosterDrift,
7081
+ summarizeSymlinkPlan,
7082
+ summarizeWiring,
5781
7083
  tryRemoteUrl,
5782
7084
  ulid,
7085
+ unknownManifestKeys,
5783
7086
  updateTaskStatusWithEvent,
5784
7087
  verifyEventsChain,
5785
7088
  writeEventsBulk,