@basou/cli 0.28.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,
@@ -3686,7 +3914,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
3686
3914
  } catch {
3687
3915
  return { path: declaredPath, kind: "unresolved" };
3688
3916
  }
3689
- return { path: declaredPath, kind: existsSync(join7(real, ".git")) ? "repo" : "non-repo" };
3917
+ return { path: declaredPath, kind: existsSync(join9(real, ".git")) ? "repo" : "non-repo" };
3690
3918
  }
3691
3919
  async function doRunProjectAdopt(options, ctx) {
3692
3920
  const cwd = ctx.cwd ?? process.cwd();
@@ -3790,7 +4018,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3790
4018
  } catch {
3791
4019
  return { ...base, reachable: false, instructionFiles: [] };
3792
4020
  }
3793
- if (!existsSync(join7(real, ".git"))) {
4021
+ if (!existsSync(join9(real, ".git"))) {
3794
4022
  return { ...base, reachable: false, instructionFiles: [] };
3795
4023
  }
3796
4024
  try {
@@ -3798,7 +4026,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3798
4026
  for (const name of INSTRUCTION_FILES) {
3799
4027
  let present = true;
3800
4028
  try {
3801
- lstatSync(join7(real, name));
4029
+ lstatSync(join9(real, name));
3802
4030
  } catch {
3803
4031
  present = false;
3804
4032
  }
@@ -3909,10 +4137,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
3909
4137
  } catch {
3910
4138
  return { ...base, reachable: false, currentLines: [] };
3911
4139
  }
3912
- if (!existsSync(join7(real, ".git"))) {
4140
+ if (!existsSync(join9(real, ".git"))) {
3913
4141
  return { ...base, reachable: false, currentLines: [] };
3914
4142
  }
3915
- return { ...base, reachable: true, currentLines: readGitignoreLines(join7(real, ".gitignore")) };
4143
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join9(real, ".gitignore")) };
3916
4144
  }
3917
4145
  function hasErrorCode(error) {
3918
4146
  return error instanceof Error && typeof error.code === "string";
@@ -3926,7 +4154,7 @@ function readGitignoreLines(file) {
3926
4154
  }
3927
4155
  }
