@basou/cli 0.27.0 → 0.29.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
@@ -2034,11 +2034,73 @@ async function assertWorkspaceInitialized4(basouRoot) {
2034
2034
  }
2035
2035
 
2036
2036
  // src/commands/hook.ts
2037
- import { open, readFile as readFile2, stat as stat2 } from "fs/promises";
2037
+ import { open as open2, readFile as readFile2, stat as stat3 } from "fs/promises";
2038
+ import { homedir as homedir4 } from "os";
2039
+ import { join as join6 } from "path";
2040
+ import { fileURLToPath } from "url";
2038
2041
  import {
2042
+ buildStopHookCommand,
2039
2043
  DEFAULT_STOP_HOOK_MIN_EDITS,
2040
- evaluateStopHook
2044
+ evaluateStopHook,
2045
+ findBasouStopHookCommand,
2046
+ removeStopHook,
2047
+ upsertStopHook
2041
2048
  } from "@basou/core";
2049
+
2050
+ // src/lib/durable-write.ts
2051
+ import { randomUUID } from "crypto";
2052
+ import { lstat, open, rename, stat as stat2, unlink as unlink2 } from "fs/promises";
2053
+ import { basename as basename2, dirname, join as join5 } from "path";
2054
+ async function assertNotSymlink(targetPath) {
2055
+ try {
2056
+ const st = await lstat(targetPath);
2057
+ if (st.isSymbolicLink()) {
2058
+ throw new Error(
2059
+ "Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
2060
+ );
2061
+ }
2062
+ } catch (error) {
2063
+ if (error instanceof Error && error.code === "ENOENT") return;
2064
+ throw error;
2065
+ }
2066
+ }
2067
+ async function writeFileDurable(targetPath, content) {
2068
+ const dir = dirname(targetPath);
2069
+ const tmpPath = join5(dir, `.${basename2(targetPath)}.tmp.${randomUUID()}`);
2070
+ let mode = 420;
2071
+ try {
2072
+ mode = (await stat2(targetPath)).mode & 511;
2073
+ } catch (error) {
2074
+ if (!(error instanceof Error && error.code === "ENOENT")) {
2075
+ throw error;
2076
+ }
2077
+ }
2078
+ let handle;
2079
+ try {
2080
+ handle = await open(tmpPath, "wx", mode);
2081
+ await handle.writeFile(content, "utf8");
2082
+ await handle.chmod(mode);
2083
+ await handle.sync();
2084
+ await handle.close();
2085
+ handle = void 0;
2086
+ await rename(tmpPath, targetPath);
2087
+ } catch (error) {
2088
+ if (handle) await handle.close().catch(() => void 0);
2089
+ await unlink2(tmpPath).catch(() => void 0);
2090
+ throw error;
2091
+ }
2092
+ try {
2093
+ const dirHandle = await open(dir, "r");
2094
+ try {
2095
+ await dirHandle.sync();
2096
+ } finally {
2097
+ await dirHandle.close();
2098
+ }
2099
+ } catch {
2100
+ }
2101
+ }
2102
+
2103
+ // src/commands/hook.ts
2042
2104
  var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
