@basou/cli 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/program.js CHANGED
@@ -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(program) {
2044
2106
  const hook = program.command("hook").description(
@@ -2049,29 +2111,58 @@ function registerHookCommand(program) {
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"
2117
+ ).option(
2118
+ "--require-review",
2119
+ "Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
2052
2120
  ).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
2053
2121
  const minEdits = parseMinEdits(options.minEdits);
2054
- await runHookStop(minEdits !== void 0 ? { minEdits } : {});
2122
+ await runHookStop({
2123
+ ...minEdits !== void 0 ? { minEdits } : {},
2124
+ ...options.block === true ? { block: true } : {},
2125
+ ...options.requireReview === true ? { requireReview: true } : {}
2126
+ });
2127
+ });
2128
+ hook.command("install").description(
2129
+ "Register the Stop hook in ~/.claude/settings.json (reproducible, idempotent). Default is advisory capture-only; --block opts into in-turn enforcement, --require-review opts into the review gate."
2130
+ ).option("--block", "Register the blocking (opt-in enforcement) form instead of advisory").option("--require-review", "Register with the opt-in review gate enabled").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) => {
2131
+ await runHookInstall(opts);
2132
+ });
2133
+ hook.command("uninstall").description(
2134
+ "Remove the basou Stop hook from ~/.claude/settings.json (leaves other hooks intact)"
2135
+ ).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) => {
2136
+ await runHookUninstall(opts);
2137
+ });
2138
+ 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) => {
2139
+ await runHookStatus(opts);
2055
2140
  });
2056
2141
  }
2057
2142
  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
- }
2143
+ Register this Stop hook reproducibly with 'basou hook install' (it writes the
2144
+ correct node-path command into ~/.claude/settings.json). 'basou hook uninstall'
2145
+ removes it; 'basou hook status' reports whether it is registered.
2066
2146
 
2067
2147
  On every turn end basou inspects the session transcript. If the session did
2068
2148
  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.