3928
4156
  function applyGitignorePlan(repositoryRoot, plan) {
3929
- const file = join7(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4157
+ const file = join9(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
3930
4158
  let existing = "";
3931
4159
  try {
3932
4160
  existing = readFileSync(file, "utf8");
@@ -4057,16 +4285,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4057
4285
  if (real === anchorReal) {
4058
4286
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
4059
4287
  }
4060
- if (!existsSync(join7(real, ".git"))) {
4288
+ if (!existsSync(join9(real, ".git"))) {
4061
4289
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4062
4290
  }
4063
- const canonicalFile = isSelf ? join7(real, CANONICAL_FILE) : join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
4291
+ const canonicalFile = isSelf ? join9(real, CANONICAL_FILE) : join9(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4064
4292
  if (!existsSync(canonicalFile)) {
4065
4293
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
4066
4294
  }
4067
4295
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
4068
4296
  (spec) => {
4069
- const { state, actualTarget } = inspectSymlink(join7(real, spec.name), spec.target);
4297
+ const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
4070
4298
  return {
4071
4299
  name: spec.name,
4072
4300
  expectedTarget: spec.target,
@@ -4080,7 +4308,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4080
4308
  isAnchor: false,
4081
4309
  reachable: true,
4082
4310
  canonicalPresent: true,
4083
- canonicalName: basename4(real),
4311
+ canonicalName: basename5(real),
4084
4312
  files
4085
4313
  };
4086
4314
  }
@@ -4095,9 +4323,9 @@ function applySymlinkPlan(repositoryRoot, plan) {
4095
4323
  const created = [];
4096
4324
  const failed = [];
4097
4325
  for (const { name, target } of plan.toCreate) {
4098
- const filePath = join7(real, name);
4326
+ const filePath = join9(real, name);
4099
4327
  try {
4100
- mkdirSync(dirname2(filePath), { recursive: true });
4328
+ mkdirSync(dirname3(filePath), { recursive: true });
4101
4329
  symlinkSync(target, filePath);
4102
4330
  created.push(name);
4103
4331
  } catch (error) {
@@ -4248,7 +4476,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
4248
4476
  return realpathSync(abs);
4249
4477
  } catch {
4250
4478
  try {
4251
- return join7(realpathSync(dirname2(abs)), basename4(abs));
4479
+ return join9(realpathSync(dirname3(abs)), basename5(abs));
4252
4480
  } catch {
4253
4481
  return abs;
4254
4482
  }
@@ -4265,8 +4493,8 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
4265
4493
  if (expectedTarget === "" || expectedTarget === ".") {
4266
4494
  return { path: entry.path, reachable: false };
4267
4495
  }
4268
- const linkName = basename4(repoReal);
4269
- const { state, actualTarget } = inspectSymlink(join7(viewDir, linkName), expectedTarget);
4496
+ const linkName = basename5(repoReal);
4497
+ const { state, actualTarget } = inspectSymlink(join9(viewDir, linkName), expectedTarget);
4270
4498
  return {
4271
4499
  path: entry.path,
4272
4500
  reachable: true,
@@ -4280,9 +4508,9 @@ function applyViewPlan(viewDir, toCreate) {
4280
4508
  const created = [];
4281
4509
  const failed = [];
4282
4510
  for (const { name, target } of toCreate) {
4283
- const filePath = join7(viewDir, name);
4511
+ const filePath = join9(viewDir, name);
4284
4512
  try {
4285
- mkdirSync(dirname2(filePath), { recursive: true });
4513
+ mkdirSync(dirname3(filePath), { recursive: true });
4286
4514
  symlinkSync(target, filePath);
4287
4515
  created.push(name);
4288
4516
  } catch (error) {
@@ -4295,7 +4523,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
4295
4523
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
4296
4524
  );
4297
4525
  function classifyViewLink(viewDir, name, rosterRealpaths) {
4298
- const filePath = join7(viewDir, name);
4526
+ const filePath = join9(viewDir, name);
4299
4527
  let isLink;
4300
4528
  try {
4301
4529
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -4324,7 +4552,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
4324
4552
  if (!isDir) {
4325
4553
  return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
4326
4554
  }
4327
- return { target, kind: existsSync(join7(resolved, ".git")) ? "repo" : "non-repo" };
4555
+ return { target, kind: existsSync(join9(resolved, ".git")) ? "repo" : "non-repo" };
4328
4556
  }
4329
4557
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4330
4558
  let names;
@@ -4349,7 +4577,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4349
4577
  const pruned = [];
4350
4578
  const failed = [];
4351
4579
  for (const { name } of toPrune) {
4352
- const filePath = join7(viewDir, name);
4580
+ const filePath = join9(viewDir, name);
4353
4581
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
4354
4582
  if (c === null || c.kind !== "repo") {
4355
4583
  failed.push({
@@ -4395,7 +4623,7 @@ async function doRunProjectWorkspace(options, ctx) {
4395
4623
  } else {
4396
4624
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
4397
4625
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
4398
- const rosterNames = roster.map((entry) => basename4(resolve7(repositoryRoot, entry.path)));
4626
+ const rosterNames = roster.map((entry) => basename5(resolve7(repositoryRoot, entry.path)));
4399
4627
  const rosterRealpaths = /* @__PURE__ */ new Set();
4400
4628
  for (const entry of roster) {
4401
4629
  try {
@@ -4562,10 +4790,10 @@ async function runProjectPreset(options, ctx = {}) {
4562
4790
  }
4563
4791
  }
4564
4792
  function canonicalFileFor(anchorReal, canonicalName) {
4565
- return join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4793
+ return join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4566
4794
  }
4567
4795
  function canonicalLabelFor(canonicalName) {
4568
- return join7("agents", canonicalName, CANONICAL_FILE);
4796
+ return join9("agents", canonicalName, CANONICAL_FILE);
4569
4797
  }
4570
4798
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4571
4799
  const declared = {
@@ -4586,10 +4814,10 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4586
4814
  if (real === anchorReal) {
4587
4815
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
4588
4816
  }
4589
- if (!existsSync(join7(real, ".git"))) {
4817
+ if (!existsSync(join9(real, ".git"))) {
4590
4818
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
4591
4819
  }
4592
- const canonicalName = basename4(real);
4820
+ const canonicalName = basename5(real);
4593
4821
  let content;
4594
4822
  try {
4595
4823
  content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
@@ -4634,7 +4862,7 @@ async function applyPresetPlan(anchorReal, plan) {
4634
4862
  isLink = false;
4635
4863
  }
4636
4864
  if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
4637
- if (plan.action === "create") mkdirSync(dirname2(file), { recursive: true });
4865
+ if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
4638
4866
  const existing = await readMarkdownFile4(file);
4639
4867
  await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
4640
4868
  }
@@ -4824,28 +5052,28 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4824
5052
  return empty;
4825
5053
  }
4826
5054
  const anchorReal = realpathSync(repositoryRoot);
4827
- const canonicalName = basename4(real);
5055
+ const canonicalName = basename5(real);
4828
5056
  const instructionFiles = [];
4829
5057
  for (const name of INSTRUCTION_FILES) {
4830
5058
  try {
4831
- lstatSync(join7(real, name));
5059
+ lstatSync(join9(real, name));
4832
5060
  instructionFiles.push(name);
4833
5061
  } catch {
4834
5062
  }
4835
5063
  }
4836
5064
  let ignored;
4837
5065
  try {
4838
- ignored = new Set(readGitignoreLines(join7(real, ".gitignore")).map((l) => l.trim()));
5066
+ ignored = new Set(readGitignoreLines(join9(real, ".gitignore")).map((l) => l.trim()));
4839
5067
  } catch {
4840
5068
  ignored = /* @__PURE__ */ new Set();
4841
5069
  }
4842
5070
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
4843
- const canonical2 = existsSync(join7(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5071
+ const canonical2 = existsSync(join9(anchorReal, "agents", canonicalName, CANONICAL_FILE));
4844
5072
  let viewLink = false;
4845
5073
  const viewPath = manifest.workspace.view;
4846
5074
  if (viewPath !== void 0) {
4847
5075
  try {
4848
- lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5076
+ lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), canonicalName));
4849
5077
  viewLink = true;
4850
5078
  } catch {
4851
5079
  }
@@ -4859,11 +5087,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4859
5087
  };
4860
5088
  }
4861
5089
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
4862
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5090
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4863
5091
  return expectedSymlinkTargets(repoReal, canonicalFile);
4864
5092
  }
4865
5093
  function viewLinkPointsAt(viewDir, name, repoReal) {
4866
- const filePath = join7(viewDir, name);
5094
+ const filePath = join9(viewDir, name);
4867
5095
  try {
4868
5096
  if (!lstatSync(filePath).isSymbolicLink()) return false;
4869
5097
  const target = readlinkSync(filePath);
@@ -4874,7 +5102,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
4874
5102
  }
4875
5103
  }
4876
5104
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
4877
- const filePath = join7(viewDir, name);
5105
+ const filePath = join9(viewDir, name);
4878
5106
  try {
4879
5107
  if (!lstatSync(filePath).isSymbolicLink()) return false;
4880
5108
  const target = readlinkSync(filePath);
@@ -4894,7 +5122,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4894
5122
  }
4895
5123
  const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
4896
5124
  const targetAbs = resolve7(repositoryRoot, target);
4897
- const canonicalName = basename4(repoReal ?? targetAbs);
5125
+ const canonicalName = basename5(repoReal ?? targetAbs);
4898
5126
  const roster = manifest.repos ?? [];
4899
5127
  const declaredEntry = roster.find((r) => {
4900
5128
  try {
@@ -4915,17 +5143,17 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4915
5143
  }
4916
5144
  if (rReal !== null) {
4917
5145
  if (repoReal !== void 0 && rReal === repoReal) return false;
4918
- return basename4(rReal).toLowerCase() === cnFold;
5146
+ return basename5(rReal).toLowerCase() === cnFold;
4919
5147
  }
4920
5148
  if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
4921
- return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
5149
+ return basename5(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
4922
5150
  });
4923
5151
  const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
4924
5152
  const items = [];
4925
5153
  if (!isAnchor) {
4926
5154
  if (repoReal !== void 0) {
4927
5155
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
4928
- const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
5156
+ const { state, actualTarget } = inspectSymlink(join9(repoReal, spec.name), spec.target);
4929
5157
  if (isSelf) {
4930
5158
  if (state !== "missing")
4931
5159
  items.push({
@@ -4962,7 +5190,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4962
5190
  }
4963
5191
  let ignored;
4964
5192
  try {
4965
- ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
5193
+ ignored = new Set(readGitignoreLines(join9(repoReal, ".gitignore")).map((l) => l.trim()));
4966
5194
  for (const p of INSTRUCTION_FILES) {
4967
5195
  if (ignored.has(p) || ignored.has(`/${p}`)) {
4968
5196
  items.push({
@@ -4985,7 +5213,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4985
5213
  const viewPath = manifest.workspace.view;
4986
5214
  if (viewPath !== void 0) {
4987
5215
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
4988
- const linkPath = join7(viewDir, canonicalName);
5216
+ const linkPath = join9(viewDir, canonicalName);
4989
5217
  let isLink = false;
4990
5218
  try {
4991
5219
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -5011,8 +5239,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5011
5239
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
5012
5240
  }
5013
5241
  }
5014
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5015
- const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
5242
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5243
+ const canonicalLabel = join9("agents", canonicalName, CANONICAL_FILE);
5016
5244
  let canonicalIsLink = false;
5017
5245
  try {
5018
5246
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -5116,12 +5344,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5116
5344
  );
5117
5345
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5118
5346
  const expected = expectedByName.get(item.label);
5119
- 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") {
5120
5348
  changed(item.label);
5121
5349
  continue;
5122
5350
  }
5123
5351
  try {
5124
- unlinkSync(join7(repoReal, item.label));
5352
+ unlinkSync(join9(repoReal, item.label));
5125
5353
  removed.push(item.label);
5126
5354
  } catch (error) {
5127
5355
  failed.push({ label: item.label, message: failureReason(error) });
@@ -5140,7 +5368,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5140
5368
  continue;
5141
5369
  }
5142
5370
  try {
5143
- unlinkSync(join7(viewDir, item.label));
5371
+ unlinkSync(join9(viewDir, item.label));
5144
5372
  removed.push(`view/${item.label}`);
5145
5373
  } catch (error) {
5146
5374
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -5148,7 +5376,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5148
5376
  }
5149
5377
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5150
5378
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5151
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5379
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5152
5380
  try {
5153
5381
  if (lstatSync(canonicalFile).isSymbolicLink()) {
5154
5382
  changed(item.label);
@@ -5383,7 +5611,7 @@ function renderProjectArchive(result) {
5383
5611
  if (t.gitignorePatterns.length > 0)
5384
5612
  items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
5385
5613
  if (t.canonical)
5386
- 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)`);
5387
5615
  if (!t.inspected) {
5388
5616
  lines.push(
5389
5617
  "## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
@@ -5422,12 +5650,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
5422
5650
  } catch {
5423
5651
  return { canonicalDirOld: false, viewLinkOld: false };
5424
5652
  }
5425
- const canonicalDirOld = existsSync(join7(anchorReal, "agents", oldBasename));
5653
+ const canonicalDirOld = existsSync(join9(anchorReal, "agents", oldBasename));
5426
5654
  let viewLinkOld = false;
5427
5655
  const viewPath = manifest.workspace.view;
5428
5656
  if (viewPath !== void 0) {
5429
5657
  try {
5430
- lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), oldBasename));
5658
+ lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), oldBasename));
5431
5659
  viewLinkOld = true;
5432
5660
  } catch {
5433
5661
  }
@@ -5586,7 +5814,7 @@ async function resolveRepositoryRootForNew(cwd) {
5586
5814
  async function doRunProjectNew(repos, options, ctx) {
5587
5815
  const cwd = ctx.cwd ?? process.cwd();
5588
5816
  const repositoryRoot = await resolveRepositoryRootForNew(cwd);
5589
- const workspaceName = basename4(repositoryRoot);
5817
+ const workspaceName = basename5(repositoryRoot);
5590
5818
  const declared = repos.map((p) => {
5591
5819
  const abs = resolve7(cwd, p);
5592
5820
  let real;
@@ -5768,7 +5996,7 @@ function regularFileSpokes(repoReal) {
5768
5996
  const out = [];
5769
5997
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
5770
5998
  try {
5771
- const st = lstatSync(join7(repoReal, spoke));
5999
+ const st = lstatSync(join9(repoReal, spoke));
5772
6000
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
5773
6001
  } catch {
5774
6002
  }
@@ -5798,7 +6026,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5798
6026
  const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
5799
6027
  const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
5800
6028
  const path = displayRel === "" ? "." : displayRel;
5801
- const canonicalName = basename4(argReal ?? argAbs);
6029
+ const canonicalName = basename5(argReal ?? argAbs);
5802
6030
  if (argReal === void 0) {
5803
6031
  return {
5804
6032
  path,
@@ -5813,8 +6041,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5813
6041
  };
5814
6042
  }
5815
6043
  const isAnchor = argReal === anchorReal;
5816
- const reachable = existsSync(join7(argReal, ".git"));
5817
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6044
+ const reachable = existsSync(join9(argReal, ".git"));
6045
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5818
6046
  return {
5819
6047
  path,
5820
6048
  declared,
@@ -5822,15 +6050,15 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5822
6050
  isAnchor,
5823
6051
  reachable,
5824
6052
  canonicalName,
5825
- agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
6053
+ agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
5826
6054
  canonicalExists: pathPresent(canonicalFile),
5827
6055
  regularSpokes: regularFileSpokes(argReal)
5828
6056
  };
5829
6057
  }
5830
6058
  function relocateAgentsFile(repoReal, canonicalFile) {
5831
- const agentsFile = join7(repoReal, CANONICAL_FILE);
6059
+ const agentsFile = join9(repoReal, CANONICAL_FILE);
5832
6060
  try {
5833
- mkdirSync(dirname2(canonicalFile), { recursive: true });
6061
+ mkdirSync(dirname3(canonicalFile), { recursive: true });
5834
6062
  } catch (error) {
5835
6063
  return { ok: false, message: failureReason(error), partial: false };
5836
6064
  }
@@ -5868,7 +6096,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
5868
6096
  let failure;
5869
6097
  let partial = false;
5870
6098
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
5871
- const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6099
+ const canonicalFile = join9(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
5872
6100
  const res = relocateAgentsFile(argReal, canonicalFile);
5873
6101
  if (res.ok) {
5874
6102
  applied = true;
@@ -6000,70 +6228,17 @@ import {
6000
6228
  removeMarkerSection as removeMarkerSection2
6001
6229
  } from "@basou/core";
6002
6230
 
6003
- // src/lib/durable-write.ts
6004
- import { randomUUID } from "crypto";
6005
- import { lstat, open as open2, rename, stat as stat4, unlink as unlink2 } from "fs/promises";
6006
- import { basename as basename5, dirname as dirname3, join as join8 } from "path";
6007
- async function assertNotSymlink(targetPath) {
6008
- try {
6009
- const st = await lstat(targetPath);
6010
- if (st.isSymbolicLink()) {
6011
- throw new Error(
6012
- "Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
6013
- );
6014
- }
6015
- } catch (error) {
6016
- if (error instanceof Error && error.code === "ENOENT") return;
6017
- throw error;
6018
- }
6019
- }
6020
- async function writeFileDurable(targetPath, content) {
6021
- const dir = dirname3(targetPath);
6022
- const tmpPath = join8(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
6023
- let mode = 420;
6024
- try {
6025
- mode = (await stat4(targetPath)).mode & 511;
6026
- } catch (error) {
6027
- if (!(error instanceof Error && error.code === "ENOENT")) {
6028
- throw error;
6029
- }
6030
- }
6031
- let handle;
6032
- try {
6033
- handle = await open2(tmpPath, "wx", mode);
6034
- await handle.writeFile(content, "utf8");
6035
- await handle.chmod(mode);
6036
- await handle.sync();
6037
- await handle.close();
6038
- handle = void 0;
6039
- await rename(tmpPath, targetPath);
6040
- } catch (error) {
6041
- if (handle) await handle.close().catch(() => void 0);
6042
- await unlink2(tmpPath).catch(() => void 0);
6043
- throw error;
6044
- }
6045
- try {
6046
- const dirHandle = await open2(dir, "r");
6047
- try {
6048
- await dirHandle.sync();
6049
- } finally {
6050
- await dirHandle.close();
6051
- }
6052
- } catch {
6053
- }
6054
- }
6055
-
6056
6231
  // src/lib/protocols-config.ts
6057
- import { homedir as homedir6 } from "os";
6058
- 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";
6059
6234
  import { readYamlFile as readYamlFile5 } from "@basou/core";
6060
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join9(homedir6(), ".basou", "protocols.yaml");
6061
- 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");
6062
6237
  var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
6063
6238
  var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
6064
6239
  function expandTilde3(p) {
6065
- if (p === "~") return homedir6();
6066
- if (p.startsWith("~/")) return join9(homedir6(), p.slice(2));
6240
+ if (p === "~") return homedir7();
6241
+ if (p.startsWith("~/")) return join10(homedir7(), p.slice(2));
6067
6242
  return p;
6068
6243
  }
6069
6244
  function isRecord3(value) {
@@ -6315,16 +6490,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
6315
6490
 
6316
6491
  // src/commands/refresh-watch.ts
6317
6492
  import { readdir as readdir2, stat as stat5 } from "fs/promises";
6318
- import { homedir as homedir7 } from "os";
6319
- import { join as join10 } from "path";
6493
+ import { homedir as homedir8 } from "os";
6494
+ import { join as join11 } from "path";
6320
6495
  import { findErrorCode as findErrorCode8 } from "@basou/core";
6321
6496
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
6322
6497
  var MIN_WATCH_INTERVAL_SEC = 5;
6323
6498
  var MAX_WATCH_INTERVAL_SEC = 86400;
6324
6499
  function watchedRoots(ctx) {
6325
6500
  return [
6326
- ctx.codexSessionsDir ?? join10(homedir7(), ".codex", "sessions"),
6327
- ctx.claudeProjectsDir ?? join10(homedir7(), ".claude", "projects")
6501
+ ctx.codexSessionsDir ?? join11(homedir8(), ".codex", "sessions"),
6502
+ ctx.claudeProjectsDir ?? join11(homedir8(), ".claude", "projects")
6328
6503
  ];
6329
6504
  }
6330
6505
  async function scanSourceLogs(roots) {
@@ -6338,7 +6513,7 @@ async function scanSourceLogs(roots) {
6338
6513
  throw new Error("Failed to read a source log directory", { cause: error });
6339
6514
  }
6340
6515
  for (const entry of entries) {
6341
- const full = join10(dir, entry.name);
6516
+ const full = join11(dir, entry.name);
6342
6517
  if (entry.isDirectory()) {
6343
6518
  await walk(full);
6344
6519
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -6859,8 +7034,8 @@ function renderReviewGaps(summary) {
6859
7034
 
6860
7035
  // src/commands/run.ts
6861
7036
  import { mkdir as mkdir2 } from "fs/promises";
6862
- import { homedir as homedir8 } from "os";
6863
- import { join as join11 } from "path";
7037
+ import { homedir as homedir9 } from "os";
7038
+ import { join as join12 } from "path";
6864
7039
  import {
6865
7040
  acquireLock as acquireLock5,
6866
7041
  assertBasouRootSafe as assertBasouRootSafe11,
@@ -6913,13 +7088,13 @@ async function runClaudeCode(args, options, ctx = {}) {
6913
7088
  await assertBasouRootSafe11(paths.root);
6914
7089
  const manifest = await readManifest7(paths);
6915
7090
  const sessionId = prefixedUlid4("ses");
6916
- const sessionDir = join11(paths.sessions, sessionId);
7091
+ const sessionDir = join12(paths.sessions, sessionId);
6917
7092
  await mkdir2(sessionDir, { recursive: true });
6918
7093
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
6919
7094
  await coreAppendChainedEvent2(paths, sessionId, event);
6920
7095
  });
6921
7096
  const startedAt = now().toISOString();
6922
- const sessionYamlPath = join11(sessionDir, "session.yaml");
7097
+ const sessionYamlPath = join12(sessionDir, "session.yaml");
6923
7098
  const session = buildInitialSession2({
6924
7099
  id: sessionId,
6925
7100
  command,
@@ -7045,7 +7220,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7045
7220
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
7046
7221
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
7047
7222
  workingDirectory: repoRoot,
7048
- homedir: homedir8()
7223
+ homedir: homedir9()
7049
7224
  }).sanitized;
7050
7225
  const finalStatus = decideFinalStatus2(result, signalReceived);
7051
7226
  await appendEvent(sessionDir, {
@@ -7189,7 +7364,7 @@ function buildInitialSession2(input) {
7189
7364
  source: { ...claudeCodeAdapterMetadata },
7190
7365
  started_at: input.startedAt,
7191
7366
  status: "initialized",
7192
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir8() }),
7367
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir9() }),
7193
7368
  invocation: {
7194
7369
  command: input.command,
7195
7370
  args: [...input.args],
@@ -7262,7 +7437,7 @@ async function resolveRepositoryRootForRun(cwd) {
7262
7437
 
7263
7438
  // src/commands/session.ts
7264
7439
  import { readFile as readFile5 } from "fs/promises";
7265
- 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";
7266
7441
  import {
7267
7442
  acquireLock as acquireLock6,
7268
7443
  appendEventToExistingSession as appendEventToExistingSession3,
@@ -7387,8 +7562,8 @@ async function doRunSessionShow(idInput, options, ctx) {
7387
7562
  const paths = basouPaths15(repositoryRoot);
7388
7563
  await assertWorkspaceInitialized10(paths.root);
7389
7564
  const sessionId = await resolveSessionId3(paths, idInput);
7390
- const sessionDir = join12(paths.sessions, sessionId);
7391
- const sessionYamlPath = join12(sessionDir, "session.yaml");
7565
+ const sessionDir = join13(paths.sessions, sessionId);
7566
+ const sessionYamlPath = join13(sessionDir, "session.yaml");
7392
7567
  let session;
7393
7568
  try {
7394
7569
  const raw = await readYamlFile7(sessionYamlPath);
@@ -8131,7 +8306,7 @@ async function resolveRepositoryRootForStatus(cwd) {
8131
8306
 
8132
8307
  // src/commands/task.ts
8133
8308
  import { readFile as readFile6 } from "fs/promises";
8134
- import { join as join13 } from "path";
8309
+ import { join as join14 } from "path";
8135
8310
  import {
8136
8311
  archiveTask,
8137
8312
  assertBasouRootSafe as assertBasouRootSafe15,
@@ -8457,7 +8632,7 @@ async function doRunTaskShow(idInput, options, ctx) {
8457
8632
  const events = [];
8458
8633
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
8459
8634
  for (const s of sessions) {
8460
- const sessionDir = join13(paths.sessions, s.sessionId);
8635
+ const sessionDir = join14(paths.sessions, s.sessionId);
8461
8636
  try {
8462
8637
  for await (const ev of replayEvents3(sessionDir, {
8463
8638
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -9363,7 +9538,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
9363
9538
  // src/lib/portfolio-safety.ts
9364
9539
  import { execFile } from "child_process";
9365
9540
  import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
9366
- 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";
9367
9542
  import { promisify } from "util";
9368
9543
  import { readManifest as readManifest11 } from "@basou/core";
9369
9544
  var execFileAsync = promisify(execFile);
@@ -9387,7 +9562,7 @@ function isBasouPath(p) {
9387
9562
  async function inspectRepo(repoPath) {
9388
9563
  let hasEntry = false;
9389
9564
  try {
9390
- await lstat2(join14(repoPath, ".basou"));
9565
+ await lstat2(join15(repoPath, ".basou"));
9391
9566
  hasEntry = true;
9392
9567
  } catch (error) {
9393
9568
  if (errorCode(error) !== "ENOENT") {
@@ -9488,7 +9663,7 @@ function formatSafetyReport(result) {
9488
9663
 
9489
9664
  // src/lib/view-server.ts
9490
9665
  import { createServer } from "http";
9491
- import { join as join15 } from "path";
9666
+ import { join as join16 } from "path";
9492
9667
  import {
9493
9668
  computeWorkStats as computeWorkStats2,
9494
9669
  enumerateApprovals as enumerateApprovals2,
@@ -10447,7 +10622,7 @@ async function sessionDetail(ws, sessionId) {
10447
10622
  throw error;
10448
10623
  }
10449
10624
  try {
10450
- const events = await readAllEvents2(join15(ws.paths.sessions, sessionId));
10625
+ const events = await readAllEvents2(join16(ws.paths.sessions, sessionId));
10451
10626
  return { session, events };
10452
10627
  } catch {
10453
10628
  return { session, events: [], degraded: true };