2043
2105
  function registerHookCommand(program2) {
2044
2106
  const hook = program2.command("hook").description(
@@ -2049,29 +2111,48 @@ function registerHookCommand(program2) {
2049
2111
  ).option(
2050
2112
  "--min-edits <n>",
2051
2113
  `Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
2114
+ ).option(
2115
+ "--block",
2116
+ "Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking reminder"
2052
2117
  ).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
2053
2118
  const minEdits = parseMinEdits(options.minEdits);
2054
- await runHookStop(minEdits !== void 0 ? { minEdits } : {});
2119
+ await runHookStop({
2120
+ ...minEdits !== void 0 ? { minEdits } : {},
2121
+ ...options.block === true ? { block: true } : {}
2122
+ });
2123
+ });
2124
+ hook.command("install").description(
2125
+ "Register the Stop hook in ~/.claude/settings.json (reproducible, idempotent). Default is advisory; --block opts into in-turn enforcement."
2126
+ ).option("--block", "Register the blocking (opt-in enforcement) form instead of advisory").option("--min-edits <n>", "Pass a custom file-edit threshold to the registered hook").option("--settings <path>", "Override the settings.json path (intended for tests)").option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (opts) => {
2127
+ await runHookInstall(opts);
2128
+ });
2129
+ hook.command("uninstall").description(
2130
+ "Remove the basou Stop hook from ~/.claude/settings.json (leaves other hooks intact)"
2131
+ ).option("--settings <path>", "Override the settings.json path (intended for tests)").option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (opts) => {
2132
+ await runHookUninstall(opts);
2133
+ });
2134
+ hook.command("status").description("Report whether the basou Stop hook is registered, and in which mode").option("--settings <path>", "Override the settings.json path (intended for tests)").option("-v, --verbose", "Show error causes").action(async (opts) => {
2135
+ await runHookStatus(opts);
2055
2136
  });
2056
2137
  }
2057
2138
  var HOOK_STOP_HELP = `
2058
- Install as a Claude Code Stop hook in ~/.claude/settings.json:
2059
- {
2060
- "hooks": {
2061
- "Stop": [
2062
- { "hooks": [ { "type": "command", "command": "basou hook stop" } ] }
2063
- ]
2064
- }
2065
- }
2139
+ Register this Stop hook reproducibly with 'basou hook install' (it writes the
2140
+ correct node-path command into ~/.claude/settings.json). 'basou hook uninstall'
2141
+ removes it; 'basou hook status' reports whether it is registered.
2066
2142
 
2067
2143
  On every turn end basou inspects the session transcript. If the session did
2068
2144
  content-substantive work but ran no capture verb ('basou decision capture' /
2069
- 'decision record' / 'note'), it emits a non-blocking reminder so the agent can
2070
- record the why / next step. Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default)
2071
- OR a free-form AskUserQuestion answer (an uncaptured conversational decision).
2072
- Read-only Bash (ls / grep / git status) does NOT count. The reminder is
2073
- non-blocking: Claude sees it and may act on it or stop; it never forces the turn
2074
- to continue. The 'stop_hook_active' flag is honored so the nudge cannot loop.
2145
+ 'decision record' / 'note'), it reminds the agent to record the why / next step.
2146
+ Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default) OR a free-form AskUserQuestion
2147
+ answer (an uncaptured conversational decision). Read-only Bash (ls / grep /
2148
+ git status) does NOT count.
2149
+
2150
+ By default the reminder is non-blocking: Claude sees it and may act on it or
2151
+ stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
2152
+ returns decision:block, holding the agent in-turn to act on the reminder; the
2153
+ 'stop_hook_active' flag and Claude Code's own loop prevention bound it to a
2154
+ single turn. Either way the hook fails open: a bad payload or unreadable
2155
+ transcript exits cleanly with no output.
2075
2156
  `;
2076
2157
  async function runHookStop(options, ctx = {}) {
2077
2158
  try {
@@ -2110,15 +2191,14 @@ async function doRunHookStop(options, ctx) {
2110
2191
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
2111
2192
  });
2112
2193
  if (evaluation.kind !== "nudge") return;
2113
- write(
2114
- `${JSON.stringify({
2115
- hookSpecificOutput: {
2116
- hookEventName: "Stop",
2117
- additionalContext: evaluation.additionalContext
2118
- }
2119
- })}
2120
- `
2121
- );
2194
+ const payloadJson = options.block === true ? JSON.stringify({ decision: "block", reason: evaluation.additionalContext }) : JSON.stringify({
2195
+ hookSpecificOutput: {
2196
+ hookEventName: "Stop",
2197
+ additionalContext: evaluation.additionalContext
2198
+ }
2199
+ });
2200
+ write(`${payloadJson}
2201
+ `);
2122
2202
  }
2123
2203
  function parseTranscript(transcript) {
2124
2204
  const records = [];
@@ -2143,9 +2223,9 @@ async function defaultReadStdin() {
2143
2223
  return Buffer.concat(chunks).toString("utf8");
2144
2224
  }
2145
2225
  async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
2146
- const { size } = await stat2(path);
2226
+ const { size } = await stat3(path);
2147
2227
  if (size <= maxBytes) return readFile2(path, "utf8");
2148
- const handle = await open(path, "r");
2228
+ const handle = await open2(path, "r");
2149
2229
  try {
2150
2230
  const buffer = Buffer.alloc(maxBytes);
2151
2231
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, size - maxBytes);
@@ -2160,12 +2240,160 @@ function parseMinEdits(raw) {
2160
2240
  if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
2161
2241
  return Number(raw);
2162
2242
  }
2243
+ var DEFAULT_CLAUDE_SETTINGS_PATH = join6(homedir4(), ".claude", "settings.json");
2244
+ function resolveCliEntry() {
2245
+ return fileURLToPath(import.meta.url);
2246
+ }
2247
+ function normalizeInstallOptions(raw) {
2248
+ const out = {};
2249
+ if (raw.block === true) out.block = true;
2250
+ if (raw.settings !== void 0) out.settings = raw.settings;
2251
+ if (raw.dryRun === true) out.dryRun = true;
2252
+ if (raw.verbose === true) out.verbose = true;
2253
+ if (raw.minEdits !== void 0) {
2254
+ const parsed = parseMinEdits(raw.minEdits);
2255
+ if (parsed === void 0) {
2256
+ throw new Error("--min-edits must be a non-negative integer.");
2257
+ }
2258
+ out.minEdits = parsed;
2259
+ }
2260
+ return out;
2261
+ }
2262
+ async function readSettings(path) {
2263
+ let raw;
2264
+ try {
2265
+ raw = await readFile2(path, "utf8");
2266
+ } catch (error) {
2267
+ if (error instanceof Error && error.code === "ENOENT") {
2268
+ return { raw: null, parsed: void 0 };
2269
+ }
2270
+ throw error;
2271
+ }
2272
+ if (raw.trim().length === 0) return { raw, parsed: void 0 };
2273
+ try {
2274
+ return { raw, parsed: JSON.parse(raw) };
2275
+ } catch (error) {
2276
+ throw new Error(
2277
+ "The Claude settings.json is not valid JSON. Fix it (or remove it) and retry.",
2278
+ {
2279
+ cause: error
2280
+ }
2281
+ );
2282
+ }
2283
+ }
2284
+ async function backupSettingsOnce(path, raw) {
2285
+ if (raw === null) return;
2286
+ const bak = `${path}.basou-bak`;
2287
+ try {
2288
+ await stat3(bak);
2289
+ return;
2290
+ } catch (error) {
2291
+ if (!(error instanceof Error && error.code === "ENOENT")) throw error;
2292
+ }
2293
+ await writeFileDurable(bak, raw);
2294
+ }
2295
+ async function runHookInstall(options, ctx = {}) {
2296
+ try {
2297
+ await doRunHookInstall(normalizeInstallOptions(options), ctx);
2298
+ } catch (error) {
2299
+ renderCliError(error, { verbose: isVerbose(options) });
2300
+ process.exitCode = 1;
2301
+ }
2302
+ }
2303
+ async function doRunHookInstall(options, ctx = {}) {
2304
+ const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
2305
+ const cliEntry = (ctx.resolveCliEntry ?? resolveCliEntry)();
2306
+ const command = buildStopHookCommand({
2307
+ cliEntry,
2308
+ ...options.block === true ? { block: true } : {},
2309
+ ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
2310
+ });
2311
+ const mode = options.block === true ? "blocking" : "advisory";
2312
+ await assertNotSymlink(settingsPath);
2313
+ const { raw, parsed } = await readSettings(settingsPath);
2314
+ const { settings, action } = upsertStopHook(parsed, command);
2315
+ const newBody = `${JSON.stringify(settings, null, 2)}
2316
+ `;
2317
+ if (raw !== null && newBody === raw) {
2318
+ console.log(`The basou Stop hook is already registered (${mode}); no change.`);
2319
+ return;
2320
+ }
2321
+ if (options.dryRun === true) {
2322
+ console.log(`[dry-run] Would ${action} the basou Stop hook (${mode}).`);
2323
+ return;
2324
+ }
2325
+ const recheck = await readSettings(settingsPath);
2326
+ if (recheck.raw !== raw) {
2327
+ throw new Error(
2328
+ "The settings.json changed during install; aborting so a concurrent edit is not overwritten. Re-run 'basou hook install'."
2329
+ );
2330
+ }
2331
+ await backupSettingsOnce(settingsPath, raw);
2332
+ await writeFileDurable(settingsPath, newBody);
2333
+ console.log(`${action === "installed" ? "Installed" : "Updated"} the basou Stop hook (${mode}).`);
2334
+ }
2335
+ async function runHookUninstall(options) {
2336
+ try {
2337
+ await doRunHookUninstall(normalizeInstallOptions(options));
2338
+ } catch (error) {
2339
+ renderCliError(error, { verbose: isVerbose(options) });
2340
+ process.exitCode = 1;
2341
+ }
2342
+ }
2343
+ async function doRunHookUninstall(options) {
2344
+ const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
2345
+ await assertNotSymlink(settingsPath);
2346
+ const { raw, parsed } = await readSettings(settingsPath);
2347
+ if (raw === null) {
2348
+ console.log("No settings.json; nothing to remove.");
2349
+ return;
2350
+ }
2351
+ const { settings, action } = removeStopHook(parsed);
2352
+ if (action === "absent") {
2353
+ console.log("No basou Stop hook found; nothing removed.");
2354
+ return;
2355
+ }
2356
+ const newBody = `${JSON.stringify(settings, null, 2)}
2357
+ `;
2358
+ if (options.dryRun === true) {
2359
+ console.log("[dry-run] Would remove the basou Stop hook from settings.json.");
2360
+ return;
2361
+ }
2362
+ const recheck = await readSettings(settingsPath);
2363
+ if (recheck.raw !== raw) {
2364
+ throw new Error(
2365
+ "The settings.json changed during uninstall; aborting so a concurrent edit is not overwritten. Re-run 'basou hook uninstall'."
2366
+ );
2367
+ }
2368
+ await backupSettingsOnce(settingsPath, raw);
2369
+ await writeFileDurable(settingsPath, newBody);
2370
+ console.log("Removed the basou Stop hook from settings.json.");
2371
+ }
2372
+ async function runHookStatus(options) {
2373
+ try {
2374
+ await doRunHookStatus(normalizeInstallOptions(options));
2375
+ } catch (error) {
2376
+ renderCliError(error, { verbose: isVerbose(options) });
2377
+ process.exitCode = 1;
2378
+ }
2379
+ }
2380
+ async function doRunHookStatus(options) {
2381
+ const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
2382
+ const { parsed } = await readSettings(settingsPath);
2383
+ const command = findBasouStopHookCommand(parsed);
2384
+ if (command === null) {
2385
+ console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
2386
+ return;
2387
+ }
2388
+ const mode = / --block\b/.test(command) ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
2389
+ console.log(`basou Stop hook: registered, ${mode}.`);
2390
+ }
2163
2391
 
2164
2392
  // src/commands/import.ts
2165
2393
  import { createReadStream } from "fs";
2166
- import { readdir, readFile as readFile3, rm, stat as stat3 } from "fs/promises";
2167
- import { homedir as homedir4 } from "os";
2168
- import { basename as basename2, dirname, join as join5, resolve as resolve4 } from "path";
2394
+ import { readdir, readFile as readFile3, rm, stat as stat4 } from "fs/promises";
2395
+ import { homedir as homedir5 } from "os";
2396
+ import { basename as basename3, dirname as dirname2, join as join7, resolve as resolve4 } from "path";
2169
2397
  import { createInterface } from "readline";
2170
2398
  import {
2171
2399
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
@@ -2251,11 +2479,11 @@ async function doRunImportClaudeCode(options, ctx) {
2251
2479
  repoRoot: repositoryRoot,
2252
2480
  cwd: ctx.cwd ?? process.cwd()
2253
2481
  });
2254
- const projectsRoot = ctx.claudeProjectsDir ?? join5(homedir4(), ".claude", "projects");
2482
+ const projectsRoot = ctx.claudeProjectsDir ?? join7(homedir5(), ".claude", "projects");
2255
2483
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
2256
2484
  const projectSet = new Set(projectPaths);
2257
2485
  const candidates = files.map((file) => {
2258
- const externalId = basename2(file, ".jsonl");
2486
+ const externalId = basename3(file, ".jsonl");
2259
2487
  return {
2260
2488
  externalId,
2261
2489
  sourcePath: file,
@@ -2290,7 +2518,7 @@ async function doRunImportCodex(options, ctx) {
2290
2518
  repoRoot: repositoryRoot,
2291
2519
  cwd: ctx.cwd ?? process.cwd()
2292
2520
  });
2293
- const sessionsRoot = ctx.codexSessionsDir ?? join5(homedir4(), ".codex", "sessions");
2521
+ const sessionsRoot = ctx.codexSessionsDir ?? join7(homedir5(), ".codex", "sessions");
2294
2522
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
2295
2523
  const candidates = rollouts.map(({ file, externalId }) => ({
2296
2524
  externalId,
@@ -2345,7 +2573,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2345
2573
  files: payload.session.related_files ?? [],
2346
2574
  workingDirectory: payload.session.working_directory,
2347
2575
  sourceRoots: projectPaths,
2348
- masterRoot: dirname(paths.root),
2576
+ masterRoot: dirname2(paths.root),
2349
2577
  extraInRoot: AGENT_INFRA_DIRS2
2350
2578
  });
2351
2579
  if (scope.outOfRoot.length > 0) crossProject.push({ externalId, outOfRoot: scope.outOfRoot });
@@ -2419,7 +2647,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2419
2647
  if (priors.length > 0 && options.force === true) {
2420
2648
  if (options.dryRun !== true) {
2421
2649
  for (const { sessionId } of priors) {
2422
- await rm(join5(paths.sessions, sessionId), { recursive: true, force: true });
2650
+ await rm(join7(paths.sessions, sessionId), { recursive: true, force: true });
2423
2651
  }
2424
2652
  }
2425
2653
  counts.replaced++;
@@ -2530,7 +2758,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2530
2758
  if (options.session !== void 0) {
2531
2759
  const matches = [];
2532
2760
  for (const projectPath of projectPaths) {
2533
- const file = join5(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2761
+ const file = join7(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2534
2762
  if (await pathExists(file)) matches.push(file);
2535
2763
  }
2536
2764
  if (matches.length === 0) {
@@ -2541,7 +2769,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2541
2769
  const files = [];
2542
2770
  let anyDirFound = false;
2543
2771
  for (const projectPath of projectPaths) {
2544
- const transcriptDir = join5(projectsRoot, encodeProjectDir(projectPath));
2772
+ const transcriptDir = join7(projectsRoot, encodeProjectDir(projectPath));
2545
2773
  let entries;
2546
2774
  try {
2547
2775
  entries = await readdir(transcriptDir);
@@ -2551,7 +2779,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2551
2779
  }
2552
2780
  anyDirFound = true;
2553
2781
  for (const name of entries) {
2554
- if (name.endsWith(".jsonl")) files.push(join5(transcriptDir, name));
2782
+ if (name.endsWith(".jsonl")) files.push(join7(transcriptDir, name));
2555
2783
  }
2556
2784
  }
2557
2785
  if (!anyDirFound) {
@@ -2561,7 +2789,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2561
2789
  }
2562
2790
  async function pathExists(file) {
2563
2791
  try {
2564
- await stat3(file);
2792
+ await stat4(file);
2565
2793
  return true;
2566
2794
  } catch (error) {
2567
2795
  if (findErrorCode5(error, "ENOENT")) return false;
@@ -2570,7 +2798,7 @@ async function pathExists(file) {
2570
2798
  }
2571
2799
  async function statSize(file) {
2572
2800
  try {
2573
- return (await stat3(file)).size;
2801
+ return (await stat4(file)).size;
2574
2802
  } catch (error) {
2575
2803
  if (findErrorCode5(error, "ENOENT")) return void 0;
2576
2804
  throw error;
@@ -2608,7 +2836,7 @@ async function findRolloutFiles(sessionsRoot) {
2608
2836
  throw new Error("Failed to read Codex sessions directory", { cause: error });
2609
2837
  }
2610
2838
  for (const entry of entries) {
2611
- const full = join5(dir, entry.name);
2839
+ const full = join7(dir, entry.name);
2612
2840
  if (entry.isDirectory()) {
2613
2841
  await walk(full, false);
2614
2842
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -2786,7 +3014,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
2786
3014
  }
2787
3015
 
2788
3016
  // src/commands/init.ts
2789
- import { basename as basename3, relative, resolve as resolve5 } from "path";
3017
+ import { basename as basename4, relative, resolve as resolve5 } from "path";
2790
3018
  import {
2791
3019
  appendBasouGitignore,
2792
3020
  createManifest,
@@ -2825,7 +3053,7 @@ async function runInit(options, ctx = {}) {
2825
3053
  async function doRunInit(options, ctx) {
2826
3054
  const cwd = ctx.cwd ?? process.cwd();
2827
3055
  const repositoryRoot = await resolveRepositoryRootForInit(cwd);
2828
- const workspaceName = options.name ?? basename3(repositoryRoot);
3056
+ const workspaceName = options.name ?? basename4(repositoryRoot);
2829
3057
  let repositoryUrl;
2830
3058
  if (options.repoUrl !== void 0) {
2831
3059
  repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
@@ -3050,13 +3278,13 @@ import {
3050
3278
  } from "@basou/core";
3051
3279
 
3052
3280
  // src/lib/hosts-config.ts
3053
- import { homedir as homedir5 } from "os";
3054
- import { isAbsolute as isAbsolute2, join as join6, resolve as resolve6 } from "path";
3281
+ import { homedir as homedir6 } from "os";
3282
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve6 } from "path";
3055
3283
  import { readYamlFile as readYamlFile4 } from "@basou/core";
3056
- var DEFAULT_HOSTS_CONFIG_PATH = join6(homedir5(), ".basou", "hosts.yaml");
3284
+ var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
3057
3285
  function expandTilde2(p) {
3058
- if (p === "~") return homedir5();
3059
- if (p.startsWith("~/")) return join6(homedir5(), p.slice(2));
3286
+ if (p === "~") return homedir6();
3287
+ if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
3060
3288
  return p;
3061
3289
  }
3062
3290
  function isRecord2(value) {
@@ -3382,7 +3610,7 @@ import {
3382
3610
  writeFileSync,
3383
3611
  writeSync
3384
3612
  } from "fs";
3385
- import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
3613
+ import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve7 } from "path";
3386
3614
  import {
3387
3615
  appendBasouGitignore as appendBasouGitignore2,
3388
3616
  basouPaths as basouPaths10,
@@ -3391,6 +3619,7 @@ import {
3391
3619
  ensureBasouDirectory as ensureBasouDirectory2,
3392
3620
  GENERATED_END,
3393
3621
  GENERATED_START,
3622
+ instructionMode,
3394
3623
  isGitNotFound,
3395
3624
  parseMarkers,
3396
3625
  pathBasename,
@@ -3685,7 +3914,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
3685
3914
  } catch {
3686
3915
  return { path: declaredPath, kind: "unresolved" };
3687
3916
  }
3688
- return { path: declaredPath, kind: existsSync(join7(real, ".git")) ? "repo" : "non-repo" };
3917
+ return { path: declaredPath, kind: existsSync(join9(real, ".git")) ? "repo" : "non-repo" };
3689
3918
  }
3690
3919
  async function doRunProjectAdopt(options, ctx) {
3691
3920
  const cwd = ctx.cwd ?? process.cwd();
@@ -3780,7 +4009,8 @@ async function isTrackedByGit(repoRoot, relPath) {
3780
4009
  async function gatherRepoWiring(repositoryRoot, entry) {
3781
4010
  const base = {
3782
4011
  path: entry.path,
3783
- ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
4012
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
4013
+ ...instructionMode(entry) === "self" ? { self: true } : {}
3784
4014
  };
3785
4015
  let real;
3786
4016
  try {
@@ -3788,7 +4018,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3788
4018
  } catch {
3789
4019
  return { ...base, reachable: false, instructionFiles: [] };
3790
4020
  }
3791
- if (!existsSync(join7(real, ".git"))) {
4021
+ if (!existsSync(join9(real, ".git"))) {
3792
4022
  return { ...base, reachable: false, instructionFiles: [] };
3793
4023
  }
3794
4024
  try {
@@ -3796,7 +4026,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3796
4026
  for (const name of INSTRUCTION_FILES) {
3797
4027
  let present = true;
3798
4028
  try {
3799
- lstatSync(join7(real, name));
4029
+ lstatSync(join9(real, name));
3800
4030
  } catch {
3801
4031
  present = false;
3802
4032
  }
@@ -3863,6 +4093,13 @@ function renderProjectWiring(result) {
3863
4093
  for (const p of result.unknown) lines.push(`- ${p}`);
3864
4094
  lines.push("");
3865
4095
  }
4096
+ if (result.self.length > 0) {
4097
+ lines.push(
4098
+ `## instructions: self (${result.self.length}) \u2014 committed instruction files are intentional (no leak risk)`
4099
+ );
4100
+ for (const p of result.self) lines.push(`- ${p}`);
4101
+ lines.push("");
4102
+ }
3866
4103
  if (result.incomplete.length > 0) {
3867
4104
  lines.push(
3868
4105
  `## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
@@ -3891,7 +4128,8 @@ async function runProjectGitignore(options, ctx = {}) {
3891
4128
  function gatherRepoGitignore(repositoryRoot, entry) {
3892
4129
  const base = {
3893
4130
  path: entry.path,
3894
- ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
4131
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
4132
+ ...instructionMode(entry) === "self" ? { self: true } : {}
3895
4133
  };
3896
4134
  let real;
3897
4135
  try {
@@ -3899,10 +4137,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
3899
4137
  } catch {
3900
4138
  return { ...base, reachable: false, currentLines: [] };
3901
4139
  }
3902
- if (!existsSync(join7(real, ".git"))) {
4140
+ if (!existsSync(join9(real, ".git"))) {
3903
4141
  return { ...base, reachable: false, currentLines: [] };
3904
4142
  }
3905
- return { ...base, reachable: true, currentLines: readGitignoreLines(join7(real, ".gitignore")) };
4143
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join9(real, ".gitignore")) };
3906
4144
  }
3907
4145
  function hasErrorCode(error) {
3908
4146
  return error instanceof Error && typeof error.code === "string";
@@ -3916,7 +4154,7 @@ function readGitignoreLines(file) {
3916
4154
  }
3917
4155
  }
3918
4156
  function applyGitignorePlan(repositoryRoot, plan) {
3919
- const file = join7(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4157
+ const file = join9(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
3920
4158
  let existing = "";
3921
4159
  try {
3922
4160
  existing = readFileSync(file, "utf8");
@@ -3986,6 +4224,13 @@ function renderProjectGitignore(result) {
3986
4224
  for (const p of result.unknown) lines.push(`- ${p}`);
3987
4225
  lines.push("");
3988
4226
  }
4227
+ if (result.self.length > 0) {
4228
+ lines.push(
4229
+ `## instructions: self (${result.self.length}) \u2014 skipped by design; their committed instruction files are shared, never gitignored`
4230
+ );
4231
+ for (const p of result.self) lines.push(`- ${p}`);
4232
+ lines.push("");
4233
+ }
3989
4234
  if (result.unreachable.length > 0) {
3990
4235
  lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
3991
4236
  for (const p of result.unreachable) lines.push(`- ${p}`);
@@ -4007,12 +4252,13 @@ async function runProjectSymlinks(options, ctx = {}) {
4007
4252
  process.exitCode = 1;
4008
4253
  }
4009
4254
  }