2149
+ 'decision record' / 'note'), it reminds the agent to record the why / next step.
2150
+ Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default) OR a free-form AskUserQuestion
2151
+ answer (an uncaptured conversational decision). Read-only Bash (ls / grep /
2152
+ git status) does NOT count.
2153
+
2154
+ With --require-review (opt-in, 'basou hook install --require-review') it also
2155
+ reminds when the session SHIPPED substantive code (git push / git merge /
2156
+ gh pr create|merge) without recording a review ('basou review record'). This
2157
+ gate is off by default; when on, its reminder is composed into the same
2158
+ envelope as the capture reminder.
2159
+
2160
+ By default the reminder is non-blocking: Claude sees it and may act on it or
2161
+ stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
2162
+ returns decision:block, holding the agent in-turn to act on the reminder; the
2163
+ 'stop_hook_active' flag and Claude Code's own loop prevention bound it to a
2164
+ single turn. Either way the hook fails open: a bad payload or unreadable
2165
+ transcript exits cleanly with no output.
2075
2166
  `;
2076
2167
  async function runHookStop(options, ctx = {}) {
2077
2168
  try {
@@ -2109,16 +2200,21 @@ async function doRunHookStop(options, ctx) {
2109
2200
  stopHookActive: false,
2110
2201
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
2111
2202
  });
2112
- if (evaluation.kind !== "nudge") return;
2113
- write(
2114
- `${JSON.stringify({
2115
- hookSpecificOutput: {
2116
- hookEventName: "Stop",
2117
- additionalContext: evaluation.additionalContext
2118
- }
2119
- })}
2120
- `
2121
- );
2203
+ const parts = [];
2204
+ if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
2205
+ if (options.requireReview === true && evaluation.review.fires) {
2206
+ parts.push(evaluation.review.additionalContext);
2207
+ }
2208
+ if (parts.length === 0) return;
2209
+ const reason = parts.join("\n\n");
2210
+ const payloadJson = options.block === true ? JSON.stringify({ decision: "block", reason }) : JSON.stringify({
2211
+ hookSpecificOutput: {
2212
+ hookEventName: "Stop",
2213
+ additionalContext: reason
2214
+ }
2215
+ });
2216
+ write(`${payloadJson}
2217
+ `);
2122
2218
  }
2123
2219
  function parseTranscript(transcript) {
2124
2220
  const records = [];
@@ -2143,9 +2239,9 @@ async function defaultReadStdin() {
2143
2239
  return Buffer.concat(chunks).toString("utf8");
2144
2240
  }
2145
2241
  async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
2146
- const { size } = await stat2(path);
2242
+ const { size } = await stat3(path);
2147
2243
  if (size <= maxBytes) return readFile2(path, "utf8");
2148
- const handle = await open(path, "r");
2244
+ const handle = await open2(path, "r");
2149
2245
  try {
2150
2246
  const buffer = Buffer.alloc(maxBytes);
2151
2247
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, size - maxBytes);
@@ -2160,12 +2256,173 @@ function parseMinEdits(raw) {
2160
2256
  if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
2161
2257
  return Number(raw);
2162
2258
  }
2259
+ var DEFAULT_CLAUDE_SETTINGS_PATH = join6(homedir4(), ".claude", "settings.json");
2260
+ function resolveCliEntry() {
2261
+ return fileURLToPath(import.meta.url);
2262
+ }
2263
+ function normalizeInstallOptions(raw) {
2264
+ const out = {};
2265
+ if (raw.block === true) out.block = true;
2266
+ if (raw.requireReview === true) out.requireReview = true;
2267
+ if (raw.settings !== void 0) out.settings = raw.settings;
2268
+ if (raw.dryRun === true) out.dryRun = true;
2269
+ if (raw.verbose === true) out.verbose = true;
2270
+ if (raw.minEdits !== void 0) {
2271
+ const parsed = parseMinEdits(raw.minEdits);
2272
+ if (parsed === void 0) {
2273
+ throw new Error("--min-edits must be a non-negative integer.");
2274
+ }
2275
+ out.minEdits = parsed;
2276
+ }
2277
+ return out;
2278
+ }
2279
+ async function readSettings(path) {
2280
+ let raw;
2281
+ try {
2282
+ raw = await readFile2(path, "utf8");
2283
+ } catch (error) {
2284
+ if (error instanceof Error && error.code === "ENOENT") {
2285
+ return { raw: null, parsed: void 0 };
2286
+ }
2287
+ throw error;
2288
+ }
2289
+ if (raw.trim().length === 0) return { raw, parsed: void 0 };
2290
+ try {
2291
+ return { raw, parsed: JSON.parse(raw) };
2292
+ } catch (error) {
2293
+ throw new Error(
2294
+ "The Claude settings.json is not valid JSON. Fix it (or remove it) and retry.",
2295
+ {
2296
+ cause: error
2297
+ }
2298
+ );
2299
+ }
2300
+ }
2301
+ async function backupSettingsOnce(path, raw) {
2302
+ if (raw === null) return;
2303
+ const bak = `${path}.basou-bak`;
2304
+ try {
2305
+ await stat3(bak);
2306
+ return;
2307
+ } catch (error) {
2308
+ if (!(error instanceof Error && error.code === "ENOENT")) throw error;
2309
+ }
2310
+ await writeFileDurable(bak, raw);
2311
+ }
2312
+ async function runHookInstall(options, ctx = {}) {
2313
+ try {
2314
+ await doRunHookInstall(normalizeInstallOptions(options), ctx);
2315
+ } catch (error) {
2316
+ renderCliError(error, { verbose: isVerbose(options) });
2317
+ process.exitCode = 1;
2318
+ }
2319
+ }
2320
+ async function doRunHookInstall(options, ctx = {}) {
2321
+ const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
2322
+ const cliEntry = (ctx.resolveCliEntry ?? resolveCliEntry)();
2323
+ const command = buildStopHookCommand({
2324
+ cliEntry,
2325
+ ...options.block === true ? { block: true } : {},
2326
+ ...options.requireReview === true ? { requireReview: true } : {},
2327
+ ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
2328
+ });
2329
+ const mode = describeHookMode({
2330
+ block: options.block === true,
2331
+ review: options.requireReview === true
2332
+ });
2333
+ await assertNotSymlink(settingsPath);
2334
+ const { raw, parsed } = await readSettings(settingsPath);
2335
+ const { settings, action } = upsertStopHook(parsed, command);
2336
+ const newBody = `${JSON.stringify(settings, null, 2)}
2337
+ `;
2338
+ if (raw !== null && newBody === raw) {
2339
+ console.log(`The basou Stop hook is already registered (${mode}); no change.`);
2340
+ return;
2341
+ }
2342
+ if (options.dryRun === true) {
2343
+ console.log(`[dry-run] Would ${action} the basou Stop hook (${mode}).`);
2344
+ return;
2345
+ }
2346
+ const recheck = await readSettings(settingsPath);
2347
+ if (recheck.raw !== raw) {
2348
+ throw new Error(
2349
+ "The settings.json changed during install; aborting so a concurrent edit is not overwritten. Re-run 'basou hook install'."
2350
+ );
2351
+ }
2352
+ await backupSettingsOnce(settingsPath, raw);
2353
+ await writeFileDurable(settingsPath, newBody);
2354
+ console.log(`${action === "installed" ? "Installed" : "Updated"} the basou Stop hook (${mode}).`);
2355
+ }
2356
+ async function runHookUninstall(options) {
2357
+ try {
2358
+ await doRunHookUninstall(normalizeInstallOptions(options));
2359
+ } catch (error) {
2360
+ renderCliError(error, { verbose: isVerbose(options) });
2361
+ process.exitCode = 1;
2362
+ }
2363
+ }
2364
+ async function doRunHookUninstall(options) {
2365
+ const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
2366
+ await assertNotSymlink(settingsPath);
2367
+ const { raw, parsed } = await readSettings(settingsPath);
2368
+ if (raw === null) {
2369
+ console.log("No settings.json; nothing to remove.");
2370
+ return;
2371
+ }
2372
+ const { settings, action } = removeStopHook(parsed);
2373
+ if (action === "absent") {
2374
+ console.log("No basou Stop hook found; nothing removed.");
2375
+ return;
2376
+ }
2377
+ const newBody = `${JSON.stringify(settings, null, 2)}
2378
+ `;
2379
+ if (options.dryRun === true) {
2380
+ console.log("[dry-run] Would remove the basou Stop hook from settings.json.");
2381
+ return;
2382
+ }
2383
+ const recheck = await readSettings(settingsPath);
2384
+ if (recheck.raw !== raw) {
2385
+ throw new Error(
2386
+ "The settings.json changed during uninstall; aborting so a concurrent edit is not overwritten. Re-run 'basou hook uninstall'."
2387
+ );
2388
+ }
2389
+ await backupSettingsOnce(settingsPath, raw);
2390
+ await writeFileDurable(settingsPath, newBody);
2391
+ console.log("Removed the basou Stop hook from settings.json.");
2392
+ }
2393
+ async function runHookStatus(options) {
2394
+ try {
2395
+ await doRunHookStatus(normalizeInstallOptions(options));
2396
+ } catch (error) {
2397
+ renderCliError(error, { verbose: isVerbose(options) });
2398
+ process.exitCode = 1;
2399
+ }
2400
+ }
2401
+ async function doRunHookStatus(options) {
2402
+ const settingsPath = options.settings ?? DEFAULT_CLAUDE_SETTINGS_PATH;
2403
+ const { parsed } = await readSettings(settingsPath);
2404
+ const command = findBasouStopHookCommand(parsed);
2405
+ if (command === null) {
2406
+ console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
2407
+ return;
2408
+ }
2409
+ const mode = describeHookMode({
2410
+ block: / --block\b/.test(command),
2411
+ review: / --require-review\b/.test(command)
2412
+ });
2413
+ console.log(`basou Stop hook: registered, ${mode}.`);
2414
+ }
2415
+ function describeHookMode(tiers) {
2416
+ const enforcement = tiers.block ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
2417
+ const gates = tiers.review ? "capture + review" : "capture";
2418
+ return `${enforcement}, ${gates}`;
2419
+ }
2163
2420
 
2164
2421
  // src/commands/import.ts
2165
2422
  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";
2423
+ import { readdir, readFile as readFile3, rm, stat as stat4 } from "fs/promises";
2424
+ import { homedir as homedir5 } from "os";
2425
+ import { basename as basename3, dirname as dirname2, join as join7, resolve as resolve4 } from "path";
2169
2426
  import { createInterface } from "readline";
2170
2427
  import {
2171
2428
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
@@ -2251,11 +2508,11 @@ async function doRunImportClaudeCode(options, ctx) {
2251
2508
  repoRoot: repositoryRoot,
2252
2509
  cwd: ctx.cwd ?? process.cwd()
2253
2510
  });
2254
- const projectsRoot = ctx.claudeProjectsDir ?? join5(homedir4(), ".claude", "projects");
2511
+ const projectsRoot = ctx.claudeProjectsDir ?? join7(homedir5(), ".claude", "projects");
2255
2512
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
2256
2513
  const projectSet = new Set(projectPaths);
2257
2514
  const candidates = files.map((file) => {
2258
- const externalId = basename2(file, ".jsonl");
2515
+ const externalId = basename3(file, ".jsonl");
2259
2516
  return {
2260
2517
  externalId,
2261
2518
  sourcePath: file,
@@ -2290,7 +2547,7 @@ async function doRunImportCodex(options, ctx) {
2290
2547
  repoRoot: repositoryRoot,
2291
2548
  cwd: ctx.cwd ?? process.cwd()
2292
2549
  });
2293
- const sessionsRoot = ctx.codexSessionsDir ?? join5(homedir4(), ".codex", "sessions");
2550
+ const sessionsRoot = ctx.codexSessionsDir ?? join7(homedir5(), ".codex", "sessions");
2294
2551
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
2295
2552
  const candidates = rollouts.map(({ file, externalId }) => ({
2296
2553
  externalId,
@@ -2345,7 +2602,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2345
2602
  files: payload.session.related_files ?? [],
2346
2603
  workingDirectory: payload.session.working_directory,
2347
2604
  sourceRoots: projectPaths,
2348
- masterRoot: dirname(paths.root),
2605
+ masterRoot: dirname2(paths.root),
2349
2606
  extraInRoot: AGENT_INFRA_DIRS2
2350
2607
  });
2351
2608
  if (scope.outOfRoot.length > 0) crossProject.push({ externalId, outOfRoot: scope.outOfRoot });
@@ -2419,7 +2676,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2419
2676
  if (priors.length > 0 && options.force === true) {
2420
2677
  if (options.dryRun !== true) {
2421
2678
  for (const { sessionId } of priors) {
2422
- await rm(join5(paths.sessions, sessionId), { recursive: true, force: true });
2679
+ await rm(join7(paths.sessions, sessionId), { recursive: true, force: true });
2423
2680
  }
2424
2681
  }
2425
2682
  counts.replaced++;
@@ -2530,7 +2787,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2530
2787
  if (options.session !== void 0) {
2531
2788
  const matches = [];
2532
2789
  for (const projectPath of projectPaths) {
2533
- const file = join5(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2790
+ const file = join7(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2534
2791
  if (await pathExists(file)) matches.push(file);
2535
2792
  }
2536
2793
  if (matches.length === 0) {
@@ -2541,7 +2798,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2541
2798
  const files = [];
2542
2799
  let anyDirFound = false;
2543
2800
  for (const projectPath of projectPaths) {
2544
- const transcriptDir = join5(projectsRoot, encodeProjectDir(projectPath));
2801
+ const transcriptDir = join7(projectsRoot, encodeProjectDir(projectPath));
2545
2802
  let entries;
2546
2803
  try {
2547
2804
  entries = await readdir(transcriptDir);
@@ -2551,7 +2808,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2551
2808
  }
2552
2809
  anyDirFound = true;
2553
2810
  for (const name of entries) {
2554
- if (name.endsWith(".jsonl")) files.push(join5(transcriptDir, name));
2811
+ if (name.endsWith(".jsonl")) files.push(join7(transcriptDir, name));
2555
2812
  }
2556
2813
  }
2557
2814
  if (!anyDirFound) {
@@ -2561,7 +2818,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2561
2818
  }
2562
2819
  async function pathExists(file) {
2563
2820
  try {
2564
- await stat3(file);
2821
+ await stat4(file);
2565
2822
  return true;
2566
2823
  } catch (error) {
2567
2824
  if (findErrorCode5(error, "ENOENT")) return false;
@@ -2570,7 +2827,7 @@ async function pathExists(file) {
2570
2827
  }
2571
2828
  async function statSize(file) {
2572
2829
  try {
2573
- return (await stat3(file)).size;
2830
+ return (await stat4(file)).size;
2574
2831
  } catch (error) {
2575
2832
  if (findErrorCode5(error, "ENOENT")) return void 0;
2576
2833
  throw error;
@@ -2608,7 +2865,7 @@ async function findRolloutFiles(sessionsRoot) {
2608
2865
  throw new Error("Failed to read Codex sessions directory", { cause: error });
2609
2866
  }
2610
2867
  for (const entry of entries) {
2611
- const full = join5(dir, entry.name);
2868
+ const full = join7(dir, entry.name);
2612
2869
  if (entry.isDirectory()) {
2613
2870
  await walk(full, false);
2614
2871
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -2786,7 +3043,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
2786
3043
  }
2787
3044
 
2788
3045
  // src/commands/init.ts
2789
- import { basename as basename3, relative, resolve as resolve5 } from "path";
3046
+ import { basename as basename4, relative, resolve as resolve5 } from "path";
2790
3047
  import {
2791
3048
  appendBasouGitignore,
2792
3049
  createManifest,
@@ -2825,7 +3082,7 @@ async function runInit(options, ctx = {}) {
2825
3082
  async function doRunInit(options, ctx) {
2826
3083
  const cwd = ctx.cwd ?? process.cwd();
2827
3084
  const repositoryRoot = await resolveRepositoryRootForInit(cwd);
2828
- const workspaceName = options.name ?? basename3(repositoryRoot);
3085
+ const workspaceName = options.name ?? basename4(repositoryRoot);
2829
3086
  let repositoryUrl;
2830
3087
  if (options.repoUrl !== void 0) {
2831
3088
  repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
@@ -3050,13 +3307,13 @@ import {
3050
3307
  } from "@basou/core";
3051
3308
 
3052
3309
  // 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";
3310
+ import { homedir as homedir6 } from "os";
3311
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve6 } from "path";
3055
3312
  import { readYamlFile as readYamlFile4 } from "@basou/core";
3056
- var DEFAULT_HOSTS_CONFIG_PATH = join6(homedir5(), ".basou", "hosts.yaml");
3313
+ var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
3057
3314
  function expandTilde2(p) {
3058
- if (p === "~") return homedir5();
3059
- if (p.startsWith("~/")) return join6(homedir5(), p.slice(2));
3315
+ if (p === "~") return homedir6();
3316
+ if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
3060
3317
  return p;
3061
3318
  }
3062
3319
  function isRecord2(value) {
@@ -3382,7 +3639,7 @@ import {
3382
3639
  writeFileSync,
3383
3640
  writeSync
3384
3641
  } from "fs";
3385
- import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
3642
+ import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve7 } from "path";
3386
3643
  import {
3387
3644
  appendBasouGitignore as appendBasouGitignore2,
3388
3645
  basouPaths as basouPaths10,
@@ -3686,7 +3943,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
3686
3943
  } catch {
3687
3944
  return { path: declaredPath, kind: "unresolved" };
3688
3945
  }
3689
- return { path: declaredPath, kind: existsSync(join7(real, ".git")) ? "repo" : "non-repo" };
3946
+ return { path: declaredPath, kind: existsSync(join9(real, ".git")) ? "repo" : "non-repo" };
3690
3947
  }
3691
3948
  async function doRunProjectAdopt(options, ctx) {
3692
3949
  const cwd = ctx.cwd ?? process.cwd();
@@ -3790,7 +4047,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3790
4047
  } catch {
3791
4048
  return { ...base, reachable: false, instructionFiles: [] };
3792
4049
  }
3793
- if (!existsSync(join7(real, ".git"))) {
4050
+ if (!existsSync(join9(real, ".git"))) {
3794
4051
  return { ...base, reachable: false, instructionFiles: [] };
3795
4052
  }
3796
4053
  try {
@@ -3798,7 +4055,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3798
4055
  for (const name of INSTRUCTION_FILES) {
3799
4056
  let present = true;
3800
4057
  try {
3801
- lstatSync(join7(real, name));
4058
+ lstatSync(join9(real, name));
3802
4059
  } catch {
3803
4060
  present = false;
3804
4061
  }
@@ -3909,10 +4166,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
3909
4166
  } catch {
3910
4167
  return { ...base, reachable: false, currentLines: [] };
3911
4168
  }
3912
- if (!existsSync(join7(real, ".git"))) {
4169
+ if (!existsSync(join9(real, ".git"))) {
3913
4170
  return { ...base, reachable: false, currentLines: [] };
3914
4171
  }
3915
- return { ...base, reachable: true, currentLines: readGitignoreLines(join7(real, ".gitignore")) };
4172
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join9(real, ".gitignore")) };
3916
4173
  }
3917
4174
  function hasErrorCode(error) {
3918
4175
  return error instanceof Error && typeof error.code === "string";
@@ -3926,7 +4183,7 @@ function readGitignoreLines(file) {
3926
4183
  }
3927
4184
  }
3928
4185
  function applyGitignorePlan(repositoryRoot, plan) {
3929
- const file = join7(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4186
+ const file = join9(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
3930
4187
  let existing = "";
3931
4188
  try {
3932
4189
  existing = readFileSync(file, "utf8");
@@ -4057,16 +4314,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4057
4314
  if (real === anchorReal) {
4058
4315
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
4059
4316
  }
4060
- if (!existsSync(join7(real, ".git"))) {
4317
+ if (!existsSync(join9(real, ".git"))) {
4061
4318
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4062
4319
  }
4063
- const canonicalFile = isSelf ? join7(real, CANONICAL_FILE) : join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
4320
+ const canonicalFile = isSelf ? join9(real, CANONICAL_FILE) : join9(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4064
4321
  if (!existsSync(canonicalFile)) {
4065
4322
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
4066
4323
  }
4067
4324
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
4068
4325
  (spec) => {
4069
- const { state, actualTarget } = inspectSymlink(join7(real, spec.name), spec.target);
4326
+ const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
4070
4327
  return {
4071
4328
  name: spec.name,
4072
4329
  expectedTarget: spec.target,
@@ -4080,7 +4337,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4080
4337
  isAnchor: false,
4081
4338
  reachable: true,
4082
4339
  canonicalPresent: true,
4083
- canonicalName: basename4(real),
4340
+ canonicalName: basename5(real),
4084
4341
  files
4085
4342
  };
4086
4343
  }
@@ -4095,9 +4352,9 @@ function applySymlinkPlan(repositoryRoot, plan) {
4095
4352
  const created = [];
4096
4353
  const failed = [];
4097
4354
  for (const { name, target } of plan.toCreate) {
4098
- const filePath = join7(real, name);
4355
+ const filePath = join9(real, name);
4099
4356
  try {
4100
- mkdirSync(dirname2(filePath), { recursive: true });
4357
+ mkdirSync(dirname3(filePath), { recursive: true });
4101
4358
  symlinkSync(target, filePath);
4102
4359
  created.push(name);
4103
4360
  } catch (error) {
@@ -4248,7 +4505,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
4248
4505
  return realpathSync(abs);
4249
4506
  } catch {
4250
4507
  try {
4251
- return join7(realpathSync(dirname2(abs)), basename4(abs));
4508
+ return join9(realpathSync(dirname3(abs)), basename5(abs));
4252
4509
  } catch {
4253
4510
  return abs;
4254
4511
  }
@@ -4265,8 +4522,8 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
4265
4522
  if (expectedTarget === "" || expectedTarget === ".") {
4266
4523
  return { path: entry.path, reachable: false };
4267
4524
  }
4268
- const linkName = basename4(repoReal);
4269
- const { state, actualTarget } = inspectSymlink(join7(viewDir, linkName), expectedTarget);
4525
+ const linkName = basename5(repoReal);
4526
+ const { state, actualTarget } = inspectSymlink(join9(viewDir, linkName), expectedTarget);
4270
4527
  return {
4271
4528
  path: entry.path,
4272
4529
  reachable: true,
@@ -4280,9 +4537,9 @@ function applyViewPlan(viewDir, toCreate) {
4280
4537
  const created = [];
4281
4538
  const failed = [];
4282
4539
  for (const { name, target } of toCreate) {
4283
- const filePath = join7(viewDir, name);
4540
+ const filePath = join9(viewDir, name);
4284
4541
  try {
4285
- mkdirSync(dirname2(filePath), { recursive: true });
4542
+ mkdirSync(dirname3(filePath), { recursive: true });
4286
4543
  symlinkSync(target, filePath);
4287
4544
  created.push(name);
4288
4545
  } catch (error) {
@@ -4295,7 +4552,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
4295
4552
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
4296
4553
  );
4297
4554
  function classifyViewLink(viewDir, name, rosterRealpaths) {
4298
- const filePath = join7(viewDir, name);
4555
+ const filePath = join9(viewDir, name);
4299
4556
  let isLink;
4300
4557
  try {
4301
4558
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -4324,7 +4581,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
4324
4581
  if (!isDir) {
4325
4582
  return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
4326
4583
  }
4327
- return { target, kind: existsSync(join7(resolved, ".git")) ? "repo" : "non-repo" };
4584
+ return { target, kind: existsSync(join9(resolved, ".git")) ? "repo" : "non-repo" };
4328
4585
  }
4329
4586
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4330
4587
  let names;
@@ -4349,7 +4606,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4349
4606
  const pruned = [];
4350
4607
  const failed = [];
4351
4608
  for (const { name } of toPrune) {
4352
- const filePath = join7(viewDir, name);
4609
+ const filePath = join9(viewDir, name);
4353
4610
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
4354
4611
  if (c === null || c.kind !== "repo") {
4355
4612
  failed.push({
@@ -4395,7 +4652,7 @@ async function doRunProjectWorkspace(options, ctx) {
4395
4652
  } else {
4396
4653
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
4397
4654
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
4398
- const rosterNames = roster.map((entry) => basename4(resolve7(repositoryRoot, entry.path)));
4655
+ const rosterNames = roster.map((entry) => basename5(resolve7(repositoryRoot, entry.path)));
4399
4656
  const rosterRealpaths = /* @__PURE__ */ new Set();
4400
4657
  for (const entry of roster) {
4401
4658
  try {
@@ -4562,10 +4819,10 @@ async function runProjectPreset(options, ctx = {}) {
4562
4819
  }
4563
4820
  }
4564
4821
  function canonicalFileFor(anchorReal, canonicalName) {
4565
- return join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4822
+ return join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4566
4823
  }
4567
4824
  function canonicalLabelFor(canonicalName) {
4568
- return join7("agents", canonicalName, CANONICAL_FILE);
4825
+ return join9("agents", canonicalName, CANONICAL_FILE);
4569
4826
  }
4570
4827
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4571
4828
  const declared = {
@@ -4586,10 +4843,10 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4586
4843
  if (real === anchorReal) {
4587
4844
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
4588
4845
  }
4589
- if (!existsSync(join7(real, ".git"))) {
4846
+ if (!existsSync(join9(real, ".git"))) {
4590
4847
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
4591
4848
  }
4592
- const canonicalName = basename4(real);
4849
+ const canonicalName = basename5(real);
4593
4850
  let content;
4594
4851
  try {
4595
4852
  content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
@@ -4634,7 +4891,7 @@ async function applyPresetPlan(anchorReal, plan) {
4634
4891
  isLink = false;
4635
4892
  }
4636
4893
  if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
4637
- if (plan.action === "create") mkdirSync(dirname2(file), { recursive: true });
4894
+ if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
4638
4895
  const existing = await readMarkdownFile4(file);
4639
4896
  await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
4640
4897
  }
@@ -4824,28 +5081,28 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4824
5081
  return empty;
4825
5082
  }
4826
5083
  const anchorReal = realpathSync(repositoryRoot);
4827
- const canonicalName = basename4(real);
5084
+ const canonicalName = basename5(real);
4828
5085
  const instructionFiles = [];
4829
5086
  for (const name of INSTRUCTION_FILES) {
4830
5087
  try {
4831
- lstatSync(join7(real, name));
5088
+ lstatSync(join9(real, name));
4832
5089
  instructionFiles.push(name);
4833
5090
  } catch {
4834
5091
  }
4835
5092
  }
4836
5093
  let ignored;
4837
5094
  try {
4838
- ignored = new Set(readGitignoreLines(join7(real, ".gitignore")).map((l) => l.trim()));
5095
+ ignored = new Set(readGitignoreLines(join9(real, ".gitignore")).map((l) => l.trim()));
4839
5096
  } catch {
4840
5097
  ignored = /* @__PURE__ */ new Set();
4841
5098
  }
4842
5099
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
4843
- const canonical2 = existsSync(join7(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5100
+ const canonical2 = existsSync(join9(anchorReal, "agents", canonicalName, CANONICAL_FILE));
4844
5101
  let viewLink = false;
4845
5102
  const viewPath = manifest.workspace.view;
4846
5103
  if (viewPath !== void 0) {
4847
5104
  try {
4848
- lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5105
+ lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), canonicalName));
4849
5106
  viewLink = true;
4850
5107
  } catch {
4851
5108
  }
@@ -4859,11 +5116,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4859
5116
  };
4860
5117
  }
4861
5118
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
4862
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5119
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4863
5120
  return expectedSymlinkTargets(repoReal, canonicalFile);
4864
5121
  }
4865
5122
  function viewLinkPointsAt(viewDir, name, repoReal) {
4866
- const filePath = join7(viewDir, name);
5123
+ const filePath = join9(viewDir, name);
4867
5124
  try {
4868
5125
  if (!lstatSync(filePath).isSymbolicLink()) return false;
4869
5126
  const target = readlinkSync(filePath);
@@ -4874,7 +5131,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
4874
5131
  }
4875
5132
  }
4876
5133
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
4877
- const filePath = join7(viewDir, name);
5134
+ const filePath = join9(viewDir, name);
4878
5135
  try {
4879
5136
  if (!lstatSync(filePath).isSymbolicLink()) return false;
4880
5137
  const target = readlinkSync(filePath);
@@ -4894,7 +5151,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4894
5151
  }
4895
5152
  const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
4896
5153
  const targetAbs = resolve7(repositoryRoot, target);
4897
- const canonicalName = basename4(repoReal ?? targetAbs);
5154
+ const canonicalName = basename5(repoReal ?? targetAbs);
4898
5155
  const roster = manifest.repos ?? [];
4899
5156
  const declaredEntry = roster.find((r) => {
4900
5157
  try {
@@ -4915,17 +5172,17 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4915
5172
  }
4916
5173
  if (rReal !== null) {
4917
5174
  if (repoReal !== void 0 && rReal === repoReal) return false;
4918
- return basename4(rReal).toLowerCase() === cnFold;
5175
+ return basename5(rReal).toLowerCase() === cnFold;
4919
5176
  }
4920
5177
  if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
4921
- return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
5178
+ return basename5(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
4922
5179
  });
4923
5180
  const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
4924
5181
  const items = [];
4925
5182
  if (!isAnchor) {
4926
5183
  if (repoReal !== void 0) {
4927
5184
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
4928
- const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
5185
+ const { state, actualTarget } = inspectSymlink(join9(repoReal, spec.name), spec.target);
4929
5186
  if (isSelf) {
4930
5187
  if (state !== "missing")
4931
5188
  items.push({
@@ -4962,7 +5219,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4962
5219
  }
4963
5220
  let ignored;
4964
5221
  try {
4965
- ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
5222
+ ignored = new Set(readGitignoreLines(join9(repoReal, ".gitignore")).map((l) => l.trim()));
4966
5223
  for (const p of INSTRUCTION_FILES) {
4967
5224
  if (ignored.has(p) || ignored.has(`/${p}`)) {
4968
5225
  items.push({
@@ -4985,7 +5242,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4985
5242
  const viewPath = manifest.workspace.view;
4986
5243
  if (viewPath !== void 0) {
4987
5244
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
4988
- const linkPath = join7(viewDir, canonicalName);
5245
+ const linkPath = join9(viewDir, canonicalName);
4989
5246
  let isLink = false;
4990
5247
  try {
4991
5248
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -5011,8 +5268,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5011
5268
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
5012
5269
  }
5013
5270
  }
5014
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5015
- const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
5271
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5272
+ const canonicalLabel = join9("agents", canonicalName, CANONICAL_FILE);
5016
5273
  let canonicalIsLink = false;
5017
5274
  try {
5018
5275
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -5116,12 +5373,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5116
5373
  );
5117
5374
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5118
5375
  const expected = expectedByName.get(item.label);
5119
- if (repoReal === null || expected === void 0 || inspectSymlink(join7(repoReal, item.label), expected).state !== "correct") {
5376
+ if (repoReal === null || expected === void 0 || inspectSymlink(join9(repoReal, item.label), expected).state !== "correct") {
5120
5377
  changed(item.label);
5121
5378
  continue;
5122
5379
  }
5123
5380
  try {
5124
- unlinkSync(join7(repoReal, item.label));
5381
+ unlinkSync(join9(repoReal, item.label));
5125
5382
  removed.push(item.label);
5126
5383
  } catch (error) {
5127
5384
  failed.push({ label: item.label, message: failureReason(error) });
@@ -5140,7 +5397,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5140
5397
  continue;
5141
5398
  }
5142
5399
  try {
5143
- unlinkSync(join7(viewDir, item.label));
5400
+ unlinkSync(join9(viewDir, item.label));
5144
5401
  removed.push(`view/${item.label}`);
5145
5402
  } catch (error) {
5146
5403
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -5148,7 +5405,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5148
5405
  }
5149
5406
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5150
5407
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5151
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5408
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5152
5409
  try {
5153
5410
  if (lstatSync(canonicalFile).isSymbolicLink()) {
5154
5411
  changed(item.label);
@@ -5383,7 +5640,7 @@ function renderProjectArchive(result) {
5383
5640
  if (t.gitignorePatterns.length > 0)
5384
5641
  items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
5385
5642
  if (t.canonical)
5386
- items.push(`the anchor's canonical (agents/${basename4(result.target)}/AGENTS.md)`);
5643
+ items.push(`the anchor's canonical (agents/${basename5(result.target)}/AGENTS.md)`);
5387
5644
  if (!t.inspected) {
5388
5645
  lines.push(
5389
5646
  "## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
@@ -5422,12 +5679,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
5422
5679
  } catch {
5423
5680
  return { canonicalDirOld: false, viewLinkOld: false };
5424
5681
  }
5425
- const canonicalDirOld = existsSync(join7(anchorReal, "agents", oldBasename));
5682
+ const canonicalDirOld = existsSync(join9(anchorReal, "agents", oldBasename));
5426
5683
  let viewLinkOld = false;
5427
5684
  const viewPath = manifest.workspace.view;
5428
5685
  if (viewPath !== void 0) {
5429
5686
  try {
5430
- lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), oldBasename));
5687
+ lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), oldBasename));
5431
5688
  viewLinkOld = true;
5432
5689
  } catch {
5433
5690
  }
@@ -5586,7 +5843,7 @@ async function resolveRepositoryRootForNew(cwd) {
5586
5843
  async function doRunProjectNew(repos, options, ctx) {
5587
5844
  const cwd = ctx.cwd ?? process.cwd();
5588
5845
  const repositoryRoot = await resolveRepositoryRootForNew(cwd);
5589
- const workspaceName = basename4(repositoryRoot);
5846
+ const workspaceName = basename5(repositoryRoot);
5590
5847
  const declared = repos.map((p) => {
5591
5848
  const abs = resolve7(cwd, p);
5592
5849
  let real;
@@ -5768,7 +6025,7 @@ function regularFileSpokes(repoReal) {
5768
6025
  const out = [];
5769
6026
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
5770
6027
  try {
5771
- const st = lstatSync(join7(repoReal, spoke));
6028
+ const st = lstatSync(join9(repoReal, spoke));
5772
6029
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
5773
6030
  } catch {
5774
6031
  }
@@ -5798,7 +6055,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5798
6055
  const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
5799
6056
  const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
5800
6057
  const path = displayRel === "" ? "." : displayRel;
5801
- const canonicalName = basename4(argReal ?? argAbs);
6058
+ const canonicalName = basename5(argReal ?? argAbs);
5802
6059
  if (argReal === void 0) {
5803
6060
  return {
5804
6061
  path,
@@ -5813,8 +6070,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5813
6070
  };
5814
6071
  }
5815
6072
  const isAnchor = argReal === anchorReal;
5816
- const reachable = existsSync(join7(argReal, ".git"));
5817
- const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6073
+ const reachable = existsSync(join9(argReal, ".git"));
6074
+ const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5818
6075
  return {
5819
6076
  path,
5820
6077
  declared,
@@ -5822,15 +6079,15 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
5822
6079
  isAnchor,
5823
6080
  reachable,
5824
6081
  canonicalName,
5825
- agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
6082
+ agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
5826
6083
  canonicalExists: pathPresent(canonicalFile),
5827
6084
  regularSpokes: regularFileSpokes(argReal)
5828
6085
  };
5829
6086
  }
5830
6087
  function relocateAgentsFile(repoReal, canonicalFile) {
5831
- const agentsFile = join7(repoReal, CANONICAL_FILE);
6088
+ const agentsFile = join9(repoReal, CANONICAL_FILE);
5832
6089
  try {
5833
- mkdirSync(dirname2(canonicalFile), { recursive: true });
6090
+ mkdirSync(dirname3(canonicalFile), { recursive: true });
5834
6091
  } catch (error) {
5835
6092
  return { ok: false, message: failureReason(error), partial: false };
5836
6093
  }
@@ -5868,7 +6125,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
5868
6125
  let failure;
5869
6126
  let partial = false;
5870
6127
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
5871
- const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6128
+ const canonicalFile = join9(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
5872
6129
  const res = relocateAgentsFile(argReal, canonicalFile);
5873
6130
  if (res.ok) {
5874
6131
  applied = true;
@@ -5992,78 +6249,131 @@ function renderProjectRetrofit(result) {
5992
6249
 
5993
6250
  // src/commands/protocol.ts
5994
6251
  import { readFile as readFile4 } from "fs/promises";
6252
+ import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdownFile as readMarkdownFile6 } from "@basou/core";
6253
+
6254
+ // src/lib/context-channel.ts
6255
+ import { homedir as homedir7 } from "os";
6256
+ import { join as join10 } from "path";
5995
6257
  import {
5996
- PROTOCOL_END,
5997
- PROTOCOL_START,
6258
+ ORIENTATION_END,
6259
+ ORIENTATION_START,
5998
6260
  parseMarkers as parseMarkers2,
5999
6261
  readMarkdownFile as readMarkdownFile5,
6000
6262
  removeMarkerSection as removeMarkerSection2
6001
6263
  } from "@basou/core";
6002
-
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()) {
6264
+ var CODEX_TARGET_PATH = join10(homedir7(), ".codex", "AGENTS.md");
6265
+ var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
6266
+ var ORIENTATION_MANAGED_NOTE = "<!-- Managed by basou: 'basou refresh' regenerates everything between the BASOU:ORIENTATION markers with the workspace's current position. This block is transient \u2014 it changes every refresh; do not edit it. -->";
6267
+ function buildTargetBody(existing, block, markers) {
6268
+ const wrapped = `${markers.start}
6269
+ ${block}${markers.end}
6270
+ `;
6271
+ if (existing === null || existing === "") return wrapped;
6272
+ const section = parseMarkers2(existing, markers);
6273
+ switch (section.kind) {
6274
+ case "ok":
6275
+ return `${section.before}${markers.start}
6276
+ ${block}${markers.end}${section.after}`;
6277
+ case "no_markers": {
6278
+ const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
6279
+ return `${existing}${sep}${wrapped}`;
6280
+ }
6281
+ default:
6011
6282
  throw new Error(
6012
- "Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
6283
+ "The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
6013
6284
  );
6014
- }
6015
- } catch (error) {
6016
- if (error instanceof Error && error.code === "ENOENT") return;
6017
- throw error;
6018
6285
  }
6019
6286
  }
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;
6287
+ async function backupOnce(target, existing) {
6288
+ if (existing === null) return;
6289
+ const bak = `${target}.basou-bak`;
6290
+ const already = await readMarkdownFile5(bak);
6291
+ if (already !== null) return;
6292
+ await writeFileDurable(bak, existing);
6293
+ }
6294
+ async function syncMarkerBlock(opts) {
6295
+ const { target, markers, block } = opts;
6296
+ await assertNotSymlink(target);
6297
+ const existing = await readMarkdownFile5(target);
6298
+ const newBody = buildTargetBody(existing, block, markers);
6299
+ if (newBody === existing) return { action: "unchanged" };
6300
+ const hadBlock = existing !== null && parseMarkers2(existing, markers).kind === "ok";
6301
+ const action = hadBlock ? "updated" : "installed";
6302
+ if (opts.dryRun === true) return { action };
6303
+ const recheck = await readMarkdownFile5(target);
6304
+ if (recheck !== existing) {
6305
+ throw new Error(
6306
+ "The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
6307
+ );
6044
6308
  }
6045
- try {
6046
- const dirHandle = await open2(dir, "r");
6047
- try {
6048
- await dirHandle.sync();
6049
- } finally {
6050
- await dirHandle.close();
6309
+ await backupOnce(target, existing);
6310
+ await writeFileDurable(target, newBody);
6311
+ return { action };
6312
+ }
6313
+ function assertNoMarkerLine(body, markers) {
6314
+ for (const line of body.split(/\r?\n/)) {
6315
+ if (line === markers.start || line === markers.end) {
6316
+ throw new Error(
6317
+ "The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
6318
+ );
6051
6319
  }
6052
- } catch {
6053
6320
  }
6054
6321
  }
6322
+ async function removeMarkerBlock(opts) {
6323
+ const { target, markers, fileLabel } = opts;
6324
+ await assertNotSymlink(target);
6325
+ const existing = await readMarkdownFile5(target);
6326
+ if (existing === null) return { removed: false };
6327
+ const newBody = removeMarkerSection2(existing, fileLabel, markers);
6328
+ if (newBody === existing) return { removed: false };
6329
+ if (opts.dryRun === true) return { removed: true };
6330
+ const recheck = await readMarkdownFile5(target);
6331
+ if (recheck !== existing) {
6332
+ throw new Error(
6333
+ "The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
6334
+ );
6335
+ }
6336
+ await backupOnce(target, existing);
6337
+ await writeFileDurable(target, newBody);
6338
+ return { removed: true };
6339
+ }
6340
+ async function syncOrientationChannel(opts) {
6341
+ assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
6342
+ const block = `${ORIENTATION_MANAGED_NOTE}
6343
+
6344
+ ${opts.body.replace(/\s+$/, "")}
6345
+ `;
6346
+ return syncMarkerBlock({
6347
+ target: opts.target ?? CODEX_TARGET_PATH,
6348
+ markers: ORIENTATION_MARKERS,
6349
+ block,
6350
+ ...opts.dryRun === true ? { dryRun: true } : {}
6351
+ });
6352
+ }
6353
+ async function renderOrientationToCodexChannel(opts) {
6354
+ const body = await readMarkdownFile5(opts.orientationPath);
6355
+ if (body === null) return null;
6356
+ const { action } = await syncOrientationChannel({
6357
+ body,
6358
+ ...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
6359
+ });
6360
+ return {
6361
+ action,
6362
+ line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
6363
+ };
6364
+ }
6055
6365
 
6056
6366
  // 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";
6367
+ import { homedir as homedir8 } from "os";
6368
+ import { isAbsolute as isAbsolute4, join as join11, resolve as resolve8 } from "path";
6059
6369
  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");
6370
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join11(homedir8(), ".basou", "protocols.yaml");
6371
+ var DEFAULT_TARGET_PATH = join11(homedir8(), ".claude", "CLAUDE.md");
6062
6372
  var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
6063
6373
  var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
6064
6374
  function expandTilde3(p) {
6065
- if (p === "~") return homedir6();
6066
- if (p.startsWith("~/")) return join9(homedir6(), p.slice(2));
6375
+ if (p === "~") return homedir8();
6376
+ if (p.startsWith("~/")) return join11(homedir8(), p.slice(2));
6067
6377
  return p;
6068
6378
  }
6069
6379
  function isRecord3(value) {
@@ -6184,13 +6494,7 @@ async function readProtocolSources(entries) {
6184
6494
  }
6185
6495
  throw new Error("Failed to read a protocol source file.", { cause: error });
6186
6496
  }
6187
- for (const line of content.split(/\r?\n/)) {
6188
- if (line === PROTOCOL_START || line === PROTOCOL_END) {
6189
- throw new Error(
6190
- "A protocol source contains a BASOU:PROTOCOLS marker line, which would corrupt the managed block. Remove that line from the source."
6191
- );
6192
- }
6193
- }
6497
+ assertNoMarkerLine(content, PROTOCOL_MARKERS);
6194
6498
  out.push({ entry, content });
6195
6499
  }
6196
6500
  return out;
@@ -6207,74 +6511,41 @@ ${body}` : body;
6207
6511
  ${sections.join("\n\n")}
6208
6512
  `;
6209
6513
  }
6210
- function buildTargetBody(existing, block) {
6211
- const wrapped = `${PROTOCOL_START}
6212
- ${block}${PROTOCOL_END}
6213
- `;
6214
- if (existing === null || existing === "") return wrapped;
6215
- const section = parseMarkers2(existing, PROTOCOL_MARKERS);
6216
- switch (section.kind) {
6217
- case "ok":
6218
- return `${section.before}${PROTOCOL_START}
6219
- ${block}${PROTOCOL_END}${section.after}`;
6220
- case "no_markers": {
6221
- const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
6222
- return `${existing}${sep}${wrapped}`;
6223
- }
6224
- default:
6225
- throw new Error(
6226
- "The BASOU:PROTOCOLS markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
6227
- );
6228
- }
6229
- }
6230
- async function backupOnce(target, existing) {
6231
- if (existing === null) return;
6232
- const bak = `${target}.basou-bak`;
6233
- const already = await readMarkdownFile5(bak);
6234
- if (already !== null) return;
6235
- await writeFileDurable(bak, existing);
6236
- }
6237
6514
  async function doRunProtocolSync(options) {
6238
6515
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
6239
6516
  const target = options.target ?? DEFAULT_TARGET_PATH;
6240
6517
  const entries = await loadProtocolsConfig(configPath);
6241
6518
  const sources = await readProtocolSources(entries);
6242
6519
  const block = buildBlock(sources);
6243
- await assertNotSymlink(target);
6244
- const existing = await readMarkdownFile5(target);
6245
- const newBody = buildTargetBody(existing, block);
6246
- if (newBody === existing) {
6520
+ const result = await syncMarkerBlock({
6521
+ target,
6522
+ markers: PROTOCOL_MARKERS,
6523
+ block,
6524
+ ...options.dryRun === true ? { dryRun: true } : {}
6525
+ });
6526
+ if (result.action === "unchanged") {
6247
6527
  console.log(`The basou:protocols block is already up to date (${entries.length} protocol(s)).`);
6248
6528
  return;
6249
6529
  }
6250
- const hadBlock = existing !== null && parseMarkers2(existing, PROTOCOL_MARKERS).kind === "ok";
6251
6530
  if (options.dryRun === true) {
6252
6531
  console.log(
6253
- `[dry-run] Would ${hadBlock ? "update" : "install"} the basou:protocols block (${entries.length} protocol(s)).`
6532
+ `[dry-run] Would ${result.action === "updated" ? "update" : "install"} the basou:protocols block (${entries.length} protocol(s)).`
6254
6533
  );
6255
6534
  for (const { entry } of sources) {
6256
6535
  console.log(` - ${entry.title ?? entry.source}`);
6257
6536
  }
6258
6537
  return;
6259
6538
  }
6260
- const recheck = await readMarkdownFile5(target);
6261
- if (recheck !== existing) {
6262
- throw new Error(
6263
- "The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol sync'."
6264
- );
6265
- }
6266
- await backupOnce(target, existing);
6267
- await writeFileDurable(target, newBody);
6268
6539
  console.log(
6269
- `${hadBlock ? "Updated" : "Installed"} the basou:protocols block in the global CLAUDE.md (${entries.length} protocol(s)).`
6540
+ `${result.action === "updated" ? "Updated" : "Installed"} the basou:protocols block in the global CLAUDE.md (${entries.length} protocol(s)).`
6270
6541
  );
6271
6542
  }
6272
6543
  async function doRunProtocolList(options) {
6273
6544
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
6274
6545
  const target = options.target ?? DEFAULT_TARGET_PATH;
6275
6546
  const entries = await loadProtocolsConfig(configPath);
6276
- const existing = await readMarkdownFile5(target);
6277
- const installed = existing !== null && parseMarkers2(existing, PROTOCOL_MARKERS).kind === "ok";
6547
+ const existing = await readMarkdownFile6(target);
6548
+ const installed = existing !== null && parseMarkers3(existing, PROTOCOL_MARKERS).kind === "ok";
6278
6549
  console.log(`Declared protocols (${entries.length}):`);
6279
6550
  for (const entry of entries) {
6280
6551
  console.log(` - ${entry.title ?? entry.source}`);
@@ -6283,14 +6554,13 @@ async function doRunProtocolList(options) {
6283
6554
  }
6284
6555
  async function doRunProtocolUnsync(options) {
6285
6556
  const target = options.target ?? DEFAULT_TARGET_PATH;
6286
- await assertNotSymlink(target);
6287
- const existing = await readMarkdownFile5(target);
6288
- if (existing === null) {
6289
- console.log("No target file; nothing to remove.");
6290
- return;
6291
- }
6292
- const newBody = removeMarkerSection2(existing, "CLAUDE.md", PROTOCOL_MARKERS);
6293
- if (newBody === existing) {
6557
+ const result = await removeMarkerBlock({
6558
+ target,
6559
+ markers: PROTOCOL_MARKERS,
6560
+ fileLabel: "CLAUDE.md",
6561
+ ...options.dryRun === true ? { dryRun: true } : {}
6562
+ });
6563
+ if (!result.removed) {
6294
6564
  console.log("No basou:protocols block found; nothing removed.");
6295
6565
  return;
6296
6566
  }
@@ -6298,14 +6568,6 @@ async function doRunProtocolUnsync(options) {
6298
6568
  console.log("[dry-run] Would remove the basou:protocols block from the global CLAUDE.md.");
6299
6569
  return;
6300
6570
  }
6301
- const recheck = await readMarkdownFile5(target);
6302
- if (recheck !== existing) {
6303
- throw new Error(
6304
- "The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol unsync'."
6305
- );
6306
- }
6307
- await backupOnce(target, existing);
6308
- await writeFileDurable(target, newBody);
6309
6571
  console.log("Removed the basou:protocols block from the global CLAUDE.md.");
6310
6572
  }
6311
6573
 
@@ -6315,16 +6577,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
6315
6577
 
6316
6578
  // src/commands/refresh-watch.ts
6317
6579
  import { readdir as readdir2, stat as stat5 } from "fs/promises";
6318
- import { homedir as homedir7 } from "os";
6319
- import { join as join10 } from "path";
6580
+ import { homedir as homedir9 } from "os";
6581
+ import { join as join12 } from "path";
6320
6582
  import { findErrorCode as findErrorCode8 } from "@basou/core";
6321
6583
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
6322
6584
  var MIN_WATCH_INTERVAL_SEC = 5;
6323
6585
  var MAX_WATCH_INTERVAL_SEC = 86400;
6324
6586
  function watchedRoots(ctx) {
6325
6587
  return [
6326
- ctx.codexSessionsDir ?? join10(homedir7(), ".codex", "sessions"),
6327
- ctx.claudeProjectsDir ?? join10(homedir7(), ".claude", "projects")
6588
+ ctx.codexSessionsDir ?? join12(homedir9(), ".codex", "sessions"),
6589
+ ctx.claudeProjectsDir ?? join12(homedir9(), ".claude", "projects")
6328
6590
  ];
6329
6591
  }
6330
6592
  async function scanSourceLogs(roots) {
@@ -6338,7 +6600,7 @@ async function scanSourceLogs(roots) {
6338
6600
  throw new Error("Failed to read a source log directory", { cause: error });
6339
6601
  }
6340
6602
  for (const entry of entries) {
6341
- const full = join10(dir, entry.name);
6603
+ const full = join12(dir, entry.name);
6342
6604
  if (entry.isDirectory()) {
6343
6605
  await walk(full);
6344
6606
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -6445,19 +6707,19 @@ function parseInterval(value) {
6445
6707
  return seconds;
6446
6708
  }
6447
6709
  function abortableSleep(ms, signal) {
6448
- return new Promise((resolve12) => {
6710
+ return new Promise((resolve13) => {
6449
6711
  if (signal.aborted) {
6450
- resolve12();
6712
+ resolve13();
6451
6713
  return;
6452
6714
  }
6453
6715
  let timer;
6454
6716
  const onAbort = () => {
6455
6717
  clearTimeout(timer);
6456
- resolve12();
6718
+ resolve13();
6457
6719
  };
6458
6720
  timer = setTimeout(() => {
6459
6721
  signal.removeEventListener("abort", onAbort);
6460
- resolve12();
6722
+ resolve13();
6461
6723
  }, ms);
6462
6724
  signal.addEventListener("abort", onAbort, { once: true });
6463
6725
  });
@@ -6510,7 +6772,7 @@ async function doRunRefreshPortfolio(options, ctx) {
6510
6772
  for (const ws of workspaces) {
6511
6773
  const label = ws.label ?? ws.path;
6512
6774
  try {
6513
- const result = await computeRefresh(
6775
+ const { result } = await computeRefresh(
6514
6776
  { ...options, portfolio: false },
6515
6777
  { ...ctx, cwd: ws.path }
6516
6778
  );
@@ -6579,7 +6841,7 @@ async function computeRefresh(options, ctx) {
6579
6841
  const paths = basouPaths11(repositoryRoot);
6580
6842
  await assertWorkspaceInitialized8(paths.root);
6581
6843
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
6582
- return refreshAll({
6844
+ const result = await refreshAll({
6583
6845
  options: {
6584
6846
  ...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
6585
6847
  ...options.force === true ? { force: true } : {},
@@ -6591,16 +6853,30 @@ async function computeRefresh(options, ctx) {
6591
6853
  paths,
6592
6854
  nowIso
6593
6855
  });
6856
+ return { result, paths };
6594
6857
  }
6595
6858
  async function doRunRefresh(options, ctx) {
6596
- const result = await computeRefresh(options, ctx);
6859
+ const { result, paths } = await computeRefresh(options, ctx);
6860
+ const channelLine = options.dryRun === true ? null : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
6597
6861
  if (options.json === true) {
6598
6862
  console.log(JSON.stringify(result));
6599
6863
  } else {
6600
6864
  printRefreshSummary(result);
6865
+ if (channelLine !== null) console.log(channelLine);
6601
6866
  }
6602
6867
  return result;
6603
6868
  }
6869
+ async function syncCodexOrientationChannel(paths, channelPath) {
6870
+ try {
6871
+ const rendered = await renderOrientationToCodexChannel({
6872
+ orientationPath: paths.files.orientation,
6873
+ ...channelPath !== void 0 ? { channelPath } : {}
6874
+ });
6875
+ return rendered === null ? null : rendered.line;
6876
+ } catch (error) {
6877
+ return `codex channel skipped: ${error instanceof Error ? error.message : String(error)}`;
6878
+ }
6879
+ }
6604
6880
  function describeImport(outcome) {
6605
6881
  if (outcome.status === "skipped") {
6606
6882
  return `${outcome.adapter}: skipped (${outcome.reason})`;
@@ -6733,9 +7009,195 @@ async function assertWorkspaceInitialized9(basouRoot) {
6733
7009
  }
6734
7010
  }
6735
7011
 
6736
- // src/commands/review-gaps.ts
7012
+ // src/commands/review.ts
7013
+ import { readFile as readFile5 } from "fs/promises";
7014
+ import { homedir as homedir10 } from "os";
7015
+ import { resolve as resolve10 } from "path";
6737
7016
  import {
7017
+ assertBasouRootSafe as assertBasouRootSafe11,
6738
7018
  basouPaths as basouPaths13,
7019
+ buildReviewRecordedEvent,
7020
+ buildReviewRecordLabel,
7021
+ createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
7022
+ findErrorCode as findErrorCode11,
7023
+ parseReviewRecordInput,
7024
+ readManifest as readManifest7,
7025
+ sanitizePath as sanitizePath2
7026
+ } from "@basou/core";
7027
+ function registerReviewCommand(program) {
7028
+ const review = program.command("review").description("Record reviews that ran (the durable signal a review happened)");
7029
+ review.command("record").description(
7030
+ "Record that a review ran, from a JSON object (stdin or --file). The in-loop agent runs an adversarial / second-opinion review and pipes a description -- reviewer, target, optional verdict/findings/blocked -- and basou writes one review_recorded event deterministically."
7031
+ ).option("--file <path>", "Read the JSON object from a file instead of stdin").option("--dry-run", "Validate and preview the review without writing it").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").addHelpText("after", REVIEW_RECORD_HELP).action(async (options) => {
7032
+ await runReviewRecord(options);
7033
+ });
7034
+ }
7035
+ var REVIEW_RECORD_HELP = `
7036
+ Input format (a single JSON object describing one review):
7037
+ {
7038
+ "reviewer": "codex",
7039
+ "target": "working-tree",
7040
+ "verdict": "needs-attention",
7041
+ "findings": [
7042
+ { "title": "Off-by-one in pager", "severity": "medium", "location": "src/page.ts:42", "summary": "..." }
7043
+ ],
7044
+ "blocked": [
7045
+ { "title": "Reviewer wanted to drop the singleton", "reason": "design-reversal", "why": "Settled in decision_X" }
7046
+ ]
7047
+ }
7048
+
7049
+ Only "reviewer" and "target" are required; verdict / findings / blocked are
7050
+ optional. Record blocked findings (spec-deviation / design-reversal) here so the
7051
+ adversarial-review protocol's "always report what you blocked" becomes a durable
7052
+ trail artifact -- an explicit empty "blocked": [] is encouraged to record that
7053
+ you blocked nothing. The review is written into one ad-hoc session timestamped
7054
+ now. Run from a workspace-view directory and it resolves to the planning repo,
7055
+ like 'basou decision capture' / 'basou note'.
7056
+
7057
+ Example (heredoc on stdin):
7058
+ basou review record <<'JSON'
7059
+ { "reviewer": "codex", "target": "working-tree", "verdict": "pass", "blocked": [] }
7060
+ JSON
7061
+ `;
7062
+ async function runReviewRecord(options, ctx = {}) {
7063
+ try {
7064
+ await doRunReviewRecord(options, ctx);
7065
+ } catch (error) {
7066
+ renderCliError(error, {
7067
+ verbose: isVerbose(options),
7068
+ classifiers: [failedToFinalizeClassifier]
7069
+ });
7070
+ process.exitCode = 1;
7071
+ }
7072
+ }
7073
+ async function doRunReviewRecord(options, ctx) {
7074
+ const cwd = ctx.cwd ?? process.cwd();
7075
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "review record");
7076
+ const paths = basouPaths13(repositoryRoot);
7077
+ await assertWorkspaceInitialized10(paths.root);
7078
+ const raw = await readReviewInput(options, ctx);
7079
+ const review = parseReviewRecordInput(raw);
7080
+ if (options.dryRun === true) {
7081
+ printReviewPreview(options, review);
7082
+ return;
7083
+ }
7084
+ const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
7085
+ const occurredAt = now.toISOString();
7086
+ const manifest = await readManifest7(paths);
7087
+ const invocationArgs = options.file !== void 0 ? [
7088
+ "--file",
7089
+ sanitizePath2(resolve10(cwd, options.file), {
7090
+ workingDirectory: repositoryRoot,
7091
+ homedir: homedir10()
7092
+ })
7093
+ ] : [];
7094
+ const adHoc = await createAdHocSessionWithEvent3({
7095
+ paths,
7096
+ manifest,
7097
+ label: buildReviewRecordLabel(review),
7098
+ occurredAt,
7099
+ sessionSource: "human",
7100
+ workingDirectory: repositoryRoot,
7101
+ invocation: { command: "basou review record", args: invocationArgs },
7102
+ targetEventBuilders: [
7103
+ (sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review })
7104
+ ]
7105
+ });
7106
+ printReviewResult(options, {
7107
+ sessionId: adHoc.sessionId,
7108
+ eventId: adHoc.targetEventIds[0],
7109
+ review
7110
+ });
7111
+ }
7112
+ async function readReviewInput(options, ctx) {
7113
+ if (options.file !== void 0) {
7114
+ try {
7115
+ return await readFile5(options.file, "utf8");
7116
+ } catch (error) {
7117
+ if (findErrorCode11(error, "ENOENT")) {
7118
+ throw new Error(`Input file not found: ${options.file}`);
7119
+ }
7120
+ throw error;
7121
+ }
7122
+ }
7123
+ if (ctx.readInput !== void 0) {
7124
+ return await ctx.readInput();
7125
+ }
7126
+ if (process.stdin.isTTY === true) {
7127
+ throw new Error(NO_INPUT_HINT2);
7128
+ }
7129
+ return await readStdinToEnd2();
7130
+ }
7131
+ async function readStdinToEnd2() {
7132
+ const chunks = [];
7133
+ for await (const chunk of process.stdin) {
7134
+ chunks.push(Buffer.from(chunk));
7135
+ }
7136
+ return Buffer.concat(chunks).toString("utf8");
7137
+ }
7138
+ var NO_INPUT_HINT2 = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
7139
+ function reviewToPayload(review) {
7140
+ const payload = {
7141
+ reviewer: review.reviewer,
7142
+ target: review.target
7143
+ };
7144
+ if (review.verdict !== void 0) payload.verdict = review.verdict;
7145
+ if (review.findings !== void 0) payload.findings = review.findings;
7146
+ if (review.blocked !== void 0) payload.blocked = review.blocked;
7147
+ return payload;
7148
+ }
7149
+ function reviewSummaryLine(review) {
7150
+ const parts = [];
7151
+ if (review.verdict !== void 0) parts.push(`verdict: ${review.verdict}`);
7152
+ if (review.findings !== void 0) {
7153
+ parts.push(`${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}`);
7154
+ }
7155
+ if (review.blocked !== void 0) {
7156
+ parts.push(`${review.blocked.length} blocked`);
7157
+ }
7158
+ return parts.length > 0 ? ` (${parts.join(", ")})` : "";
7159
+ }
7160
+ function printReviewPreview(options, review) {
7161
+ if (options.json === true) {
7162
+ console.log(JSON.stringify({ dry_run: true, review: reviewToPayload(review) }));
7163
+ return;
7164
+ }
7165
+ console.log(
7166
+ `Would record review by ${review.reviewer} of ${review.target}${reviewSummaryLine(review)} (dry run; nothing written).`
7167
+ );
7168
+ }
7169
+ function printReviewResult(options, result) {
7170
+ const sid = shortSessionId(result.sessionId);
7171
+ if (options.json === true) {
7172
+ console.log(
7173
+ JSON.stringify({
7174
+ mode: "ad-hoc",
7175
+ session_id: result.sessionId,
7176
+ session_status: "completed",
7177
+ event_id: result.eventId,
7178
+ review: reviewToPayload(result.review)
7179
+ })
7180
+ );
7181
+ return;
7182
+ }
7183
+ console.log(
7184
+ `Recorded review by ${result.review.reviewer} of ${result.review.target}${reviewSummaryLine(result.review)} in ad-hoc session ${sid}.`
7185
+ );
7186
+ }
7187
+ async function assertWorkspaceInitialized10(basouRoot) {
7188
+ try {
7189
+ await assertBasouRootSafe11(basouRoot);
7190
+ } catch (error) {
7191
+ if (findErrorCode11(error, "ENOENT")) {
7192
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
7193
+ }
7194
+ throw error;
7195
+ }
7196
+ }
7197
+
7198
+ // src/commands/review-gaps.ts
7199
+ import {
7200
+ basouPaths as basouPaths14,
6739
7201
  findReviewGaps
6740
7202
  } from "@basou/core";
6741
7203
  import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
@@ -6776,7 +7238,7 @@ async function runReviewGaps(options, ctx = {}) {
6776
7238
  async function doRunReviewGaps(options, ctx) {
6777
7239
  const cwd = ctx.cwd ?? process.cwd();
6778
7240
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
6779
- const paths = basouPaths13(repositoryRoot);
7241
+ const paths = basouPaths14(repositoryRoot);
6780
7242
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
6781
7243
  const summary = await findReviewGaps({
6782
7244
  paths,
@@ -6859,23 +7321,25 @@ function renderReviewGaps(summary) {
6859
7321
 
6860
7322
  // src/commands/run.ts
6861
7323
  import { mkdir as mkdir2 } from "fs/promises";
6862
- import { homedir as homedir8 } from "os";
6863
- import { join as join11 } from "path";
7324
+ import { homedir as homedir11 } from "os";
7325
+ import { join as join13 } from "path";
6864
7326
  import {
6865
7327
  acquireLock as acquireLock5,
6866
- assertBasouRootSafe as assertBasouRootSafe11,
6867
- basouPaths as basouPaths14,
7328
+ assertBasouRootSafe as assertBasouRootSafe12,
7329
+ basouPaths as basouPaths15,
6868
7330
  ChildProcessRunner as ChildProcessRunner2,
6869
7331
  claudeCodeAdapterMetadata,
7332
+ codexAdapterMetadata,
6870
7333
  appendChainedEvent as coreAppendChainedEvent2,
6871
7334
  finalizeSessionYaml as finalizeSessionYaml2,
6872
7335
  getDiff,
6873
7336
  getSnapshot as getSnapshot2,
6874
7337
  overwriteYamlFile as overwriteYamlFile2,
6875
7338
  prefixedUlid as prefixedUlid4,
6876
- readManifest as readManifest7,
7339
+ readManifest as readManifest8,
6877
7340
  readYamlFile as readYamlFile6,
6878
7341
  resolveClaudeCodeCommand,
7342
+ resolveCodexCommand,
6879
7343
  resolveRepositoryRoot as resolveRepositoryRoot10,
6880
7344
  SessionSchema as SessionSchema2,
6881
7345
  sanitizeRelatedFiles,
@@ -6883,50 +7347,72 @@ import {
6883
7347
  writeYamlFile as writeYamlFile2
6884
7348
  } from "@basou/core";
6885
7349
  function registerRunCommand(program, ctx = {}) {
6886
- const runCommand = program.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions().option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes");
6887
- runCommand.command("claude-code [args...]").description("Run Claude Code CLI as a Basou-tracked session").option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes").passThroughOptions().action(async (args, options, command) => {
7350
+ const runCommand = addRunOptions(
7351
+ program.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions()
7352
+ );
7353
+ const dispatch = async (args, options, command, run) => {
6888
7354
  const parentOptions = command.parent?.opts() ?? {};
6889
7355
  const snapshotOn = parentOptions.snapshot !== false && options.snapshot !== false;
6890
- const merged = {
6891
- ...parentOptions,
6892
- ...options,
6893
- snapshot: snapshotOn
6894
- };
7356
+ const merged = { ...parentOptions, ...options, snapshot: snapshotOn };
6895
7357
  try {
6896
- const exitCode = await runClaudeCode(args, merged, ctx);
7358
+ const exitCode = await run(args, merged, ctx);
6897
7359
  process.exit(exitCode);
6898
7360
  } catch (error) {
6899
7361
  renderCliError(error, { verbose: isVerbose(merged) });
6900
7362
  process.exit(1);
6901
7363
  }
7364
+ };
7365
+ addRunOptions(runCommand.command("claude-code [args...]")).description("Run Claude Code CLI as a Basou-tracked session").passThroughOptions().action(
7366
+ (args, options, command) => dispatch(args, options, command, runClaudeCode)
7367
+ );
7368
+ addRunOptions(runCommand.command("codex [args...]")).description("Run the Codex CLI as a Basou-tracked session").passThroughOptions().action(
7369
+ (args, options, command) => dispatch(args, options, command, runCodex)
7370
+ );
7371
+ }
7372
+ function addRunOptions(command) {
7373
+ return command.option("--no-snapshot", "Skip git_snapshot before/after the session").option("--cwd <path>", "Run from a Basou root other than process.cwd()").option("-v, --verbose", "Show error causes");
7374
+ }
7375
+ function runClaudeCode(args, options, ctx = {}) {
7376
+ return runTrackedTool(args, options, ctx, {
7377
+ resolveCommand: ctx.resolveCommand ?? resolveClaudeCodeCommand,
7378
+ metadata: claudeCodeAdapterMetadata
6902
7379
  });
6903
7380
  }
6904
- async function runClaudeCode(args, options, ctx = {}) {
7381
+ function runCodex(args, options, ctx = {}) {
7382
+ return runTrackedTool(args, options, ctx, {
7383
+ resolveCommand: ctx.resolveCodexCommand ?? resolveCodexCommand,
7384
+ metadata: codexAdapterMetadata,
7385
+ transformArgs: (a) => ["-c", "shell_environment_policy.inherit=all", ...a],
7386
+ preSpawn: syncCodexOrientationChannelPreSpawn
7387
+ });
7388
+ }
7389
+ async function runTrackedTool(args, options, ctx, adapter) {
6905
7390
  const runner = ctx.runner ?? new ChildProcessRunner2();
6906
7391
  const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
6907
- const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
6908
7392
  const getDiffFn = ctx.getDiff ?? getDiff;
6909
- const { command } = await resolveCommand();
7393
+ const { command } = await adapter.resolveCommand();
7394
+ const childArgs = adapter.transformArgs ? adapter.transformArgs(args) : args;
6910
7395
  const cwd = options.cwd ?? process.cwd();
6911
7396
  const repoRoot = await resolveRepositoryRootForRun(cwd);
6912
- const paths = basouPaths14(repoRoot);
6913
- await assertBasouRootSafe11(paths.root);
6914
- const manifest = await readManifest7(paths);
7397
+ const paths = basouPaths15(repoRoot);
7398
+ await assertBasouRootSafe12(paths.root);
7399
+ const manifest = await readManifest8(paths);
6915
7400
  const sessionId = prefixedUlid4("ses");
6916
- const sessionDir = join11(paths.sessions, sessionId);
7401
+ const sessionDir = join13(paths.sessions, sessionId);
6917
7402
  await mkdir2(sessionDir, { recursive: true });
6918
7403
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
6919
7404
  await coreAppendChainedEvent2(paths, sessionId, event);
6920
7405
  });
6921
7406
  const startedAt = now().toISOString();
6922
- const sessionYamlPath = join11(sessionDir, "session.yaml");
7407
+ const sessionYamlPath = join13(sessionDir, "session.yaml");
6923
7408
  const session = buildInitialSession2({
6924
7409
  id: sessionId,
6925
7410
  command,
6926
- args,
7411
+ args: childArgs,
6927
7412
  cwd: repoRoot,
6928
7413
  workspaceId: manifest.workspace.id,
6929
- startedAt
7414
+ startedAt,
7415
+ source: adapter.metadata
6930
7416
  });
6931
7417
  await writeYamlFile2(sessionYamlPath, session);
6932
7418
  await appendEvent(sessionDir, {
@@ -6935,7 +7421,7 @@ async function runClaudeCode(args, options, ctx = {}) {
6935
7421
  id: prefixedUlid4("evt"),
6936
7422
  session_id: sessionId,
6937
7423
  occurred_at: startedAt,
6938
- source: claudeCodeAdapterMetadata.kind
7424
+ source: adapter.metadata.kind
6939
7425
  });
6940
7426
  let preSnapshot = null;
6941
7427
  if (options.snapshot !== false) {
@@ -6948,7 +7434,7 @@ async function runClaudeCode(args, options, ctx = {}) {
6948
7434
  id: prefixedUlid4("evt"),
6949
7435
  session_id: sessionId,
6950
7436
  occurred_at: runningAt,
6951
- source: claudeCodeAdapterMetadata.kind,
7437
+ source: adapter.metadata.kind,
6952
7438
  from: "initialized",
6953
7439
  to: "running"
6954
7440
  });
@@ -6982,10 +7468,14 @@ async function runClaudeCode(args, options, ctx = {}) {
6982
7468
  process.on("SIGTERM", onSigTerm);
6983
7469
  process.on("exit", exitHandler);
6984
7470
  ctx.onExitHookInstalled?.(exitHandler);
7471
+ if (adapter.preSpawn !== void 0) {
7472
+ const line = await adapter.preSpawn(cwd, ctx);
7473
+ if (line !== null) console.log(line);
7474
+ }
6985
7475
  let result;
6986
7476
  try {
6987
7477
  try {
6988
- result = await runner.run(command, args, {
7478
+ result = await runner.run(command, childArgs, {
6989
7479
  cwd: repoRoot,
6990
7480
  capture: "none",
6991
7481
  signal: controller.signal,
@@ -6996,10 +7486,11 @@ async function runClaudeCode(args, options, ctx = {}) {
6996
7486
  } catch (spawnError) {
6997
7487
  await finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, {
6998
7488
  command,
6999
- args,
7489
+ args: childArgs,
7000
7490
  cwd: repoRoot,
7001
7491
  occurredAt: now().toISOString(),
7002
- signalReceived
7492
+ signalReceived,
7493
+ sourceKind: adapter.metadata.kind
7003
7494
  });
7004
7495
  throw spawnError;
7005
7496
  }
@@ -7018,7 +7509,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7018
7509
  occurred_at: endedAt,
7019
7510
  source: "terminal-recording",
7020
7511
  command,
7021
- args,
7512
+ args: childArgs,
7022
7513
  cwd: repoRoot,
7023
7514
  exit_code: result.exit_code,
7024
7515
  ...result.signal !== null ? { signal: result.signal } : {},
@@ -7045,7 +7536,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7045
7536
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
7046
7537
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
7047
7538
  workingDirectory: repoRoot,
7048
- homedir: homedir8()
7539
+ homedir: homedir11()
7049
7540
  }).sanitized;
7050
7541
  const finalStatus = decideFinalStatus2(result, signalReceived);
7051
7542
  await appendEvent(sessionDir, {
@@ -7054,7 +7545,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7054
7545
  id: prefixedUlid4("evt"),
7055
7546
  session_id: sessionId,
7056
7547
  occurred_at: endedAt,
7057
- source: claudeCodeAdapterMetadata.kind,
7548
+ source: adapter.metadata.kind,
7058
7549
  from: "running",
7059
7550
  to: finalStatus
7060
7551
  });
@@ -7064,7 +7555,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7064
7555
  id: prefixedUlid4("evt"),
7065
7556
  session_id: sessionId,
7066
7557
  occurred_at: endedAt,
7067
- source: claudeCodeAdapterMetadata.kind,
7558
+ source: adapter.metadata.kind,
7068
7559
  ...result.exit_code !== null ? { exit_code: result.exit_code } : {}
7069
7560
  });
7070
7561
  await finalizeSessionYaml2(paths, sessionId, (s) => {
@@ -7186,10 +7677,10 @@ function buildInitialSession2(input) {
7186
7677
  label: `basou run ${cmdline} (${input.startedAt})`,
7187
7678
  task_id: null,
7188
7679
  workspace_id: input.workspaceId,
7189
- source: { ...claudeCodeAdapterMetadata },
7680
+ source: { ...input.source },
7190
7681
  started_at: input.startedAt,
7191
7682
  status: "initialized",
7192
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir8() }),
7683
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir11() }),
7193
7684
  invocation: {
7194
7685
  command: input.command,
7195
7686
  args: [...input.args],
@@ -7229,7 +7720,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
7229
7720
  id: prefixedUlid4("evt"),
7230
7721
  session_id: sessionId,
7231
7722
  occurred_at: ctx.occurredAt,
7232
- source: claudeCodeAdapterMetadata.kind,
7723
+ source: ctx.sourceKind,
7233
7724
  from: "running",
7234
7725
  to: "failed"
7235
7726
  });
@@ -7239,7 +7730,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
7239
7730
  id: prefixedUlid4("evt"),
7240
7731
  session_id: sessionId,
7241
7732
  occurred_at: ctx.occurredAt,
7242
- source: claudeCodeAdapterMetadata.kind
7733
+ source: ctx.sourceKind
7243
7734
  });
7244
7735
  await finalizeSessionYaml2(paths, sessionId, (s) => {
7245
7736
  s.session.status = "failed";
@@ -7259,21 +7750,34 @@ async function resolveRepositoryRootForRun(cwd) {
7259
7750
  throw error;
7260
7751
  }
7261
7752
  }
7753
+ async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
7754
+ try {
7755
+ const root = await resolveBasouRootForCommand(cwd, "run");
7756
+ const paths = basouPaths15(root);
7757
+ const rendered = await renderOrientationToCodexChannel({
7758
+ orientationPath: paths.files.orientation,
7759
+ ...ctx.codexChannelPath !== void 0 ? { channelPath: ctx.codexChannelPath } : {}
7760
+ });
7761
+ return rendered === null ? null : rendered.line;
7762
+ } catch {
7763
+ return null;
7764
+ }
7765
+ }
7262
7766
 
7263
7767
  // src/commands/session.ts
7264
- import { readFile as readFile5 } from "fs/promises";
7265
- import { basename as basename6, isAbsolute as isAbsolute6, join as join12, relative as relative3 } from "path";
7768
+ import { readFile as readFile6 } from "fs/promises";
7769
+ import { basename as basename6, isAbsolute as isAbsolute6, join as join14, relative as relative3 } from "path";
7266
7770
  import {
7267
7771
  acquireLock as acquireLock6,
7268
7772
  appendEventToExistingSession as appendEventToExistingSession3,
7269
- assertBasouRootSafe as assertBasouRootSafe12,
7270
- basouPaths as basouPaths15,
7773
+ assertBasouRootSafe as assertBasouRootSafe13,
7774
+ basouPaths as basouPaths16,
7271
7775
  enumerateSessionDirs as enumerateSessionDirs2,
7272
- findErrorCode as findErrorCode11,
7776
+ findErrorCode as findErrorCode12,
7273
7777
  importSessionFromJson as importSessionFromJson2,
7274
7778
  loadSessionEntries as loadSessionEntries2,
7275
7779
  readAllEvents,
7276
- readManifest as readManifest8,
7780
+ readManifest as readManifest9,
7277
7781
  readYamlFile as readYamlFile7,
7278
7782
  rechainSessionInPlace,
7279
7783
  resolveSessionId as resolveSessionId3,
@@ -7332,8 +7836,8 @@ async function runSessionList(options, ctx = {}) {
7332
7836
  async function doRunSessionList(options, ctx) {
7333
7837
  const cwd = ctx.cwd ?? process.cwd();
7334
7838
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
7335
- const paths = basouPaths15(repositoryRoot);
7336
- await assertWorkspaceInitialized10(paths.root);
7839
+ const paths = basouPaths16(repositoryRoot);
7840
+ await assertWorkspaceInitialized11(paths.root);
7337
7841
  const now = /* @__PURE__ */ new Date();
7338
7842
  const records = (await loadSessionEntries2(paths, {
7339
7843
  now,
@@ -7384,17 +7888,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
7384
7888
  async function doRunSessionShow(idInput, options, ctx) {
7385
7889
  const cwd = ctx.cwd ?? process.cwd();
7386
7890
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
7387
- const paths = basouPaths15(repositoryRoot);
7388
- await assertWorkspaceInitialized10(paths.root);
7891
+ const paths = basouPaths16(repositoryRoot);
7892
+ await assertWorkspaceInitialized11(paths.root);
7389
7893
  const sessionId = await resolveSessionId3(paths, idInput);
7390
- const sessionDir = join12(paths.sessions, sessionId);
7391
- const sessionYamlPath = join12(sessionDir, "session.yaml");
7894
+ const sessionDir = join14(paths.sessions, sessionId);
7895
+ const sessionYamlPath = join14(sessionDir, "session.yaml");
7392
7896
  let session;
7393
7897
  try {
7394
7898
  const raw = await readYamlFile7(sessionYamlPath);
7395
7899
  session = SessionSchema3.parse(raw);
7396
7900
  } catch (error) {
7397
- if (findErrorCode11(error, "ENOENT")) {
7901
+ if (findErrorCode12(error, "ENOENT")) {
7398
7902
  throw new Error(`Session not found: ${idInput}`);
7399
7903
  }
7400
7904
  throw new Error("Failed to read session", { cause: error });
@@ -7588,6 +8092,11 @@ function eventVariantSummary(ev) {
7588
8092
  return `task ${shortTaskId2(ev.task_id)}: ${ev.title} (archived)`;
7589
8093
  case "note_added":
7590
8094
  return ev.body.length > 80 ? `${ev.body.slice(0, 77)}...` : ev.body;
8095
+ case "review_recorded": {
8096
+ const verdict = ev.verdict !== void 0 ? ` (${ev.verdict})` : "";
8097
+ const blocked = ev.blocked !== void 0 && ev.blocked.length > 0 ? ` ${ev.blocked.length} blocked` : "";
8098
+ return `${ev.reviewer} -> ${ev.target}${verdict}${blocked}`;
8099
+ }
7591
8100
  case "adapter_output":
7592
8101
  return `${ev.stream} "${ev.summary}" raw_ref=${ev.raw_ref}`;
7593
8102
  }
@@ -7635,11 +8144,11 @@ function maxLen2(values, floor) {
7635
8144
  async function resolveRepositoryRootForSession(cwd, subcmd) {
7636
8145
  return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
7637
8146
  }
7638
- async function assertWorkspaceInitialized10(basouRoot) {
8147
+ async function assertWorkspaceInitialized11(basouRoot) {
7639
8148
  try {
7640
- await assertBasouRootSafe12(basouRoot);
8149
+ await assertBasouRootSafe13(basouRoot);
7641
8150
  } catch (error) {
7642
- if (findErrorCode11(error, "ENOENT")) {
8151
+ if (findErrorCode12(error, "ENOENT")) {
7643
8152
  throw new Error("Workspace not initialized. Run 'basou init' first.");
7644
8153
  }
7645
8154
  throw error;
@@ -7677,9 +8186,9 @@ async function runSessionImport(options, ctx = {}) {
7677
8186
  async function doRunSessionImport(options, ctx) {
7678
8187
  const cwd = ctx.cwd ?? process.cwd();
7679
8188
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
7680
- const paths = basouPaths15(repositoryRoot);
7681
- await assertWorkspaceInitialized10(paths.root);
7682
- const manifest = await readManifest8(paths);
8189
+ const paths = basouPaths16(repositoryRoot);
8190
+ await assertWorkspaceInitialized11(paths.root);
8191
+ const manifest = await readManifest9(paths);
7683
8192
  const rawBody = await readInputFile(options.from);
7684
8193
  const json = parseJsonStrict(rawBody);
7685
8194
  const parsed = SessionImportPayloadSchema2.safeParse(json);
@@ -7706,12 +8215,12 @@ async function doRunSessionImport(options, ctx) {
7706
8215
  }
7707
8216
  async function readInputFile(path) {
7708
8217
  try {
7709
- return await readFile5(path, "utf8");
8218
+ return await readFile6(path, "utf8");
7710
8219
  } catch (error) {
7711
- if (findErrorCode11(error, "ENOENT")) {
8220
+ if (findErrorCode12(error, "ENOENT")) {
7712
8221
  throw new Error("Import source not found", { cause: error });
7713
8222
  }
7714
- if (findErrorCode11(error, "EISDIR")) {
8223
+ if (findErrorCode12(error, "EISDIR")) {
7715
8224
  throw new Error("Import source is not a file", { cause: error });
7716
8225
  }
7717
8226
  throw new Error("Failed to read import source", { cause: error });
@@ -7791,8 +8300,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
7791
8300
  }
7792
8301
  const cwd = ctx.cwd ?? process.cwd();
7793
8302
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
7794
- const paths = basouPaths15(repositoryRoot);
7795
- await assertWorkspaceInitialized10(paths.root);
8303
+ const paths = basouPaths16(repositoryRoot);
8304
+ await assertWorkspaceInitialized11(paths.root);
7796
8305
  const sessionId = await resolveSessionId3(paths, sessionIdInput);
7797
8306
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
7798
8307
  if (body.length === 0) {
@@ -7823,12 +8332,12 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
7823
8332
  }
7824
8333
  async function readNoteFile(path) {
7825
8334
  try {
7826
- return await readFile5(path, "utf8");
8335
+ return await readFile6(path, "utf8");
7827
8336
  } catch (error) {
7828
- if (findErrorCode11(error, "ENOENT")) {
8337
+ if (findErrorCode12(error, "ENOENT")) {
7829
8338
  throw new Error("Note source not found", { cause: error });
7830
8339
  }
7831
- if (findErrorCode11(error, "EISDIR")) {
8340
+ if (findErrorCode12(error, "EISDIR")) {
7832
8341
  throw new Error("Note source is not a file", { cause: error });
7833
8342
  }
7834
8343
  throw new Error("Failed to read note source", { cause: error });
@@ -7873,8 +8382,8 @@ async function doRunSessionRechain(options, ctx) {
7873
8382
  }
7874
8383
  const cwd = ctx.cwd ?? process.cwd();
7875
8384
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
7876
- const paths = basouPaths15(repositoryRoot);
7877
- await assertWorkspaceInitialized10(paths.root);
8385
+ const paths = basouPaths16(repositoryRoot);
8386
+ await assertWorkspaceInitialized11(paths.root);
7878
8387
  const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
7879
8388
  const dryRun = options.dryRun === true;
7880
8389
  const rows = [];
@@ -7927,10 +8436,10 @@ function renderRechainRow(row, dryRun) {
7927
8436
 
7928
8437
  // src/commands/stats.ts
7929
8438
  import {
7930
- assertBasouRootSafe as assertBasouRootSafe13,
7931
- basouPaths as basouPaths16,
8439
+ assertBasouRootSafe as assertBasouRootSafe14,
8440
+ basouPaths as basouPaths17,
7932
8441
  computeWorkStats,
7933
- findErrorCode as findErrorCode12,
8442
+ findErrorCode as findErrorCode13,
7934
8443
  resolveRepositoryRoot as resolveRepositoryRoot11
7935
8444
  } from "@basou/core";
7936
8445
  function registerStatsCommand(program) {
@@ -7949,8 +8458,8 @@ async function runStats(options, ctx = {}) {
7949
8458
  async function doRunStats(options, ctx) {
7950
8459
  const cwd = ctx.cwd ?? process.cwd();
7951
8460
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
7952
- const paths = basouPaths16(repositoryRoot);
7953
- await assertWorkspaceInitialized11(paths.root);
8461
+ const paths = basouPaths17(repositoryRoot);
8462
+ await assertWorkspaceInitialized12(paths.root);
7954
8463
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
7955
8464
  const result = await computeWorkStats({
7956
8465
  paths,
@@ -8044,11 +8553,11 @@ async function resolveRepositoryRootForStats(cwd) {
8044
8553
  throw error;
8045
8554
  }
8046
8555
  }
8047
- async function assertWorkspaceInitialized11(basouRoot) {
8556
+ async function assertWorkspaceInitialized12(basouRoot) {
8048
8557
  try {
8049
- await assertBasouRootSafe13(basouRoot);
8558
+ await assertBasouRootSafe14(basouRoot);
8050
8559
  } catch (error) {
8051
- if (findErrorCode12(error, "ENOENT")) {
8560
+ if (findErrorCode13(error, "ENOENT")) {
8052
8561
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8053
8562
  }
8054
8563
  throw error;
@@ -8057,11 +8566,11 @@ async function assertWorkspaceInitialized11(basouRoot) {
8057
8566
 
8058
8567
  // src/commands/status.ts
8059
8568
  import {
8060
- assertBasouRootSafe as assertBasouRootSafe14,
8061
- basouPaths as basouPaths17,
8569
+ assertBasouRootSafe as assertBasouRootSafe15,
8570
+ basouPaths as basouPaths18,
8062
8571
  buildStatusSnapshot,
8063
- findErrorCode as findErrorCode13,
8064
- readManifest as readManifest9,
8572
+ findErrorCode as findErrorCode14,
8573
+ readManifest as readManifest10,
8065
8574
  resolveRepositoryRoot as resolveRepositoryRoot12,
8066
8575
  writeStatus
8067
8576
  } from "@basou/core";
@@ -8081,20 +8590,20 @@ async function runStatus(options, ctx = {}) {
8081
8590
  async function doRunStatus(options, ctx) {
8082
8591
  const cwd = ctx.cwd ?? process.cwd();
8083
8592
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
8084
- const paths = basouPaths17(repositoryRoot);
8593
+ const paths = basouPaths18(repositoryRoot);
8085
8594
  try {
8086
- await assertBasouRootSafe14(paths.root);
8595
+ await assertBasouRootSafe15(paths.root);
8087
8596
  } catch (error) {
8088
- if (findErrorCode13(error, "ENOENT")) {
8597
+ if (findErrorCode14(error, "ENOENT")) {
8089
8598
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8090
8599
  }
8091
8600
  throw error;
8092
8601
  }
8093
8602
  let manifest;
8094
8603
  try {
8095
- manifest = await readManifest9(paths);
8604
+ manifest = await readManifest10(paths);
8096
8605
  } catch (error) {
8097
- if (findErrorCode13(error, "ENOENT")) {
8606
+ if (findErrorCode14(error, "ENOENT")) {
8098
8607
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8099
8608
  }
8100
8609
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -8130,21 +8639,21 @@ async function resolveRepositoryRootForStatus(cwd) {
8130
8639
  }
8131
8640
 
8132
8641
  // src/commands/task.ts
8133
- import { readFile as readFile6 } from "fs/promises";
8134
- import { join as join13 } from "path";
8642
+ import { readFile as readFile7 } from "fs/promises";
8643
+ import { join as join15 } from "path";
8135
8644
  import {
8136
8645
  archiveTask,
8137
- assertBasouRootSafe as assertBasouRootSafe15,
8138
- basouPaths as basouPaths18,
8646
+ assertBasouRootSafe as assertBasouRootSafe16,
8647
+ basouPaths as basouPaths19,
8139
8648
  createTaskWithEvent,
8140
8649
  deleteTask,
8141
8650
  editTask,
8142
8651
  enumerateArchivedTaskIds,
8143
- findErrorCode as findErrorCode14,
8652
+ findErrorCode as findErrorCode15,
8144
8653
  loadSessionEntries as loadSessionEntries3,
8145
8654
  loadTaskEntries,
8146
8655
  prefixedUlid as prefixedUlid5,
8147
- readManifest as readManifest10,
8656
+ readManifest as readManifest11,
8148
8657
  readTaskFile,
8149
8658
  readTaskFileWithArchiveFallback,
8150
8659
  reconcileAllTasks,
@@ -8236,8 +8745,8 @@ async function doRunTaskNew(options, ctx) {
8236
8745
  }
8237
8746
  const cwd = ctx.cwd ?? process.cwd();
8238
8747
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
8239
- const paths = basouPaths18(repositoryRoot);
8240
- await assertWorkspaceInitialized12(paths.root);
8748
+ const paths = basouPaths19(repositoryRoot);
8749
+ await assertWorkspaceInitialized13(paths.root);
8241
8750
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
8242
8751
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
8243
8752
  const occurredAt = now.toISOString();
@@ -8271,7 +8780,7 @@ async function doRunTaskNew(options, ctx) {
8271
8780
  });
8272
8781
  return;
8273
8782
  }
8274
- const manifest = await readManifest10(paths);
8783
+ const manifest = await readManifest11(paths);
8275
8784
  const result = await createTaskWithEvent({
8276
8785
  mode: "ad-hoc",
8277
8786
  paths,
@@ -8345,8 +8854,8 @@ async function runTaskList(options, ctx = {}) {
8345
8854
  async function doRunTaskList(options, ctx) {
8346
8855
  const cwd = ctx.cwd ?? process.cwd();
8347
8856
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
8348
- const paths = basouPaths18(repositoryRoot);
8349
- await assertWorkspaceInitialized12(paths.root);
8857
+ const paths = basouPaths19(repositoryRoot);
8858
+ await assertWorkspaceInitialized13(paths.root);
8350
8859
  const entries = await loadTaskEntries(paths, {
8351
8860
  onSkip: (id, reason) => printTaskSkip(id, reason)
8352
8861
  });
@@ -8449,15 +8958,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
8449
8958
  async function doRunTaskShow(idInput, options, ctx) {
8450
8959
  const cwd = ctx.cwd ?? process.cwd();
8451
8960
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
8452
- const paths = basouPaths18(repositoryRoot);
8453
- await assertWorkspaceInitialized12(paths.root);
8961
+ const paths = basouPaths19(repositoryRoot);
8962
+ await assertWorkspaceInitialized13(paths.root);
8454
8963
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
8455
8964
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
8456
8965
  const sessions = await loadSessionEntries3(paths, { now: /* @__PURE__ */ new Date() });
8457
8966
  const events = [];
8458
8967
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
8459
8968
  for (const s of sessions) {
8460
- const sessionDir = join13(paths.sessions, s.sessionId);
8969
+ const sessionDir = join15(paths.sessions, s.sessionId);
8461
8970
  try {
8462
8971
  for await (const ev of replayEvents3(sessionDir, {
8463
8972
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -8593,8 +9102,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
8593
9102
  const newStatus = parseTaskStatusPositional(newStatusInput);
8594
9103
  const cwd = ctx.cwd ?? process.cwd();
8595
9104
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
8596
- const paths = basouPaths18(repositoryRoot);
8597
- await assertWorkspaceInitialized12(paths.root);
9105
+ const paths = basouPaths19(repositoryRoot);
9106
+ await assertWorkspaceInitialized13(paths.root);
8598
9107
  const taskId = await resolveTaskId2(paths, taskIdInput);
8599
9108
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
8600
9109
  const occurredAt = now.toISOString();
@@ -8619,7 +9128,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
8619
9128
  });
8620
9129
  return;
8621
9130
  }
8622
- const manifest = await readManifest10(paths);
9131
+ const manifest = await readManifest11(paths);
8623
9132
  const result = await updateTaskStatusWithEvent({
8624
9133
  mode: "ad-hoc",
8625
9134
  paths,
@@ -8670,9 +9179,9 @@ async function runTaskReconcile(options, ctx = {}) {
8670
9179
  async function doRunTaskReconcile(options, ctx) {
8671
9180
  const cwd = ctx.cwd ?? process.cwd();
8672
9181
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
8673
- const paths = basouPaths18(repositoryRoot);
8674
- await assertWorkspaceInitialized12(paths.root);
8675
- const manifest = await readManifest10(paths);
9182
+ const paths = basouPaths19(repositoryRoot);
9183
+ await assertWorkspaceInitialized13(paths.root);
9184
+ const manifest = await readManifest11(paths);
8676
9185
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
8677
9186
  const write = options.write === true;
8678
9187
  const verbose = isVerbose(options);
@@ -8850,9 +9359,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
8850
9359
  }
8851
9360
  const cwd = ctx.cwd ?? process.cwd();
8852
9361
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
8853
- const paths = basouPaths18(repositoryRoot);
8854
- await assertWorkspaceInitialized12(paths.root);
8855
- const manifest = await readManifest10(paths);
9362
+ const paths = basouPaths19(repositoryRoot);
9363
+ await assertWorkspaceInitialized13(paths.root);
9364
+ const manifest = await readManifest11(paths);
8856
9365
  const taskId = await resolveTaskId2(paths, taskIdInput);
8857
9366
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
8858
9367
  const write = options.write === true;
@@ -8930,9 +9439,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
8930
9439
  }
8931
9440
  const cwd = ctx.cwd ?? process.cwd();
8932
9441
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
8933
- const paths = basouPaths18(repositoryRoot);
8934
- await assertWorkspaceInitialized12(paths.root);
8935
- const manifest = await readManifest10(paths);
9442
+ const paths = basouPaths19(repositoryRoot);
9443
+ await assertWorkspaceInitialized13(paths.root);
9444
+ const manifest = await readManifest11(paths);
8936
9445
  const taskId = await resolveTaskId2(paths, taskIdInput);
8937
9446
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
8938
9447
  const occurredAt = now.toISOString();
@@ -8986,9 +9495,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
8986
9495
  }
8987
9496
  const cwd = ctx.cwd ?? process.cwd();
8988
9497
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
8989
- const paths = basouPaths18(repositoryRoot);
8990
- await assertWorkspaceInitialized12(paths.root);
8991
- const manifest = await readManifest10(paths);
9498
+ const paths = basouPaths19(repositoryRoot);
9499
+ await assertWorkspaceInitialized13(paths.root);
9500
+ const manifest = await readManifest11(paths);
8992
9501
  const taskId = await resolveTaskId2(paths, taskIdInput);
8993
9502
  if (options.yes !== true) {
8994
9503
  await confirmDestructiveAction("delete", taskId);
@@ -9031,9 +9540,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
9031
9540
  }
9032
9541
  const cwd = ctx.cwd ?? process.cwd();
9033
9542
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
9034
- const paths = basouPaths18(repositoryRoot);
9035
- await assertWorkspaceInitialized12(paths.root);
9036
- const manifest = await readManifest10(paths);
9543
+ const paths = basouPaths19(repositoryRoot);
9544
+ await assertWorkspaceInitialized13(paths.root);
9545
+ const manifest = await readManifest11(paths);
9037
9546
  const taskId = await resolveTaskId2(paths, taskIdInput);
9038
9547
  if (options.yes !== true) {
9039
9548
  await confirmDestructiveAction("archive", taskId);
@@ -9145,12 +9654,12 @@ function parsePositiveInt2(raw) {
9145
9654
  }
9146
9655
  async function readDescriptionFile(path) {
9147
9656
  try {
9148
- return await readFile6(path, "utf8");
9657
+ return await readFile7(path, "utf8");
9149
9658
  } catch (error) {
9150
- if (findErrorCode14(error, "ENOENT")) {
9659
+ if (findErrorCode15(error, "ENOENT")) {
9151
9660
  throw new Error("Description source not found", { cause: error });
9152
9661
  }
9153
- if (findErrorCode14(error, "EISDIR")) {
9662
+ if (findErrorCode15(error, "EISDIR")) {
9154
9663
  throw new Error("Description source is not a file", { cause: error });
9155
9664
  }
9156
9665
  throw new Error("Failed to read description source", { cause: error });
@@ -9159,11 +9668,11 @@ async function readDescriptionFile(path) {
9159
9668
  async function resolveRepositoryRootForTask(cwd, subcmd) {
9160
9669
  return resolveBasouRootForCommand(cwd, `task ${subcmd}`);
9161
9670
  }
9162
- async function assertWorkspaceInitialized12(basouRoot) {
9671
+ async function assertWorkspaceInitialized13(basouRoot) {
9163
9672
  try {
9164
- await assertBasouRootSafe15(basouRoot);
9673
+ await assertBasouRootSafe16(basouRoot);
9165
9674
  } catch (error) {
9166
- if (findErrorCode14(error, "ENOENT")) {
9675
+ if (findErrorCode15(error, "ENOENT")) {
9167
9676
  throw new Error("Workspace not initialized. Run 'basou init' first.");
9168
9677
  }
9169
9678
  throw error;
@@ -9251,10 +9760,10 @@ function maxLen3(values, floor) {
9251
9760
 
9252
9761
  // src/commands/verify.ts
9253
9762
  import {
9254
- assertBasouRootSafe as assertBasouRootSafe16,
9255
- basouPaths as basouPaths19,
9763
+ assertBasouRootSafe as assertBasouRootSafe17,
9764
+ basouPaths as basouPaths20,
9256
9765
  enumerateSessionDirs as enumerateSessionDirs3,
9257
- findErrorCode as findErrorCode15,
9766
+ findErrorCode as findErrorCode16,
9258
9767
  resolveRepositoryRoot as resolveRepositoryRoot13,
9259
9768
  resolveSessionId as resolveSessionId5,
9260
9769
  verifyEventsChain
@@ -9278,8 +9787,8 @@ async function doRunVerify(options, ctx) {
9278
9787
  }
9279
9788
  const cwd = ctx.cwd ?? process.cwd();
9280
9789
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
9281
- const paths = basouPaths19(repositoryRoot);
9282
- await assertWorkspaceInitialized13(paths.root);
9790
+ const paths = basouPaths20(repositoryRoot);
9791
+ await assertWorkspaceInitialized14(paths.root);
9283
9792
  const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
9284
9793
  const rows = [];
9285
9794
  for (const sessionId of sessionIds) {
@@ -9336,11 +9845,11 @@ async function resolveRepositoryRootForVerify(cwd) {
9336
9845
  throw error;
9337
9846
  }
9338
9847
  }
9339
- async function assertWorkspaceInitialized13(basouRoot) {
9848
+ async function assertWorkspaceInitialized14(basouRoot) {
9340
9849
  try {
9341
- await assertBasouRootSafe16(basouRoot);
9850
+ await assertBasouRootSafe17(basouRoot);
9342
9851
  } catch (error) {
9343
- if (findErrorCode15(error, "ENOENT")) {
9852
+ if (findErrorCode16(error, "ENOENT")) {
9344
9853
  throw new Error("Workspace not initialized. Run 'basou init' first.");
9345
9854
  }
9346
9855
  throw error;
@@ -9350,12 +9859,12 @@ async function assertWorkspaceInitialized13(basouRoot) {
9350
9859
  // src/commands/view.ts
9351
9860
  import { spawn } from "child_process";
9352
9861
  import { createHash } from "crypto";
9353
- import { basename as basename7, resolve as resolve11 } from "path";
9862
+ import { basename as basename7, resolve as resolve12 } from "path";
9354
9863
  import {
9355
- assertBasouRootSafe as assertBasouRootSafe17,
9356
- basouPaths as basouPaths20,
9357
- findErrorCode as findErrorCode17,
9358
- readManifest as readManifest13,
9864
+ assertBasouRootSafe as assertBasouRootSafe18,
9865
+ basouPaths as basouPaths21,
9866
+ findErrorCode as findErrorCode18,
9867
+ readManifest as readManifest14,
9359
9868
  resolveRepositoryRoot as resolveRepositoryRoot14
9360
9869
  } from "@basou/core";
9361
9870
  import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
@@ -9363,9 +9872,9 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
9363
9872
  // src/lib/portfolio-safety.ts
9364
9873
  import { execFile } from "child_process";
9365
9874
  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";
9875
+ import { isAbsolute as isAbsolute7, join as join16, relative as relative4, resolve as resolve11 } from "path";
9367
9876
  import { promisify } from "util";
9368
- import { readManifest as readManifest11 } from "@basou/core";
9877
+ import { readManifest as readManifest12 } from "@basou/core";
9369
9878
  var execFileAsync = promisify(execFile);
9370
9879
  function errorCode(error) {
9371
9880
  return error instanceof Error ? error.code : void 0;
@@ -9374,7 +9883,7 @@ async function canonical(p) {
9374
9883
  try {
9375
9884
  return await realpath2(p);
9376
9885
  } catch {
9377
- return resolve10(p);
9886
+ return resolve11(p);
9378
9887
  }
9379
9888
  }
9380
9889
  function isInside(child, parent) {
@@ -9387,7 +9896,7 @@ function isBasouPath(p) {
9387
9896
  async function inspectRepo(repoPath) {
9388
9897
  let hasEntry = false;
9389
9898
  try {
9390
- await lstat2(join14(repoPath, ".basou"));
9899
+ await lstat2(join16(repoPath, ".basou"));
9391
9900
  hasEntry = true;
9392
9901
  } catch (error) {
9393
9902
  if (errorCode(error) !== "ENOENT") {
@@ -9418,7 +9927,7 @@ async function checkPortfolioSafety(workspaces) {
9418
9927
  const wsReal = await canonical(ws.repoRoot);
9419
9928
  let sourceRoots = [];
9420
9929
  try {
9421
- const manifest = await readManifest11(ws.paths);
9930
+ const manifest = await readManifest12(ws.paths);
9422
9931
  sourceRoots = manifest.import?.source_roots ?? [];
9423
9932
  } catch (error) {
9424
9933
  if (error instanceof Error && error.message === "YAML file not found") {
@@ -9436,7 +9945,7 @@ async function checkPortfolioSafety(workspaces) {
9436
9945
  }
9437
9946
  const monitored = /* @__PURE__ */ new Map();
9438
9947
  for (const root of sourceRoots) {
9439
- const display = resolve10(ws.repoRoot, root);
9948
+ const display = resolve11(ws.repoRoot, root);
9440
9949
  const real = await canonical(display);
9441
9950
  if (real !== wsReal) monitored.set(real, display);
9442
9951
  }
@@ -9488,18 +9997,18 @@ function formatSafetyReport(result) {
9488
9997
 
9489
9998
  // src/lib/view-server.ts
9490
9999
  import { createServer } from "http";
9491
- import { join as join15 } from "path";
10000
+ import { join as join17 } from "path";
9492
10001
  import {
9493
10002
  computeWorkStats as computeWorkStats2,
9494
10003
  enumerateApprovals as enumerateApprovals2,
9495
- findErrorCode as findErrorCode16,
10004
+ findErrorCode as findErrorCode17,
9496
10005
  isLazyExpired as isLazyExpired2,
9497
10006
  loadApproval as loadApproval2,
9498
10007
  loadSessionEntries as loadSessionEntries4,
9499
10008
  loadTaskEntries as loadTaskEntries2,
9500
10009
  readAllEvents as readAllEvents2,
9501
- readManifest as readManifest12,
9502
- readMarkdownFile as readMarkdownFile6,
10010
+ readManifest as readManifest13,
10011
+ readMarkdownFile as readMarkdownFile7,
9503
10012
  readSessionYaml as readSessionYaml3,
9504
10013
  readTaskFile as readTaskFile2,
9505
10014
  renderDecisions as renderDecisions3,
@@ -10149,7 +10658,7 @@ function startViewServer(opts) {
10149
10658
  };
10150
10659
  let boundPort = port;
10151
10660
  const getPort = () => boundPort;
10152
- return new Promise((resolve12, reject) => {
10661
+ return new Promise((resolve13, reject) => {
10153
10662
  const server = createServer((req, res) => {
10154
10663
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
10155
10664
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -10160,7 +10669,7 @@ function startViewServer(opts) {
10160
10669
  const address = server.address();
10161
10670
  boundPort = isAddressInfo(address) ? address.port : port;
10162
10671
  server.off("error", reject);
10163
- resolve12({
10672
+ resolve13({
10164
10673
  url: `http://${host}:${boundPort}`,
10165
10674
  port: boundPort,
10166
10675
  close: () => closeServer(server)
@@ -10172,8 +10681,8 @@ function isAddressInfo(value) {
10172
10681
  return value !== null && typeof value === "object";
10173
10682
  }
10174
10683
  function closeServer(server) {
10175
- return new Promise((resolve12) => {
10176
- server.close(() => resolve12());
10684
+ return new Promise((resolve13) => {
10685
+ server.close(() => resolve13());
10177
10686
  server.closeAllConnections();
10178
10687
  });
10179
10688
  }
@@ -10390,9 +10899,9 @@ async function captureStaleness(ws, nowIso) {
10390
10899
  async function overview(ws, nowProvider) {
10391
10900
  let manifest;
10392
10901
  try {
10393
- manifest = await readManifest12(ws.paths);
10902
+ manifest = await readManifest13(ws.paths);
10394
10903
  } catch (error) {
10395
- if (findErrorCode16(error, "ENOENT")) {
10904
+ if (findErrorCode17(error, "ENOENT")) {
10396
10905
  return { initialized: false, repoRoot: ws.repoRoot };
10397
10906
  }
10398
10907
  throw error;
@@ -10447,7 +10956,7 @@ async function sessionDetail(ws, sessionId) {
10447
10956
  throw error;
10448
10957
  }
10449
10958
  try {
10450
- const events = await readAllEvents2(join15(ws.paths.sessions, sessionId));
10959
+ const events = await readAllEvents2(join17(ws.paths.sessions, sessionId));
10451
10960
  return { session, events };
10452
10961
  } catch {
10453
10962
  return { session, events: [], degraded: true };
@@ -10469,7 +10978,7 @@ async function taskDetail(ws, taskId) {
10469
10978
  }
10470
10979
  }
10471
10980
  async function decisionsView(ws, nowProvider) {
10472
- const fromDisk = await readMarkdownFile6(ws.paths.files.decisions);
10981
+ const fromDisk = await readMarkdownFile7(ws.paths.files.decisions);
10473
10982
  if (fromDisk !== null) {
10474
10983
  return { body: fromDisk, fromDisk: true };
10475
10984
  }
@@ -10492,7 +11001,7 @@ async function approvalsView(ws, nowProvider) {
10492
11001
  return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
10493
11002
  }
10494
11003
  async function handoffView(ws, nowProvider) {
10495
- const fromDisk = await readMarkdownFile6(ws.paths.files.handoff);
11004
+ const fromDisk = await readMarkdownFile7(ws.paths.files.handoff);
10496
11005
  if (fromDisk !== null) {
10497
11006
  return { body: fromDisk, fromDisk: true };
10498
11007
  }
@@ -10661,18 +11170,18 @@ async function doRunView(options, ctx) {
10661
11170
  }
10662
11171
  async function buildSingleDeps(ctx, cwd) {
10663
11172
  const repositoryRoot = await resolveRepositoryRootForView(cwd);
10664
- const paths = basouPaths20(repositoryRoot);
10665
- await assertWorkspaceInitialized14(paths.root);
11173
+ const paths = basouPaths21(repositoryRoot);
11174
+ await assertWorkspaceInitialized15(paths.root);
10666
11175
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
10667
11176
  return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
10668
11177
  }
10669
11178
  async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
10670
- const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve11(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
11179
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve12(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
10671
11180
  const entries = [];
10672
11181
  const seenPath = /* @__PURE__ */ new Set();
10673
11182
  const seenKey = /* @__PURE__ */ new Set();
10674
11183
  for (const spec of specs) {
10675
- const repoRoot = resolve11(spec.path);
11184
+ const repoRoot = resolve12(spec.path);
10676
11185
  if (seenPath.has(repoRoot)) continue;
10677
11186
  seenPath.add(repoRoot);
10678
11187
  const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
@@ -10685,14 +11194,14 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
10685
11194
  return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
10686
11195
  }
10687
11196
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
10688
- const paths = basouPaths20(repoRoot);
11197
+ const paths = basouPaths21(repoRoot);
10689
11198
  const importCtx = {
10690
11199
  cwd: repoRoot,
10691
11200
  ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
10692
11201
  ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
10693
11202
  };
10694
11203
  try {
10695
- const manifest = await readManifest13(paths);
11204
+ const manifest = await readManifest14(paths);
10696
11205
  return {
10697
11206
  key: manifest.workspace.id,
10698
11207
  label: labelOverride ?? manifest.workspace.name,
@@ -10721,7 +11230,7 @@ async function startListening(port, deps) {
10721
11230
  try {
10722
11231
  return await startViewServer({ port, deps });
10723
11232
  } catch (error) {
10724
- if (findErrorCode17(error, "EADDRINUSE")) {
11233
+ if (findErrorCode18(error, "EADDRINUSE")) {
10725
11234
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
10726
11235
  cause: error
10727
11236
  });
@@ -10744,7 +11253,7 @@ function openInBrowser(url, override) {
10744
11253
  }
10745
11254
  }
10746
11255
  function waitForShutdown(signal) {
10747
- return new Promise((resolve12) => {
11256
+ return new Promise((resolve13) => {
10748
11257
  const cleanup = () => {
10749
11258
  process.off("SIGINT", onSignal);
10750
11259
  process.off("SIGTERM", onSignal);
@@ -10752,18 +11261,18 @@ function waitForShutdown(signal) {
10752
11261
  };
10753
11262
  const onSignal = () => {
10754
11263
  cleanup();
10755
- resolve12();
11264
+ resolve13();
10756
11265
  };
10757
11266
  const onAbort = () => {
10758
11267
  cleanup();
10759
- resolve12();
11268
+ resolve13();
10760
11269
  };
10761
11270
  process.on("SIGINT", onSignal);
10762
11271
  process.on("SIGTERM", onSignal);
10763
11272
  if (signal !== void 0) {
10764
11273
  if (signal.aborted) {
10765
11274
  cleanup();
10766
- resolve12();
11275
+ resolve13();
10767
11276
  return;
10768
11277
  }
10769
11278
  signal.addEventListener("abort", onAbort);
@@ -10782,11 +11291,11 @@ async function resolveRepositoryRootForView(cwd) {
10782
11291
  throw error;
10783
11292
  }
10784
11293
  }
10785
- async function assertWorkspaceInitialized14(basouRoot) {
11294
+ async function assertWorkspaceInitialized15(basouRoot) {
10786
11295
  try {
10787
- await assertBasouRootSafe17(basouRoot);
11296
+ await assertBasouRootSafe18(basouRoot);
10788
11297
  } catch (error) {
10789
- if (findErrorCode17(error, "ENOENT")) {
11298
+ if (findErrorCode18(error, "ENOENT")) {
10790
11299
  throw new Error("Workspace not initialized. Run 'basou init' first.");
10791
11300
  }
10792
11301
  throw error;
@@ -10818,6 +11327,7 @@ function buildProgram() {
10818
11327
  registerDecisionsCommand(program);
10819
11328
  registerReportCommand(program);
10820
11329
  registerOrientCommand(program);
11330
+ registerReviewCommand(program);
10821
11331
  registerReviewGapsCommand(program);
10822
11332
  registerProjectCommand(program);
10823
11333
  registerProtocolCommand(program);