4010
- function expectedSymlinkTargets(repoDirReal, canonicalFile) {
4011
- return [
4012
- { name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) },
4255
+ function expectedSymlinkTargets(repoDirReal, canonicalFile, mode = "hub") {
4256
+ const spokes = [
4013
4257
  { name: "CLAUDE.md", target: CANONICAL_FILE },
4014
4258
  { name: ".github/copilot-instructions.md", target: `../${CANONICAL_FILE}` }
4015
4259
  ];
4260
+ if (mode === "self") return spokes;
4261
+ return [{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) }, ...spokes];
4016
4262
  }
4017
4263
  function inspectSymlink(filePath, expectedTarget) {
4018
4264
  let isLink;
@@ -4027,7 +4273,9 @@ function inspectSymlink(filePath, expectedTarget) {
4027
4273
  return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
4028
4274
  }
4029
4275
  function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4030
- const base = { path: entry.path };
4276
+ const mode = instructionMode(entry);
4277
+ const isSelf = mode === "self";
4278
+ const base = { path: entry.path, ...isSelf ? { self: true } : {} };
4031
4279
  let real;
4032
4280
  try {
4033
4281
  real = realpathSync(resolve7(repositoryRoot, entry.path));
@@ -4037,16 +4285,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4037
4285
  if (real === anchorReal) {
4038
4286
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
4039
4287
  }
4040
- if (!existsSync(join7(real, ".git"))) {
4288
+ if (!existsSync(join9(real, ".git"))) {
4041
4289
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4042
4290
  }
4043
- const canonicalFile = join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
4291
+ const canonicalFile = isSelf ? join9(real, CANONICAL_FILE) : join9(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4044
4292
  if (!existsSync(canonicalFile)) {
4045
4293
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
4046
4294
  }
4047
- const files = expectedSymlinkTargets(real, canonicalFile).map(
4295
+ const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
4048
4296
  (spec) => {
4049
- const { state, actualTarget } = inspectSymlink(join7(real, spec.name), spec.target);
4297
+ const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
4050
4298
  return {
4051
4299
  name: spec.name,
4052
4300
  expectedTarget: spec.target,
@@ -4060,7 +4308,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4060
4308
  isAnchor: false,
4061
4309
  reachable: true,
4062
4310
  canonicalPresent: true,
4063
- canonicalName: basename4(real),
4311
+ canonicalName: basename5(real),
4064
4312
  files
4065
4313
  };
4066
4314
  }
@@ -4075,9 +4323,9 @@ function applySymlinkPlan(repositoryRoot, plan) {
4075
4323
  const created = [];
4076
4324
  const failed = [];
4077
4325
  for (const { name, target } of plan.toCreate) {
4078
- const filePath = join7(real, name);
4326
+ const filePath = join9(real, name);
4079
4327
  try {
4080
- mkdirSync(dirname2(filePath), { recursive: true });
4328
+ mkdirSync(dirname3(filePath), { recursive: true });
4081
4329
  symlinkSync(target, filePath);
4082
4330
  created.push(name);
4083
4331
  } catch (error) {
@@ -4197,6 +4445,13 @@ function renderProjectSymlinks(result) {
4197
4445
  for (const p of result.missingCanonical) lines.push(`- ${p}`);
4198
4446
  lines.push("");
4199
4447
  }
4448
+ if (result.selfAgentsMissing.length > 0) {
4449
+ lines.push(
4450
+ `## AGENTS.md missing (${result.selfAgentsMissing.length}) \u2014 these \`instructions: self\` repos have no committed AGENTS.md yet; author it, then re-run to wire the spokes`
4451
+ );
4452
+ for (const p of result.selfAgentsMissing) lines.push(`- ${p}`);
4453
+ lines.push("");
4454
+ }
4200
4455
  if (result.unreachable.length > 0) {
4201
4456
  lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4202
4457
  for (const p of result.unreachable) lines.push(`- ${p}`);
@@ -4221,7 +4476,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
4221
4476
  return realpathSync(abs);
4222
4477
  } catch {
4223
4478
  try {
4224
- return join7(realpathSync(dirname2(abs)), basename4(abs));
4479
+ return join9(realpathSync(dirname3(abs)), basename5(abs));
4225
4480
  } catch {
4226
4481
  return abs;
4227
4482
  }
@@ -4238,8 +4493,8 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
4238
4493
  if (expectedTarget === "" || expectedTarget === ".") {
4239
4494
  return { path: entry.path, reachable: false };
4240
4495
  }
4241
- const linkName = basename4(repoReal);
4242
- const { state, actualTarget } = inspectSymlink(join7(viewDir, linkName), expectedTarget);
4496
+ const linkName = basename5(repoReal);
4497
+ const { state, actualTarget } = inspectSymlink(join9(viewDir, linkName), expectedTarget);
4243
4498
  return {
4244
4499
  path: entry.path,
4245
4500
  reachable: true,
@@ -4253,9 +4508,9 @@ function applyViewPlan(viewDir, toCreate) {
4253
4508
  const created = [];
4254
4509
  const failed = [];
4255
4510
  for (const { name, target } of toCreate) {
4256
- const filePath = join7(viewDir, name);
4511
+ const filePath = join9(viewDir, name);
4257
4512
  try {
4258
- mkdirSync(dirname2(filePath), { recursive: true });
4513
+ mkdirSync(dirname3(filePath), { recursive: true });
4259
4514
  symlinkSync(target, filePath);
4260
4515
  created.push(name);
4261
4516
  } catch (error) {
@@ -4268,7 +4523,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
4268
4523
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
4269
4524
  );
4270
4525
  function classifyViewLink(viewDir, name, rosterRealpaths) {
4271
- const filePath = join7(viewDir, name);
4526
+ const filePath = join9(viewDir, name);
4272
4527
  let isLink;
4273
4528
  try {
4274
4529
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -4297,7 +4552,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
4297
4552
  if (!isDir) {
4298
4553
  return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
4299
4554
  }
4300
- return { target, kind: existsSync(join7(resolved, ".git")) ? "repo" : "non-repo" };
4555
+ return { target, kind: existsSync(join9(resolved, ".git")) ? "repo" : "non-repo" };
4301
4556
  }
4302
4557
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4303
4558
  let names;
@@ -4322,7 +4577,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4322
4577
  const pruned = [];
4323
4578
  const failed = [];
4324
4579
  for (const { name } of toPrune) {
4325
- const filePath = join7(viewDir, name);
4580
+ const filePath = join9(viewDir, name);
4326
4581
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
4327
4582
  if (c === null || c.kind !== "repo") {
4328
4583
  failed.push({
@@ -4368,7 +4623,7 @@ async function doRunProjectWorkspace(options, ctx) {
4368
4623
  } else {
4369
4624
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
4370
4625
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
4371
- const rosterNames = roster.map((entry) => basename4(resolve7(repositoryRoot, entry.path)));
4626
+ const rosterNames = roster.map((entry) => basename5(resolve7(repositoryRoot, entry.path)));
4372
4627
  const rosterRealpaths = /* @__PURE__ */ new Set();
4373
4628
  for (const entry of roster) {
4374
4629
  try {
@@ -4535,10 +4790,10 @@ async function runProjectPreset(options, ctx = {}) {
4535
4790
  }
4536
4791
  }
4537
4792
  function canonicalFileFor(anchorReal, canonicalName) {
4538
- return join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4793
+ return join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4539
4794
  }
4540
4795
  function canonicalLabelFor(canonicalName) {
4541
- return join7("agents", canonicalName, CANONICAL_FILE);
4796
+ return join9("agents", canonicalName, CANONICAL_FILE);
4542
4797
  }
4543
4798
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4544
4799
  const declared = {
@@ -4547,6 +4802,9 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4547
4802
  ...entry.language !== void 0 ? { language: entry.language } : {},
4548
4803
  ...entry.publishes !== void 0 ? { publishes: entry.publishes } : {}
4549
4804
  };
4805
+ if (instructionMode(entry) === "self") {
4806
+ return { ...declared, self: true, isAnchor: false, reachable: true, canonicalPresent: false };
4807
+ }
4550
4808
  let real;
4551
4809
  try {
4552
4810
  real = realpathSync(resolve7(repositoryRoot, entry.path));
@@ -4556,10 +4814,10 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4556
4814
  if (real === anchorReal) {
4557
4815
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
4558
4816
  }
4559
- if (!existsSync(join7(real, ".git"))) {
4817
+ if (!existsSync(join9(real, ".git"))) {
4560
4818
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
4561
4819
  }
4562
- const canonicalName = basename4(real);
4820
+ const canonicalName = basename5(real);
4563
4821
  let content;
4564
4822
  try {
4565
4823
  content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
@@ -4604,7 +4862,7 @@ async function applyPresetPlan(anchorReal, plan) {
4604
4862
  isLink = false;
4605
4863
  }
4606
4864
  if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
4607
- if (plan.action === "create") mkdirSync(dirname2(file), { recursive: true });
4865
+ if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
4608
4866
  const existing = await readMarkdownFile4(file);
4609
4867
  await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
4610
4868
  }
@@ -4754,6 +5012,13 @@ function renderProjectPreset(result) {
4754
5012
  for (const p of result.anchors) lines.push(`- ${p}`);
4755
5013
  lines.push("");
4756
5014
  }
5015
+ if (result.self.length > 0) {
5016
+ lines.push(
5017
+ `## instructions: self (${result.self.length}) \u2014 hands-off; their hand-authored AGENTS.md is never written by basou`
5018
+ );
5019
+ for (const p of result.self) lines.push(`- ${p}`);
5020
+ lines.push("");
5021
+ }
4757
5022
  if (result.unreachable.length > 0) {
4758
5023
  lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4759
5024
  for (const p of result.unreachable) lines.push(`- ${p}`);
@@ -4787,28 +5052,28 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4787
5052
  return empty;
4788
5053
  }
4789
5054
  const anchorReal = realpathSync(repositoryRoot);
4790
- const canonicalName = basename4(real);
5055
+ const canonicalName = basename5(real);
4791
5056
  const instructionFiles = [];
4792
5057
  for (const name of INSTRUCTION_FILES) {
4793
5058
  try {
4794
- lstatSync(join7(real, name));
5059
+ lstatSync(join9(real, name));
4795
5060
  instructionFiles.push(name);
4796
5061
  } catch {
4797
5062
  }
4798
5063
  }
4799
5064
  let ignored;
4800
5065
  try {
4801
- ignored = new Set(readGitignoreLines(join7(real, ".gitignore")).map((l) => l.trim()));
5066
+ ignored = new Set(readGitignoreLines(join9(real, ".gitignore")).map((l) => l.trim()));
4802
5067
  } catch {
4803
5068
  ignored = /* @__PURE__ */ new Set();
4804
5069
  }
4805
5070
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
4806
- const canonical2 = existsSync(join7(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5071
+ const canonical2 = existsSync(join9(anchorReal, "agents", canonicalName, CANONICAL_FILE));
4807
5072
  let viewLink = false;
4808
5073
  const viewPath = manifest.workspace.view;
4809
5074
  if (viewPath !== void 0) {
4810
5075
  try {
4811
- lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5076
+ lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), canonicalName));
4812
5077
  viewLink = true;
4813
5078
  } catch {
4814
5079
  }
@@ -4822,11 +5087,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4822
5087
  };
4823
5088
  }
4824
5089
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
4825
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5090
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4826
5091
  return expectedSymlinkTargets(repoReal, canonicalFile);
4827
5092
  }
4828
5093
  function viewLinkPointsAt(viewDir, name, repoReal) {
4829
- const filePath = join7(viewDir, name);
5094
+ const filePath = join9(viewDir, name);
4830
5095
  try {
4831
5096
  if (!lstatSync(filePath).isSymbolicLink()) return false;
4832
5097
  const target = readlinkSync(filePath);
@@ -4837,7 +5102,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
4837
5102
  }
4838
5103
  }
4839
5104
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
4840
- const filePath = join7(viewDir, name);
5105
+ const filePath = join9(viewDir, name);
4841
5106
  try {
4842
5107
  if (!lstatSync(filePath).isSymbolicLink()) return false;
4843
5108
  const target = readlinkSync(filePath);
@@ -4857,15 +5122,17 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4857
5122
  }
4858
5123
  const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
4859
5124
  const targetAbs = resolve7(repositoryRoot, target);
4860
- const canonicalName = basename4(repoReal ?? targetAbs);
5125
+ const canonicalName = basename5(repoReal ?? targetAbs);
4861
5126
  const roster = manifest.repos ?? [];
4862
- const inRoster = roster.some((r) => {
5127
+ const declaredEntry = roster.find((r) => {
4863
5128
  try {
4864
5129
  return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
4865
5130
  } catch {
4866
5131
  return resolve7(repositoryRoot, r.path) === targetAbs;
4867
5132
  }
4868
5133
  });
5134
+ const inRoster = declaredEntry !== void 0;
5135
+ const isSelf = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
4869
5136
  const cnFold = canonicalName.toLowerCase();
4870
5137
  const canonicalShared = roster.some((r) => {
4871
5138
  let rReal = null;
@@ -4876,17 +5143,27 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4876
5143
  }
4877
5144
  if (rReal !== null) {
4878
5145
  if (repoReal !== void 0 && rReal === repoReal) return false;
4879
- return basename4(rReal).toLowerCase() === cnFold;
5146
+ return basename5(rReal).toLowerCase() === cnFold;
4880
5147
  }
4881
5148
  if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
4882
- return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
5149
+ return basename5(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
4883
5150
  });
4884
5151
  const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
4885
5152
  const items = [];
4886
5153
  if (!isAnchor) {
4887
5154
  if (repoReal !== void 0) {
4888
5155
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
4889
- const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
5156
+ const { state, actualTarget } = inspectSymlink(join9(repoReal, spec.name), spec.target);
5157
+ if (isSelf) {
5158
+ if (state !== "missing")
5159
+ items.push({
5160
+ kind: "instruction-symlink",
5161
+ label: spec.name,
5162
+ state: "foreign",
5163
+ note: "instructions: self \u2014 committed, left untouched"
5164
+ });
5165
+ continue;
5166
+ }
4890
5167
  if (state === "correct")
4891
5168
  items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
4892
5169
  else if (state === "mismatch")
@@ -4913,7 +5190,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4913
5190
  }
4914
5191
  let ignored;
4915
5192
  try {
4916
- ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
5193
+ ignored = new Set(readGitignoreLines(join9(repoReal, ".gitignore")).map((l) => l.trim()));
4917
5194
  for (const p of INSTRUCTION_FILES) {
4918
5195
  if (ignored.has(p) || ignored.has(`/${p}`)) {
4919
5196
  items.push({
@@ -4936,7 +5213,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4936
5213
  const viewPath = manifest.workspace.view;
4937
5214
  if (viewPath !== void 0) {
4938
5215
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
4939
- const linkPath = join7(viewDir, canonicalName);
5216
+ const linkPath = join9(viewDir, canonicalName);
4940
5217
  let isLink = false;
4941
5218
  try {
4942
5219
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -4962,8 +5239,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4962
5239
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
4963
5240
  }
4964
5241
  }
4965
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4966
- const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
5242
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5243
+ const canonicalLabel = join9("agents", canonicalName, CANONICAL_FILE);
4967
5244
  let canonicalIsLink = false;
4968
5245
  try {
4969
5246
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -5067,12 +5344,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5067
5344
  );
5068
5345
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5069
5346
  const expected = expectedByName.get(item.label);
5070
- if (repoReal === null || expected === void 0 || inspectSymlink(join7(repoReal, item.label), expected).state !== "correct") {
5347
+ if (repoReal === null || expected === void 0 || inspectSymlink(join9(repoReal, item.label), expected).state !== "correct") {
5071
5348
  changed(item.label);
5072
5349
  continue;
5073
5350
  }
5074
5351
  try {
5075
- unlinkSync(join7(repoReal, item.label));
5352
+ unlinkSync(join9(repoReal, item.label));
5076
5353
  removed.push(item.label);
5077
5354
  } catch (error) {
5078
5355
  failed.push({ label: item.label, message: failureReason(error) });
@@ -5091,7 +5368,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5091
5368
  continue;
5092
5369
  }
5093
5370
  try {
5094
- unlinkSync(join7(viewDir, item.label));
5371
+ unlinkSync(join9(viewDir, item.label));
5095
5372
  removed.push(`view/${item.label}`);
5096
5373
  } catch (error) {
5097
5374
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -5099,7 +5376,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5099
5376
  }
5100
5377
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5101
5378
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5102
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5379
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5103
5380
  try {
5104
5381
  if (lstatSync(canonicalFile).isSymbolicLink()) {
5105
5382
  changed(item.label);
@@ -5334,7 +5611,7 @@ function renderProjectArchive(result) {
5334
5611
  if (t.gitignorePatterns.length > 0)
5335
5612
  items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
5336
5613
  if (t.canonical)
5337
- items.push(`the anchor's canonical (agents/${basename4(result.target)}/AGENTS.md)`);
5614
+ items.push(`the anchor's canonical (agents/${basename5(result.target)}/AGENTS.md)`);
5338
5615
  if (!t.inspected) {
5339
5616
  lines.push(
5340
5617
  "## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
@@ -5373,12 +5650,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
5373
5650
  } catch {
5374
5651
  return { canonicalDirOld: false, viewLinkOld: false };
5375
5652
  }
5376
- const canonicalDirOld = existsSync(join7(anchorReal, "agents", oldBasename));
5653
+ const canonicalDirOld = existsSync(join9(anchorReal, "agents", oldBasename));
5377
5654
  let viewLinkOld = false;
5378
5655
  const viewPath = manifest.workspace.view;
5379
5656
  if (viewPath !== void 0) {
5380
5657
  try {
5381
- lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), oldBasename));
5658
+ lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), oldBasename));
5382
5659
  viewLinkOld = true;
5383
5660
  } catch {
5384
5661
  }
@@ -5537,7 +5814,7 @@ async function resolveRepositoryRootForNew(cwd) {
5537
5814
  async function doRunProjectNew(repos, options, ctx) {
5538
5815
  const cwd = ctx.cwd ?? process.cwd();
5539
5816
  const repositoryRoot = await resolveRepositoryRootForNew(cwd);
5540
- const workspaceName = basename4(repositoryRoot);
5817
+ const workspaceName = basename5(repositoryRoot);
5541
5818
  const declared = repos.map((p) => {
5542
5819
  const abs = resolve7(cwd, p);
5543
5820
  let real;
@@ -5719,7 +5996,7 @@ function regularFileSpokes(repoReal) {
5719
5996
  const out = [];
5720
5997
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
5721
5998
  try {
5722
- const st = lstatSync(join7(repoReal, spoke));
5999
+ const st = lstatSync(join9(repoReal, spoke));
5723
6000
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
5724
6001
  } catch {
5725
6002
  }
@@ -5735,7 +6012,7 @@ function pathPresent(p) {
5735
6012
  }
5736
6013
  }
5737
6014
  function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
5738
- const declared = roster.some((entry) => {
6015
+ const declaredEntry = roster.find((entry) => {
5739
6016
  const entryAbs = resolve7(repositoryRoot, entry.path);
5740
6017
  if (argReal !== void 0) {
5741
6018
  try {
@@ -5745,13 +6022,16 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5745
6022
  }
5746
6023
  return entryAbs === argAbs;
5747
6024
  });
6025
+ const declared = declaredEntry !== void 0;
6026
+ const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
5748
6027
  const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
5749
6028
  const path = displayRel === "" ? "." : displayRel;
5750
- const canonicalName = basename4(argReal ?? argAbs);
6029
+ const canonicalName = basename5(argReal ?? argAbs);
5751
6030
  if (argReal === void 0) {
5752
6031
  return {
5753
6032
  path,
5754
6033
  declared,
6034
+ ...self ? { self: true } : {},
5755
6035
  isAnchor: false,
5756
6036
  reachable: false,
5757
6037
  canonicalName,
@@ -5761,23 +6041,24 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5761
6041
  };
5762
6042
  }
5763
6043
  const isAnchor = argReal === anchorReal;
5764
- const reachable = existsSync(join7(argReal, ".git"));
5765
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6044
+ const reachable = existsSync(join9(argReal, ".git"));
6045
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5766
6046
  return {
5767
6047
  path,
5768
6048
  declared,
6049
+ ...self ? { self: true } : {},
5769
6050
  isAnchor,
5770
6051
  reachable,
5771
6052
  canonicalName,
5772
- agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
6053
+ agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
5773
6054
  canonicalExists: pathPresent(canonicalFile),
5774
6055
  regularSpokes: regularFileSpokes(argReal)
5775
6056
  };
5776
6057
  }
5777
6058
  function relocateAgentsFile(repoReal, canonicalFile) {
5778
- const agentsFile = join7(repoReal, CANONICAL_FILE);
6059
+ const agentsFile = join9(repoReal, CANONICAL_FILE);
5779
6060
  try {
5780
- mkdirSync(dirname2(canonicalFile), { recursive: true });
6061
+ mkdirSync(dirname3(canonicalFile), { recursive: true });
5781
6062
  } catch (error) {
5782
6063
  return { ok: false, message: failureReason(error), partial: false };
5783
6064
  }
@@ -5815,7 +6096,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
5815
6096
  let failure;
5816
6097
  let partial = false;
5817
6098
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
5818
- const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6099
+ const canonicalFile = join9(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
5819
6100
  const res = relocateAgentsFile(argReal, canonicalFile);
5820
6101
  if (res.ok) {
5821
6102
  applied = true;
@@ -5868,6 +6149,10 @@ function renderProjectRetrofit(result) {
5868
6149
  lines.push(
5869
6150
  `\u2139\uFE0F \`${result.path}\` is not declared in the roster (manifest \`repos\`). Add it first with \`basou project new\` / \`basou project adopt\`, then re-run.`
5870
6151
  );
6152
+ } else if (result.reason === "self") {
6153
+ lines.push(
6154
+ `\u2139\uFE0F \`${result.path}\` declares \`instructions: self\` \u2014 its \`${CANONICAL_FILE}\` is a hand-authored committed file that stays in the repo, so there is no anchor canonical to relocate it to. Retrofit does not apply (its CLAUDE.md / Copilot spokes are wired by \`basou project symlinks\`).`
6155
+ );
5871
6156
  } else if (result.reason === "anchor") {
5872
6157
  lines.push(
5873
6158
  `\u26A0\uFE0F \`${result.path}\` is the anchor (the project root). It owns its canonical directly \u2014 there is nothing to relocate.`
@@ -5943,70 +6228,17 @@ import {
5943
6228
  removeMarkerSection as removeMarkerSection2
5944
6229
  } from "@basou/core";
5945
6230
 
5946
- // src/lib/durable-write.ts
5947
- import { randomUUID } from "crypto";
5948
- import { lstat, open as open2, rename, stat as stat4, unlink as unlink2 } from "fs/promises";
5949
- import { basename as basename5, dirname as dirname3, join as join8 } from "path";
5950
- async function assertNotSymlink(targetPath) {
5951
- try {
5952
- const st = await lstat(targetPath);
5953
- if (st.isSymbolicLink()) {
5954
- throw new Error(
5955
- "Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
5956
- );
5957
- }
5958
- } catch (error) {
5959
- if (error instanceof Error && error.code === "ENOENT") return;
5960
- throw error;
5961
- }
5962
- }
5963
- async function writeFileDurable(targetPath, content) {
5964
- const dir = dirname3(targetPath);
5965
- const tmpPath = join8(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
5966
- let mode = 420;
5967
- try {
5968
- mode = (await stat4(targetPath)).mode & 511;
5969
- } catch (error) {
5970
- if (!(error instanceof Error && error.code === "ENOENT")) {
5971
- throw error;
5972
- }
5973
- }
5974
- let handle;
5975
- try {
5976
- handle = await open2(tmpPath, "wx", mode);
5977
- await handle.writeFile(content, "utf8");
5978
- await handle.chmod(mode);
5979
- await handle.sync();
5980
- await handle.close();
5981
- handle = void 0;
5982
- await rename(tmpPath, targetPath);
5983
- } catch (error) {
5984
- if (handle) await handle.close().catch(() => void 0);
5985
- await unlink2(tmpPath).catch(() => void 0);
5986
- throw error;
5987
- }
5988
- try {
5989
- const dirHandle = await open2(dir, "r");
5990
- try {
5991
- await dirHandle.sync();
5992
- } finally {
5993
- await dirHandle.close();
5994
- }
5995
- } catch {
5996
- }
5997
- }
5998
-
5999
6231
  // src/lib/protocols-config.ts
6000
- import { homedir as homedir6 } from "os";
6001
- import { isAbsolute as isAbsolute4, join as join9, resolve as resolve8 } from "path";
6232
+ import { homedir as homedir7 } from "os";
6233
+ import { isAbsolute as isAbsolute4, join as join10, resolve as resolve8 } from "path";
6002
6234
  import { readYamlFile as readYamlFile5 } from "@basou/core";
6003
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join9(homedir6(), ".basou", "protocols.yaml");
6004
- var DEFAULT_TARGET_PATH = join9(homedir6(), ".claude", "CLAUDE.md");
6235
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join10(homedir7(), ".basou", "protocols.yaml");
6236
+ var DEFAULT_TARGET_PATH = join10(homedir7(), ".claude", "CLAUDE.md");
6005
6237
  var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
6006
6238
  var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
6007
6239
  function expandTilde3(p) {
6008
- if (p === "~") return homedir6();
6009
- if (p.startsWith("~/")) return join9(homedir6(), p.slice(2));
6240
+ if (p === "~") return homedir7();
6241
+ if (p.startsWith("~/")) return join10(homedir7(), p.slice(2));
6010
6242
  return p;
6011
6243
  }
6012
6244
  function isRecord3(value) {
@@ -6258,16 +6490,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
6258
6490
 
6259
6491
  // src/commands/refresh-watch.ts
6260
6492
  import { readdir as readdir2, stat as stat5 } from "fs/promises";
6261
- import { homedir as homedir7 } from "os";
6262
- import { join as join10 } from "path";
6493
+ import { homedir as homedir8 } from "os";
6494
+ import { join as join11 } from "path";
6263
6495
  import { findErrorCode as findErrorCode8 } from "@basou/core";
6264
6496
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
6265
6497
  var MIN_WATCH_INTERVAL_SEC = 5;
6266
6498
  var MAX_WATCH_INTERVAL_SEC = 86400;
6267
6499
  function watchedRoots(ctx) {
6268
6500
  return [
6269
- ctx.codexSessionsDir ?? join10(homedir7(), ".codex", "sessions"),
6270
- ctx.claudeProjectsDir ?? join10(homedir7(), ".claude", "projects")
6501
+ ctx.codexSessionsDir ?? join11(homedir8(), ".codex", "sessions"),
6502
+ ctx.claudeProjectsDir ?? join11(homedir8(), ".claude", "projects")
6271
6503
  ];
6272
6504
  }
6273
6505
  async function scanSourceLogs(roots) {
@@ -6281,7 +6513,7 @@ async function scanSourceLogs(roots) {
6281
6513
  throw new Error("Failed to read a source log directory", { cause: error });
6282
6514
  }
6283
6515
  for (const entry of entries) {
6284
- const full = join10(dir, entry.name);
6516
+ const full = join11(dir, entry.name);
6285
6517
  if (entry.isDirectory()) {
6286
6518
  await walk(full);
6287
6519
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -6802,8 +7034,8 @@ function renderReviewGaps(summary) {
6802
7034
 
6803
7035
  // src/commands/run.ts
6804
7036
  import { mkdir as mkdir2 } from "fs/promises";
6805
- import { homedir as homedir8 } from "os";
6806
- import { join as join11 } from "path";
7037
+ import { homedir as homedir9 } from "os";
7038
+ import { join as join12 } from "path";
6807
7039
  import {
6808
7040
  acquireLock as acquireLock5,
6809
7041
  assertBasouRootSafe as assertBasouRootSafe11,
@@ -6856,13 +7088,13 @@ async function runClaudeCode(args, options, ctx = {}) {
6856
7088
  await assertBasouRootSafe11(paths.root);
6857
7089
  const manifest = await readManifest7(paths);
6858
7090
  const sessionId = prefixedUlid4("ses");
6859
- const sessionDir = join11(paths.sessions, sessionId);
7091
+ const sessionDir = join12(paths.sessions, sessionId);
6860
7092
  await mkdir2(sessionDir, { recursive: true });
6861
7093
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
6862
7094
  await coreAppendChainedEvent2(paths, sessionId, event);
6863
7095
  });
6864
7096
  const startedAt = now().toISOString();
6865
- const sessionYamlPath = join11(sessionDir, "session.yaml");
7097
+ const sessionYamlPath = join12(sessionDir, "session.yaml");
6866
7098
  const session = buildInitialSession2({
6867
7099
  id: sessionId,
6868
7100
  command,
@@ -6988,7 +7220,7 @@ async function runClaudeCode(args, options, ctx = {}) {
6988
7220
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
6989
7221
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
6990
7222
  workingDirectory: repoRoot,
6991
- homedir: homedir8()
7223
+ homedir: homedir9()
6992
7224
  }).sanitized;
6993
7225
  const finalStatus = decideFinalStatus2(result, signalReceived);
6994
7226
  await appendEvent(sessionDir, {
@@ -7132,7 +7364,7 @@ function buildInitialSession2(input) {
7132
7364
  source: { ...claudeCodeAdapterMetadata },
7133
7365
  started_at: input.startedAt,
7134
7366
  status: "initialized",
7135
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir8() }),
7367
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir9() }),
7136
7368
  invocation: {
7137
7369
  command: input.command,
7138
7370
  args: [...input.args],
@@ -7205,7 +7437,7 @@ async function resolveRepositoryRootForRun(cwd) {
7205
7437
 
7206
7438
  // src/commands/session.ts
7207
7439
  import { readFile as readFile5 } from "fs/promises";
7208
- import { basename as basename6, isAbsolute as isAbsolute6, join as join12, relative as relative3 } from "path";
7440
+ import { basename as basename6, isAbsolute as isAbsolute6, join as join13, relative as relative3 } from "path";
7209
7441
  import {
7210
7442
  acquireLock as acquireLock6,
7211
7443
  appendEventToExistingSession as appendEventToExistingSession3,
@@ -7330,8 +7562,8 @@ async function doRunSessionShow(idInput, options, ctx) {
7330
7562
  const paths = basouPaths15(repositoryRoot);
7331
7563
  await assertWorkspaceInitialized10(paths.root);
7332
7564
  const sessionId = await resolveSessionId3(paths, idInput);
7333
- const sessionDir = join12(paths.sessions, sessionId);
7334
- const sessionYamlPath = join12(sessionDir, "session.yaml");
7565
+ const sessionDir = join13(paths.sessions, sessionId);
7566
+ const sessionYamlPath = join13(sessionDir, "session.yaml");
7335
7567
  let session;
7336
7568
  try {
7337
7569
  const raw = await readYamlFile7(sessionYamlPath);
@@ -8074,7 +8306,7 @@ async function resolveRepositoryRootForStatus(cwd) {
8074
8306
 
8075
8307
  // src/commands/task.ts
8076
8308
  import { readFile as readFile6 } from "fs/promises";
8077
- import { join as join13 } from "path";
8309
+ import { join as join14 } from "path";
8078
8310
  import {
8079
8311
  archiveTask,
8080
8312
  assertBasouRootSafe as assertBasouRootSafe15,
@@ -8400,7 +8632,7 @@ async function doRunTaskShow(idInput, options, ctx) {
8400
8632
  const events = [];
8401
8633
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
8402
8634
  for (const s of sessions) {
8403
- const sessionDir = join13(paths.sessions, s.sessionId);
8635
+ const sessionDir = join14(paths.sessions, s.sessionId);
8404
8636
  try {
8405
8637
  for await (const ev of replayEvents3(sessionDir, {
8406
8638
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -9306,7 +9538,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
9306
9538
  // src/lib/portfolio-safety.ts
9307
9539
  import { execFile } from "child_process";
9308
9540
  import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
9309
- import { isAbsolute as isAbsolute7, join as join14, relative as relative4, resolve as resolve10 } from "path";
9541
+ import { isAbsolute as isAbsolute7, join as join15, relative as relative4, resolve as resolve10 } from "path";
9310
9542
  import { promisify } from "util";
9311
9543
  import { readManifest as readManifest11 } from "@basou/core";
9312
9544
  var execFileAsync = promisify(execFile);
@@ -9330,7 +9562,7 @@ function isBasouPath(p) {
9330
9562
  async function inspectRepo(repoPath) {
9331
9563
  let hasEntry = false;
9332
9564
  try {
9333
- await lstat2(join14(repoPath, ".basou"));
9565
+ await lstat2(join15(repoPath, ".basou"));
9334
9566
  hasEntry = true;
9335
9567
  } catch (error) {
9336
9568
  if (errorCode(error) !== "ENOENT") {
@@ -9431,7 +9663,7 @@ function formatSafetyReport(result) {
9431
9663
 
9432
9664
  // src/lib/view-server.ts
9433
9665
  import { createServer } from "http";
9434
- import { join as join15 } from "path";
9666
+ import { join as join16 } from "path";
9435
9667
  import {
9436
9668
  computeWorkStats as computeWorkStats2,
9437
9669
  enumerateApprovals as enumerateApprovals2,
@@ -10390,7 +10622,7 @@ async function sessionDetail(ws, sessionId) {
10390
10622
  throw error;
10391
10623
  }
10392
10624
  try {
10393
- const events = await readAllEvents2(join15(ws.paths.sessions, sessionId));
10625
+ const events = await readAllEvents2(join16(ws.paths.sessions, sessionId));
10394
10626
  return { session, events };
10395
10627
  } catch {
10396
10628
  return { session, events: [], degraded: true };