@basou/cli 0.29.0 → 0.31.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
@@ -2114,16 +2114,20 @@ function registerHookCommand(program) {
2114
2114
  ).option(
2115
2115
  "--block",
2116
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"
2117
2120
  ).addHelpText("after", HOOK_STOP_HELP).action(async (options) => {
2118
2121
  const minEdits = parseMinEdits(options.minEdits);
2119
2122
  await runHookStop({
2120
2123
  ...minEdits !== void 0 ? { minEdits } : {},
2121
- ...options.block === true ? { block: true } : {}
2124
+ ...options.block === true ? { block: true } : {},
2125
+ ...options.requireReview === true ? { requireReview: true } : {}
2122
2126
  });
2123
2127
  });
2124
2128
  hook.command("install").description(
2125
- "Register the Stop hook in ~/.claude/settings.json (reproducible, idempotent). Default is advisory; --block opts into in-turn enforcement."
2126
- ).option("--block", "Register the blocking (opt-in enforcement) form instead of advisory").option("--min-edits <n>", "Pass a custom file-edit threshold to the registered hook").option("--settings <path>", "Override the settings.json path (intended for tests)").option("--dry-run", "Print what would change without writing").option("-v, --verbose", "Show error causes").action(async (opts) => {
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) => {
2127
2131
  await runHookInstall(opts);
2128
2132
  });
2129
2133
  hook.command("uninstall").description(
@@ -2147,6 +2151,12 @@ Substantive = EITHER >= ${DEFAULT_STOP_HOOK_MIN_EDITS} file edits (default) OR a
2147
2151
  answer (an uncaptured conversational decision). Read-only Bash (ls / grep /
2148
2152
  git status) does NOT count.
2149
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
+
2150
2160
  By default the reminder is non-blocking: Claude sees it and may act on it or
2151
2161
  stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
2152
2162
  returns decision:block, holding the agent in-turn to act on the reminder; the
@@ -2190,11 +2200,17 @@ async function doRunHookStop(options, ctx) {
2190
2200
  stopHookActive: false,
2191
2201
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
2192
2202
  });
2193
- if (evaluation.kind !== "nudge") return;
2194
- const payloadJson = options.block === true ? JSON.stringify({ decision: "block", reason: evaluation.additionalContext }) : JSON.stringify({
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({
2195
2211
  hookSpecificOutput: {
2196
2212
  hookEventName: "Stop",
2197
- additionalContext: evaluation.additionalContext
2213
+ additionalContext: reason
2198
2214
  }
2199
2215
  });
2200
2216
  write(`${payloadJson}
@@ -2247,6 +2263,7 @@ function resolveCliEntry() {
2247
2263
  function normalizeInstallOptions(raw) {
2248
2264
  const out = {};
2249
2265
  if (raw.block === true) out.block = true;
2266
+ if (raw.requireReview === true) out.requireReview = true;
2250
2267
  if (raw.settings !== void 0) out.settings = raw.settings;
2251
2268
  if (raw.dryRun === true) out.dryRun = true;
2252
2269
  if (raw.verbose === true) out.verbose = true;
@@ -2306,9 +2323,13 @@ async function doRunHookInstall(options, ctx = {}) {
2306
2323
  const command = buildStopHookCommand({
2307
2324
  cliEntry,
2308
2325
  ...options.block === true ? { block: true } : {},
2326
+ ...options.requireReview === true ? { requireReview: true } : {},
2309
2327
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
2310
2328
  });
2311
- const mode = options.block === true ? "blocking" : "advisory";
2329
+ const mode = describeHookMode({
2330
+ block: options.block === true,
2331
+ review: options.requireReview === true
2332
+ });
2312
2333
  await assertNotSymlink(settingsPath);
2313
2334
  const { raw, parsed } = await readSettings(settingsPath);
2314
2335
  const { settings, action } = upsertStopHook(parsed, command);
@@ -2385,9 +2406,17 @@ async function doRunHookStatus(options) {
2385
2406
  console.log("basou Stop hook: not registered. Run 'basou hook install' to register it.");
2386
2407
  return;
2387
2408
  }
2388
- const mode = / --block\b/.test(command) ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
2409
+ const mode = describeHookMode({
2410
+ block: / --block\b/.test(command),
2411
+ review: / --require-review\b/.test(command)
2412
+ });
2389
2413
  console.log(`basou Stop hook: registered, ${mode}.`);
2390
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
+ }
2391
2420
 
2392
2421
  // src/commands/import.ts
2393
2422
  import { createReadStream } from "fs";
@@ -3020,7 +3049,6 @@ import {
3020
3049
  createManifest,
3021
3050
  ensureBasouDirectory,
3022
3051
  resolveRepositoryRoot as resolveRepositoryRoot7,
3023
- tryRemoteUrl,
3024
3052
  writeManifest
3025
3053
  } from "@basou/core";
3026
3054
  function collectValue(value, previous) {
@@ -3029,7 +3057,7 @@ function collectValue(value, previous) {
3029
3057
  function registerInitCommand(program) {
3030
3058
  program.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
3031
3059
  "--repo-url <url>",
3032
- "Repository URL (defaults to git remote.origin.url; pass empty string for null)"
3060
+ "Deprecated and ignored (project.repository_url was removed); accepted for 0.x CLI stability, removed at 1.0"
3033
3061
  ).option(
3034
3062
  "--source-root <path>",
3035
3063
  "Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
@@ -3054,11 +3082,10 @@ async function doRunInit(options, ctx) {
3054
3082
  const cwd = ctx.cwd ?? process.cwd();
3055
3083
  const repositoryRoot = await resolveRepositoryRootForInit(cwd);
3056
3084
  const workspaceName = options.name ?? basename4(repositoryRoot);
3057
- let repositoryUrl;
3058
3085
  if (options.repoUrl !== void 0) {
3059
- repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
3060
- } else {
3061
- repositoryUrl = await tryRemoteUrl(repositoryRoot);
3086
+ console.error(
3087
+ "Warning: --repo-url is deprecated and ignored (project.repository_url was removed); the flag will be removed at 1.0."
3088
+ );
3062
3089
  }
3063
3090
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
3064
3091
  const rel = relative(repositoryRoot, resolve5(cwd, p));
@@ -3069,7 +3096,6 @@ async function doRunInit(options, ctx) {
3069
3096
  workspaceName,
3070
3097
  ...options.projectName !== void 0 ? { projectName: options.projectName } : {},
3071
3098
  ...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
3072
- ...repositoryUrl !== void 0 ? { repositoryUrl } : {},
3073
3099
  ...sourceRoots.length > 0 ? { sourceRoots } : {}
3074
3100
  });
3075
3101
  await writeManifest(paths, manifest, { force: options.force === true });
@@ -6220,25 +6246,131 @@ function renderProjectRetrofit(result) {
6220
6246
 
6221
6247
  // src/commands/protocol.ts
6222
6248
  import { readFile as readFile4 } from "fs/promises";
6249
+ import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdownFile as readMarkdownFile6 } from "@basou/core";
6250
+
6251
+ // src/lib/context-channel.ts
6252
+ import { homedir as homedir7 } from "os";
6253
+ import { join as join10 } from "path";
6223
6254
  import {
6224
- PROTOCOL_END,
6225
- PROTOCOL_START,
6255
+ ORIENTATION_END,
6256
+ ORIENTATION_START,
6226
6257
  parseMarkers as parseMarkers2,
6227
6258
  readMarkdownFile as readMarkdownFile5,
6228
6259
  removeMarkerSection as removeMarkerSection2
6229
6260
  } from "@basou/core";
6261
+ var CODEX_TARGET_PATH = join10(homedir7(), ".codex", "AGENTS.md");
6262
+ var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
6263
+ 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. -->";
6264
+ function buildTargetBody(existing, block, markers) {
6265
+ const wrapped = `${markers.start}
6266
+ ${block}${markers.end}
6267
+ `;
6268
+ if (existing === null || existing === "") return wrapped;
6269
+ const section = parseMarkers2(existing, markers);
6270
+ switch (section.kind) {
6271
+ case "ok":
6272
+ return `${section.before}${markers.start}
6273
+ ${block}${markers.end}${section.after}`;
6274
+ case "no_markers": {
6275
+ const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
6276
+ return `${existing}${sep}${wrapped}`;
6277
+ }
6278
+ default:
6279
+ throw new Error(
6280
+ "The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
6281
+ );
6282
+ }
6283
+ }
6284
+ async function backupOnce(target, existing) {
6285
+ if (existing === null) return;
6286
+ const bak = `${target}.basou-bak`;
6287
+ const already = await readMarkdownFile5(bak);
6288
+ if (already !== null) return;
6289
+ await writeFileDurable(bak, existing);
6290
+ }
6291
+ async function syncMarkerBlock(opts) {
6292
+ const { target, markers, block } = opts;
6293
+ await assertNotSymlink(target);
6294
+ const existing = await readMarkdownFile5(target);
6295
+ const newBody = buildTargetBody(existing, block, markers);
6296
+ if (newBody === existing) return { action: "unchanged" };
6297
+ const hadBlock = existing !== null && parseMarkers2(existing, markers).kind === "ok";
6298
+ const action = hadBlock ? "updated" : "installed";
6299
+ if (opts.dryRun === true) return { action };
6300
+ const recheck = await readMarkdownFile5(target);
6301
+ if (recheck !== existing) {
6302
+ throw new Error(
6303
+ "The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
6304
+ );
6305
+ }
6306
+ await backupOnce(target, existing);
6307
+ await writeFileDurable(target, newBody);
6308
+ return { action };
6309
+ }
6310
+ function assertNoMarkerLine(body, markers) {
6311
+ for (const line of body.split(/\r?\n/)) {
6312
+ if (line === markers.start || line === markers.end) {
6313
+ throw new Error(
6314
+ "The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
6315
+ );
6316
+ }
6317
+ }
6318
+ }
6319
+ async function removeMarkerBlock(opts) {
6320
+ const { target, markers, fileLabel } = opts;
6321
+ await assertNotSymlink(target);
6322
+ const existing = await readMarkdownFile5(target);
6323
+ if (existing === null) return { removed: false };
6324
+ const newBody = removeMarkerSection2(existing, fileLabel, markers);
6325
+ if (newBody === existing) return { removed: false };
6326
+ if (opts.dryRun === true) return { removed: true };
6327
+ const recheck = await readMarkdownFile5(target);
6328
+ if (recheck !== existing) {
6329
+ throw new Error(
6330
+ "The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
6331
+ );
6332
+ }
6333
+ await backupOnce(target, existing);
6334
+ await writeFileDurable(target, newBody);
6335
+ return { removed: true };
6336
+ }
6337
+ async function syncOrientationChannel(opts) {
6338
+ assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
6339
+ const block = `${ORIENTATION_MANAGED_NOTE}
6340
+
6341
+ ${opts.body.replace(/\s+$/, "")}
6342
+ `;
6343
+ return syncMarkerBlock({
6344
+ target: opts.target ?? CODEX_TARGET_PATH,
6345
+ markers: ORIENTATION_MARKERS,
6346
+ block,
6347
+ ...opts.dryRun === true ? { dryRun: true } : {}
6348
+ });
6349
+ }
6350
+ async function renderOrientationToCodexChannel(opts) {
6351
+ const body = await readMarkdownFile5(opts.orientationPath);
6352
+ if (body === null) return null;
6353
+ const { action } = await syncOrientationChannel({
6354
+ body,
6355
+ ...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
6356
+ });
6357
+ return {
6358
+ action,
6359
+ line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
6360
+ };
6361
+ }
6230
6362
 
6231
6363
  // src/lib/protocols-config.ts
6232
- import { homedir as homedir7 } from "os";
6233
- import { isAbsolute as isAbsolute4, join as join10, resolve as resolve8 } from "path";
6364
+ import { homedir as homedir8 } from "os";
6365
+ import { isAbsolute as isAbsolute4, join as join11, resolve as resolve8 } from "path";
6234
6366
  import { readYamlFile as readYamlFile5 } from "@basou/core";
6235
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join10(homedir7(), ".basou", "protocols.yaml");
6236
- var DEFAULT_TARGET_PATH = join10(homedir7(), ".claude", "CLAUDE.md");
6367
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join11(homedir8(), ".basou", "protocols.yaml");
6368
+ var DEFAULT_TARGET_PATH = join11(homedir8(), ".claude", "CLAUDE.md");
6237
6369
  var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
6238
6370
  var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
6239
6371
  function expandTilde3(p) {
6240
- if (p === "~") return homedir7();
6241
- if (p.startsWith("~/")) return join10(homedir7(), p.slice(2));
6372
+ if (p === "~") return homedir8();
6373
+ if (p.startsWith("~/")) return join11(homedir8(), p.slice(2));
6242
6374
  return p;
6243
6375
  }
6244
6376
  function isRecord3(value) {
@@ -6359,13 +6491,7 @@ async function readProtocolSources(entries) {
6359
6491
  }
6360
6492
  throw new Error("Failed to read a protocol source file.", { cause: error });
6361
6493
  }
6362
- for (const line of content.split(/\r?\n/)) {
6363
- if (line === PROTOCOL_START || line === PROTOCOL_END) {
6364
- throw new Error(
6365
- "A protocol source contains a BASOU:PROTOCOLS marker line, which would corrupt the managed block. Remove that line from the source."
6366
- );
6367
- }
6368
- }
6494
+ assertNoMarkerLine(content, PROTOCOL_MARKERS);
6369
6495
  out.push({ entry, content });
6370
6496
  }
6371
6497
  return out;
@@ -6382,74 +6508,41 @@ ${body}` : body;
6382
6508
  ${sections.join("\n\n")}
6383
6509
  `;
6384
6510
  }
6385
- function buildTargetBody(existing, block) {
6386
- const wrapped = `${PROTOCOL_START}
6387
- ${block}${PROTOCOL_END}
6388
- `;
6389
- if (existing === null || existing === "") return wrapped;
6390
- const section = parseMarkers2(existing, PROTOCOL_MARKERS);
6391
- switch (section.kind) {
6392
- case "ok":
6393
- return `${section.before}${PROTOCOL_START}
6394
- ${block}${PROTOCOL_END}${section.after}`;
6395
- case "no_markers": {
6396
- const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
6397
- return `${existing}${sep}${wrapped}`;
6398
- }
6399
- default:
6400
- throw new Error(
6401
- "The BASOU:PROTOCOLS markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
6402
- );
6403
- }
6404
- }
6405
- async function backupOnce(target, existing) {
6406
- if (existing === null) return;
6407
- const bak = `${target}.basou-bak`;
6408
- const already = await readMarkdownFile5(bak);
6409
- if (already !== null) return;
6410
- await writeFileDurable(bak, existing);
6411
- }
6412
6511
  async function doRunProtocolSync(options) {
6413
6512
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
6414
6513
  const target = options.target ?? DEFAULT_TARGET_PATH;
6415
6514
  const entries = await loadProtocolsConfig(configPath);
6416
6515
  const sources = await readProtocolSources(entries);
6417
6516
  const block = buildBlock(sources);
6418
- await assertNotSymlink(target);
6419
- const existing = await readMarkdownFile5(target);
6420
- const newBody = buildTargetBody(existing, block);
6421
- if (newBody === existing) {
6517
+ const result = await syncMarkerBlock({
6518
+ target,
6519
+ markers: PROTOCOL_MARKERS,
6520
+ block,
6521
+ ...options.dryRun === true ? { dryRun: true } : {}
6522
+ });
6523
+ if (result.action === "unchanged") {
6422
6524
  console.log(`The basou:protocols block is already up to date (${entries.length} protocol(s)).`);
6423
6525
  return;
6424
6526
  }
6425
- const hadBlock = existing !== null && parseMarkers2(existing, PROTOCOL_MARKERS).kind === "ok";
6426
6527
  if (options.dryRun === true) {
6427
6528
  console.log(
6428
- `[dry-run] Would ${hadBlock ? "update" : "install"} the basou:protocols block (${entries.length} protocol(s)).`
6529
+ `[dry-run] Would ${result.action === "updated" ? "update" : "install"} the basou:protocols block (${entries.length} protocol(s)).`
6429
6530
  );
6430
6531
  for (const { entry } of sources) {
6431
6532
  console.log(` - ${entry.title ?? entry.source}`);
6432
6533
  }
6433
6534
  return;
6434
6535
  }
6435
- const recheck = await readMarkdownFile5(target);
6436
- if (recheck !== existing) {
6437
- throw new Error(
6438
- "The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol sync'."
6439
- );
6440
- }
6441
- await backupOnce(target, existing);
6442
- await writeFileDurable(target, newBody);
6443
6536
  console.log(
6444
- `${hadBlock ? "Updated" : "Installed"} the basou:protocols block in the global CLAUDE.md (${entries.length} protocol(s)).`
6537
+ `${result.action === "updated" ? "Updated" : "Installed"} the basou:protocols block in the global CLAUDE.md (${entries.length} protocol(s)).`
6445
6538
  );
6446
6539
  }
6447
6540
  async function doRunProtocolList(options) {
6448
6541
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
6449
6542
  const target = options.target ?? DEFAULT_TARGET_PATH;
6450
6543
  const entries = await loadProtocolsConfig(configPath);
6451
- const existing = await readMarkdownFile5(target);
6452
- const installed = existing !== null && parseMarkers2(existing, PROTOCOL_MARKERS).kind === "ok";
6544
+ const existing = await readMarkdownFile6(target);
6545
+ const installed = existing !== null && parseMarkers3(existing, PROTOCOL_MARKERS).kind === "ok";
6453
6546
  console.log(`Declared protocols (${entries.length}):`);
6454
6547
  for (const entry of entries) {
6455
6548
  console.log(` - ${entry.title ?? entry.source}`);
@@ -6458,14 +6551,13 @@ async function doRunProtocolList(options) {
6458
6551
  }
6459
6552
  async function doRunProtocolUnsync(options) {
6460
6553
  const target = options.target ?? DEFAULT_TARGET_PATH;
6461
- await assertNotSymlink(target);
6462
- const existing = await readMarkdownFile5(target);
6463
- if (existing === null) {
6464
- console.log("No target file; nothing to remove.");
6465
- return;
6466
- }
6467
- const newBody = removeMarkerSection2(existing, "CLAUDE.md", PROTOCOL_MARKERS);
6468
- if (newBody === existing) {
6554
+ const result = await removeMarkerBlock({
6555
+ target,
6556
+ markers: PROTOCOL_MARKERS,
6557
+ fileLabel: "CLAUDE.md",
6558
+ ...options.dryRun === true ? { dryRun: true } : {}
6559
+ });
6560
+ if (!result.removed) {
6469
6561
  console.log("No basou:protocols block found; nothing removed.");
6470
6562
  return;
6471
6563
  }
@@ -6473,14 +6565,6 @@ async function doRunProtocolUnsync(options) {
6473
6565
  console.log("[dry-run] Would remove the basou:protocols block from the global CLAUDE.md.");
6474
6566
  return;
6475
6567
  }
6476
- const recheck = await readMarkdownFile5(target);
6477
- if (recheck !== existing) {
6478
- throw new Error(
6479
- "The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run 'basou protocol unsync'."
6480
- );
6481
- }
6482
- await backupOnce(target, existing);
6483
- await writeFileDurable(target, newBody);
6484
6568
  console.log("Removed the basou:protocols block from the global CLAUDE.md.");
6485
6569
  }
6486
6570
 
@@ -6490,16 +6574,16 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
6490
6574
 
6491
6575
  // src/commands/refresh-watch.ts
6492
6576
  import { readdir as readdir2, stat as stat5 } from "fs/promises";
6493
- import { homedir as homedir8 } from "os";
6494
- import { join as join11 } from "path";
6577
+ import { homedir as homedir9 } from "os";
6578
+ import { join as join12 } from "path";
6495
6579
  import { findErrorCode as findErrorCode8 } from "@basou/core";
6496
6580
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
6497
6581
  var MIN_WATCH_INTERVAL_SEC = 5;
6498
6582
  var MAX_WATCH_INTERVAL_SEC = 86400;
6499
6583
  function watchedRoots(ctx) {
6500
6584
  return [
6501
- ctx.codexSessionsDir ?? join11(homedir8(), ".codex", "sessions"),
6502
- ctx.claudeProjectsDir ?? join11(homedir8(), ".claude", "projects")
6585
+ ctx.codexSessionsDir ?? join12(homedir9(), ".codex", "sessions"),
6586
+ ctx.claudeProjectsDir ?? join12(homedir9(), ".claude", "projects")
6503
6587
  ];
6504
6588
  }
6505
6589
  async function scanSourceLogs(roots) {
@@ -6513,7 +6597,7 @@ async function scanSourceLogs(roots) {
6513
6597
  throw new Error("Failed to read a source log directory", { cause: error });
6514
6598
  }
6515
6599
  for (const entry of entries) {
6516
- const full = join11(dir, entry.name);
6600
+ const full = join12(dir, entry.name);
6517
6601
  if (entry.isDirectory()) {
6518
6602
  await walk(full);
6519
6603
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -6620,19 +6704,19 @@ function parseInterval(value) {
6620
6704
  return seconds;
6621
6705
  }
6622
6706
  function abortableSleep(ms, signal) {
6623
- return new Promise((resolve12) => {
6707
+ return new Promise((resolve14) => {
6624
6708
  if (signal.aborted) {
6625
- resolve12();
6709
+ resolve14();
6626
6710
  return;
6627
6711
  }
6628
6712
  let timer;
6629
6713
  const onAbort = () => {
6630
6714
  clearTimeout(timer);
6631
- resolve12();
6715
+ resolve14();
6632
6716
  };
6633
6717
  timer = setTimeout(() => {
6634
6718
  signal.removeEventListener("abort", onAbort);
6635
- resolve12();
6719
+ resolve14();
6636
6720
  }, ms);
6637
6721
  signal.addEventListener("abort", onAbort, { once: true });
6638
6722
  });
@@ -6685,7 +6769,7 @@ async function doRunRefreshPortfolio(options, ctx) {
6685
6769
  for (const ws of workspaces) {
6686
6770
  const label = ws.label ?? ws.path;
6687
6771
  try {
6688
- const result = await computeRefresh(
6772
+ const { result } = await computeRefresh(
6689
6773
  { ...options, portfolio: false },
6690
6774
  { ...ctx, cwd: ws.path }
6691
6775
  );
@@ -6754,7 +6838,7 @@ async function computeRefresh(options, ctx) {
6754
6838
  const paths = basouPaths11(repositoryRoot);
6755
6839
  await assertWorkspaceInitialized8(paths.root);
6756
6840
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
6757
- return refreshAll({
6841
+ const result = await refreshAll({
6758
6842
  options: {
6759
6843
  ...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
6760
6844
  ...options.force === true ? { force: true } : {},
@@ -6766,16 +6850,30 @@ async function computeRefresh(options, ctx) {
6766
6850
  paths,
6767
6851
  nowIso
6768
6852
  });
6853
+ return { result, paths };
6769
6854
  }
6770
6855
  async function doRunRefresh(options, ctx) {
6771
- const result = await computeRefresh(options, ctx);
6856
+ const { result, paths } = await computeRefresh(options, ctx);
6857
+ const channelLine = options.dryRun === true ? null : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
6772
6858
  if (options.json === true) {
6773
6859
  console.log(JSON.stringify(result));
6774
6860
  } else {
6775
6861
  printRefreshSummary(result);
6862
+ if (channelLine !== null) console.log(channelLine);
6776
6863
  }
6777
6864
  return result;
6778
6865
  }
6866
+ async function syncCodexOrientationChannel(paths, channelPath) {
6867
+ try {
6868
+ const rendered = await renderOrientationToCodexChannel({
6869
+ orientationPath: paths.files.orientation,
6870
+ ...channelPath !== void 0 ? { channelPath } : {}
6871
+ });
6872
+ return rendered === null ? null : rendered.line;
6873
+ } catch (error) {
6874
+ return `codex channel skipped: ${error instanceof Error ? error.message : String(error)}`;
6875
+ }
6876
+ }
6779
6877
  function describeImport(outcome) {
6780
6878
  if (outcome.status === "skipped") {
6781
6879
  return `${outcome.adapter}: skipped (${outcome.reason})`;
@@ -6908,9 +7006,195 @@ async function assertWorkspaceInitialized9(basouRoot) {
6908
7006
  }
6909
7007
  }
6910
7008
 
6911
- // src/commands/review-gaps.ts
7009
+ // src/commands/review.ts
7010
+ import { readFile as readFile5 } from "fs/promises";
7011
+ import { homedir as homedir10 } from "os";
7012
+ import { resolve as resolve10 } from "path";
6912
7013
  import {
7014
+ assertBasouRootSafe as assertBasouRootSafe11,
6913
7015
  basouPaths as basouPaths13,
7016
+ buildReviewRecordedEvent,
7017
+ buildReviewRecordLabel,
7018
+ createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
7019
+ findErrorCode as findErrorCode11,
7020
+ parseReviewRecordInput,
7021
+ readManifest as readManifest7,
7022
+ sanitizePath as sanitizePath2
7023
+ } from "@basou/core";
7024
+ function registerReviewCommand(program) {
7025
+ const review = program.command("review").description("Record reviews that ran (the durable signal a review happened)");
7026
+ review.command("record").description(
7027
+ "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."
7028
+ ).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) => {
7029
+ await runReviewRecord(options);
7030
+ });
7031
+ }
7032
+ var REVIEW_RECORD_HELP = `
7033
+ Input format (a single JSON object describing one review):
7034
+ {
7035
+ "reviewer": "codex",
7036
+ "target": "working-tree",
7037
+ "verdict": "needs-attention",
7038
+ "findings": [
7039
+ { "title": "Off-by-one in pager", "severity": "medium", "location": "src/page.ts:42", "summary": "..." }
7040
+ ],
7041
+ "blocked": [
7042
+ { "title": "Reviewer wanted to drop the singleton", "reason": "design-reversal", "why": "Settled in decision_X" }
7043
+ ]
7044
+ }
7045
+
7046
+ Only "reviewer" and "target" are required; verdict / findings / blocked are
7047
+ optional. Record blocked findings (spec-deviation / design-reversal) here so the
7048
+ adversarial-review protocol's "always report what you blocked" becomes a durable
7049
+ trail artifact -- an explicit empty "blocked": [] is encouraged to record that
7050
+ you blocked nothing. The review is written into one ad-hoc session timestamped
7051
+ now. Run from a workspace-view directory and it resolves to the planning repo,
7052
+ like 'basou decision capture' / 'basou note'.
7053
+
7054
+ Example (heredoc on stdin):
7055
+ basou review record <<'JSON'
7056
+ { "reviewer": "codex", "target": "working-tree", "verdict": "pass", "blocked": [] }
7057
+ JSON
7058
+ `;
7059
+ async function runReviewRecord(options, ctx = {}) {
7060
+ try {
7061
+ await doRunReviewRecord(options, ctx);
7062
+ } catch (error) {
7063
+ renderCliError(error, {
7064
+ verbose: isVerbose(options),
7065
+ classifiers: [failedToFinalizeClassifier]
7066
+ });
7067
+ process.exitCode = 1;
7068
+ }
7069
+ }
7070
+ async function doRunReviewRecord(options, ctx) {
7071
+ const cwd = ctx.cwd ?? process.cwd();
7072
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "review record");
7073
+ const paths = basouPaths13(repositoryRoot);
7074
+ await assertWorkspaceInitialized10(paths.root);
7075
+ const raw = await readReviewInput(options, ctx);
7076
+ const review = parseReviewRecordInput(raw);
7077
+ if (options.dryRun === true) {
7078
+ printReviewPreview(options, review);
7079
+ return;
7080
+ }
7081
+ const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
7082
+ const occurredAt = now.toISOString();
7083
+ const manifest = await readManifest7(paths);
7084
+ const invocationArgs = options.file !== void 0 ? [
7085
+ "--file",
7086
+ sanitizePath2(resolve10(cwd, options.file), {
7087
+ workingDirectory: repositoryRoot,
7088
+ homedir: homedir10()
7089
+ })
7090
+ ] : [];
7091
+ const adHoc = await createAdHocSessionWithEvent3({
7092
+ paths,
7093
+ manifest,
7094
+ label: buildReviewRecordLabel(review),
7095
+ occurredAt,
7096
+ sessionSource: "human",
7097
+ workingDirectory: repositoryRoot,
7098
+ invocation: { command: "basou review record", args: invocationArgs },
7099
+ targetEventBuilders: [
7100
+ (sessionId, eventId) => buildReviewRecordedEvent({ eventId, sessionId, occurredAt, review })
7101
+ ]
7102
+ });
7103
+ printReviewResult(options, {
7104
+ sessionId: adHoc.sessionId,
7105
+ eventId: adHoc.targetEventIds[0],
7106
+ review
7107
+ });
7108
+ }
7109
+ async function readReviewInput(options, ctx) {
7110
+ if (options.file !== void 0) {
7111
+ try {
7112
+ return await readFile5(options.file, "utf8");
7113
+ } catch (error) {
7114
+ if (findErrorCode11(error, "ENOENT")) {
7115
+ throw new Error(`Input file not found: ${options.file}`);
7116
+ }
7117
+ throw error;
7118
+ }
7119
+ }
7120
+ if (ctx.readInput !== void 0) {
7121
+ return await ctx.readInput();
7122
+ }
7123
+ if (process.stdin.isTTY === true) {
7124
+ throw new Error(NO_INPUT_HINT2);
7125
+ }
7126
+ return await readStdinToEnd2();
7127
+ }
7128
+ async function readStdinToEnd2() {
7129
+ const chunks = [];
7130
+ for await (const chunk of process.stdin) {
7131
+ chunks.push(Buffer.from(chunk));
7132
+ }
7133
+ return Buffer.concat(chunks).toString("utf8");
7134
+ }
7135
+ var NO_INPUT_HINT2 = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
7136
+ function reviewToPayload(review) {
7137
+ const payload = {
7138
+ reviewer: review.reviewer,
7139
+ target: review.target
7140
+ };
7141
+ if (review.verdict !== void 0) payload.verdict = review.verdict;
7142
+ if (review.findings !== void 0) payload.findings = review.findings;
7143
+ if (review.blocked !== void 0) payload.blocked = review.blocked;
7144
+ return payload;
7145
+ }
7146
+ function reviewSummaryLine(review) {
7147
+ const parts = [];
7148
+ if (review.verdict !== void 0) parts.push(`verdict: ${review.verdict}`);
7149
+ if (review.findings !== void 0) {
7150
+ parts.push(`${review.findings.length} finding${review.findings.length === 1 ? "" : "s"}`);
7151
+ }
7152
+ if (review.blocked !== void 0) {
7153
+ parts.push(`${review.blocked.length} blocked`);
7154
+ }
7155
+ return parts.length > 0 ? ` (${parts.join(", ")})` : "";
7156
+ }
7157
+ function printReviewPreview(options, review) {
7158
+ if (options.json === true) {
7159
+ console.log(JSON.stringify({ dry_run: true, review: reviewToPayload(review) }));
7160
+ return;
7161
+ }
7162
+ console.log(
7163
+ `Would record review by ${review.reviewer} of ${review.target}${reviewSummaryLine(review)} (dry run; nothing written).`
7164
+ );
7165
+ }
7166
+ function printReviewResult(options, result) {
7167
+ const sid = shortSessionId(result.sessionId);
7168
+ if (options.json === true) {
7169
+ console.log(
7170
+ JSON.stringify({
7171
+ mode: "ad-hoc",
7172
+ session_id: result.sessionId,
7173
+ session_status: "completed",
7174
+ event_id: result.eventId,
7175
+ review: reviewToPayload(result.review)
7176
+ })
7177
+ );
7178
+ return;
7179
+ }
7180
+ console.log(
7181
+ `Recorded review by ${result.review.reviewer} of ${result.review.target}${reviewSummaryLine(result.review)} in ad-hoc session ${sid}.`
7182
+ );
7183
+ }
7184
+ async function assertWorkspaceInitialized10(basouRoot) {
7185
+ try {
7186
+ await assertBasouRootSafe11(basouRoot);
7187
+ } catch (error) {
7188
+ if (findErrorCode11(error, "ENOENT")) {
7189
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
7190
+ }
7191
+ throw error;
7192
+ }
7193
+ }
7194
+
7195
+ // src/commands/review-gaps.ts
7196
+ import {
7197
+ basouPaths as basouPaths14,
6914
7198
  findReviewGaps
6915
7199
  } from "@basou/core";
6916
7200
  import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
@@ -6951,7 +7235,7 @@ async function runReviewGaps(options, ctx = {}) {
6951
7235
  async function doRunReviewGaps(options, ctx) {
6952
7236
  const cwd = ctx.cwd ?? process.cwd();
6953
7237
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
6954
- const paths = basouPaths13(repositoryRoot);
7238
+ const paths = basouPaths14(repositoryRoot);
6955
7239
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
6956
7240
  const summary = await findReviewGaps({
6957
7241
  paths,
@@ -7034,23 +7318,25 @@ function renderReviewGaps(summary) {
7034
7318
 
7035
7319
  // src/commands/run.ts
7036
7320
  import { mkdir as mkdir2 } from "fs/promises";
7037
- import { homedir as homedir9 } from "os";
7038
- import { join as join12 } from "path";
7321
+ import { homedir as homedir11 } from "os";
7322
+ import { join as join13 } from "path";
7039
7323
  import {
7040
7324
  acquireLock as acquireLock5,
7041
- assertBasouRootSafe as assertBasouRootSafe11,
7042
- basouPaths as basouPaths14,
7325
+ assertBasouRootSafe as assertBasouRootSafe12,
7326
+ basouPaths as basouPaths15,
7043
7327
  ChildProcessRunner as ChildProcessRunner2,
7044
7328
  claudeCodeAdapterMetadata,
7329
+ codexAdapterMetadata,
7045
7330
  appendChainedEvent as coreAppendChainedEvent2,
7046
7331
  finalizeSessionYaml as finalizeSessionYaml2,
7047
7332
  getDiff,
7048
7333
  getSnapshot as getSnapshot2,
7049
7334
  overwriteYamlFile as overwriteYamlFile2,
7050
7335
  prefixedUlid as prefixedUlid4,
7051
- readManifest as readManifest7,
7336
+ readManifest as readManifest8,
7052
7337
  readYamlFile as readYamlFile6,
7053
7338
  resolveClaudeCodeCommand,
7339
+ resolveCodexCommand,
7054
7340
  resolveRepositoryRoot as resolveRepositoryRoot10,
7055
7341
  SessionSchema as SessionSchema2,
7056
7342
  sanitizeRelatedFiles,
@@ -7058,50 +7344,72 @@ import {
7058
7344
  writeYamlFile as writeYamlFile2
7059
7345
  } from "@basou/core";
7060
7346
  function registerRunCommand(program, ctx = {}) {
7061
- 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");
7062
- 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) => {
7347
+ const runCommand = addRunOptions(
7348
+ program.command("run").description("Run an AI coding tool through Basou as a tracked session").enablePositionalOptions()
7349
+ );
7350
+ const dispatch = async (args, options, command, run) => {
7063
7351
  const parentOptions = command.parent?.opts() ?? {};
7064
7352
  const snapshotOn = parentOptions.snapshot !== false && options.snapshot !== false;
7065
- const merged = {
7066
- ...parentOptions,
7067
- ...options,
7068
- snapshot: snapshotOn
7069
- };
7353
+ const merged = { ...parentOptions, ...options, snapshot: snapshotOn };
7070
7354
  try {
7071
- const exitCode = await runClaudeCode(args, merged, ctx);
7355
+ const exitCode = await run(args, merged, ctx);
7072
7356
  process.exit(exitCode);
7073
7357
  } catch (error) {
7074
7358
  renderCliError(error, { verbose: isVerbose(merged) });
7075
7359
  process.exit(1);
7076
7360
  }
7361
+ };
7362
+ addRunOptions(runCommand.command("claude-code [args...]")).description("Run Claude Code CLI as a Basou-tracked session").passThroughOptions().action(
7363
+ (args, options, command) => dispatch(args, options, command, runClaudeCode)
7364
+ );
7365
+ addRunOptions(runCommand.command("codex [args...]")).description("Run the Codex CLI as a Basou-tracked session").passThroughOptions().action(
7366
+ (args, options, command) => dispatch(args, options, command, runCodex)
7367
+ );
7368
+ }
7369
+ function addRunOptions(command) {
7370
+ 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");
7371
+ }
7372
+ function runClaudeCode(args, options, ctx = {}) {
7373
+ return runTrackedTool(args, options, ctx, {
7374
+ resolveCommand: ctx.resolveCommand ?? resolveClaudeCodeCommand,
7375
+ metadata: claudeCodeAdapterMetadata
7077
7376
  });
7078
7377
  }
7079
- async function runClaudeCode(args, options, ctx = {}) {
7378
+ function runCodex(args, options, ctx = {}) {
7379
+ return runTrackedTool(args, options, ctx, {
7380
+ resolveCommand: ctx.resolveCodexCommand ?? resolveCodexCommand,
7381
+ metadata: codexAdapterMetadata,
7382
+ transformArgs: (a) => ["-c", "shell_environment_policy.inherit=all", ...a],
7383
+ preSpawn: syncCodexOrientationChannelPreSpawn
7384
+ });
7385
+ }
7386
+ async function runTrackedTool(args, options, ctx, adapter) {
7080
7387
  const runner = ctx.runner ?? new ChildProcessRunner2();
7081
7388
  const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
7082
- const resolveCommand = ctx.resolveCommand ?? resolveClaudeCodeCommand;
7083
7389
  const getDiffFn = ctx.getDiff ?? getDiff;
7084
- const { command } = await resolveCommand();
7390
+ const { command } = await adapter.resolveCommand();
7391
+ const childArgs = adapter.transformArgs ? adapter.transformArgs(args) : args;
7085
7392
  const cwd = options.cwd ?? process.cwd();
7086
7393
  const repoRoot = await resolveRepositoryRootForRun(cwd);
7087
- const paths = basouPaths14(repoRoot);
7088
- await assertBasouRootSafe11(paths.root);
7089
- const manifest = await readManifest7(paths);
7394
+ const paths = basouPaths15(repoRoot);
7395
+ await assertBasouRootSafe12(paths.root);
7396
+ const manifest = await readManifest8(paths);
7090
7397
  const sessionId = prefixedUlid4("ses");
7091
- const sessionDir = join12(paths.sessions, sessionId);
7398
+ const sessionDir = join13(paths.sessions, sessionId);
7092
7399
  await mkdir2(sessionDir, { recursive: true });
7093
7400
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
7094
7401
  await coreAppendChainedEvent2(paths, sessionId, event);
7095
7402
  });
7096
7403
  const startedAt = now().toISOString();
7097
- const sessionYamlPath = join12(sessionDir, "session.yaml");
7404
+ const sessionYamlPath = join13(sessionDir, "session.yaml");
7098
7405
  const session = buildInitialSession2({
7099
7406
  id: sessionId,
7100
7407
  command,
7101
- args,
7408
+ args: childArgs,
7102
7409
  cwd: repoRoot,
7103
7410
  workspaceId: manifest.workspace.id,
7104
- startedAt
7411
+ startedAt,
7412
+ source: adapter.metadata
7105
7413
  });
7106
7414
  await writeYamlFile2(sessionYamlPath, session);
7107
7415
  await appendEvent(sessionDir, {
@@ -7110,7 +7418,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7110
7418
  id: prefixedUlid4("evt"),
7111
7419
  session_id: sessionId,
7112
7420
  occurred_at: startedAt,
7113
- source: claudeCodeAdapterMetadata.kind
7421
+ source: adapter.metadata.kind
7114
7422
  });
7115
7423
  let preSnapshot = null;
7116
7424
  if (options.snapshot !== false) {
@@ -7123,7 +7431,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7123
7431
  id: prefixedUlid4("evt"),
7124
7432
  session_id: sessionId,
7125
7433
  occurred_at: runningAt,
7126
- source: claudeCodeAdapterMetadata.kind,
7434
+ source: adapter.metadata.kind,
7127
7435
  from: "initialized",
7128
7436
  to: "running"
7129
7437
  });
@@ -7157,10 +7465,14 @@ async function runClaudeCode(args, options, ctx = {}) {
7157
7465
  process.on("SIGTERM", onSigTerm);
7158
7466
  process.on("exit", exitHandler);
7159
7467
  ctx.onExitHookInstalled?.(exitHandler);
7468
+ if (adapter.preSpawn !== void 0) {
7469
+ const line = await adapter.preSpawn(cwd, ctx);
7470
+ if (line !== null) console.log(line);
7471
+ }
7160
7472
  let result;
7161
7473
  try {
7162
7474
  try {
7163
- result = await runner.run(command, args, {
7475
+ result = await runner.run(command, childArgs, {
7164
7476
  cwd: repoRoot,
7165
7477
  capture: "none",
7166
7478
  signal: controller.signal,
@@ -7171,10 +7483,11 @@ async function runClaudeCode(args, options, ctx = {}) {
7171
7483
  } catch (spawnError) {
7172
7484
  await finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEvent, {
7173
7485
  command,
7174
- args,
7486
+ args: childArgs,
7175
7487
  cwd: repoRoot,
7176
7488
  occurredAt: now().toISOString(),
7177
- signalReceived
7489
+ signalReceived,
7490
+ sourceKind: adapter.metadata.kind
7178
7491
  });
7179
7492
  throw spawnError;
7180
7493
  }
@@ -7193,7 +7506,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7193
7506
  occurred_at: endedAt,
7194
7507
  source: "terminal-recording",
7195
7508
  command,
7196
- args,
7509
+ args: childArgs,
7197
7510
  cwd: repoRoot,
7198
7511
  exit_code: result.exit_code,
7199
7512
  ...result.signal !== null ? { signal: result.signal } : {},
@@ -7220,7 +7533,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7220
7533
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
7221
7534
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
7222
7535
  workingDirectory: repoRoot,
7223
- homedir: homedir9()
7536
+ homedir: homedir11()
7224
7537
  }).sanitized;
7225
7538
  const finalStatus = decideFinalStatus2(result, signalReceived);
7226
7539
  await appendEvent(sessionDir, {
@@ -7229,7 +7542,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7229
7542
  id: prefixedUlid4("evt"),
7230
7543
  session_id: sessionId,
7231
7544
  occurred_at: endedAt,
7232
- source: claudeCodeAdapterMetadata.kind,
7545
+ source: adapter.metadata.kind,
7233
7546
  from: "running",
7234
7547
  to: finalStatus
7235
7548
  });
@@ -7239,7 +7552,7 @@ async function runClaudeCode(args, options, ctx = {}) {
7239
7552
  id: prefixedUlid4("evt"),
7240
7553
  session_id: sessionId,
7241
7554
  occurred_at: endedAt,
7242
- source: claudeCodeAdapterMetadata.kind,
7555
+ source: adapter.metadata.kind,
7243
7556
  ...result.exit_code !== null ? { exit_code: result.exit_code } : {}
7244
7557
  });
7245
7558
  await finalizeSessionYaml2(paths, sessionId, (s) => {
@@ -7361,10 +7674,10 @@ function buildInitialSession2(input) {
7361
7674
  label: `basou run ${cmdline} (${input.startedAt})`,
7362
7675
  task_id: null,
7363
7676
  workspace_id: input.workspaceId,
7364
- source: { ...claudeCodeAdapterMetadata },
7677
+ source: { ...input.source },
7365
7678
  started_at: input.startedAt,
7366
7679
  status: "initialized",
7367
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir9() }),
7680
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir11() }),
7368
7681
  invocation: {
7369
7682
  command: input.command,
7370
7683
  args: [...input.args],
@@ -7404,7 +7717,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
7404
7717
  id: prefixedUlid4("evt"),
7405
7718
  session_id: sessionId,
7406
7719
  occurred_at: ctx.occurredAt,
7407
- source: claudeCodeAdapterMetadata.kind,
7720
+ source: ctx.sourceKind,
7408
7721
  from: "running",
7409
7722
  to: "failed"
7410
7723
  });
@@ -7414,7 +7727,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
7414
7727
  id: prefixedUlid4("evt"),
7415
7728
  session_id: sessionId,
7416
7729
  occurred_at: ctx.occurredAt,
7417
- source: claudeCodeAdapterMetadata.kind
7730
+ source: ctx.sourceKind
7418
7731
  });
7419
7732
  await finalizeSessionYaml2(paths, sessionId, (s) => {
7420
7733
  s.session.status = "failed";
@@ -7434,21 +7747,34 @@ async function resolveRepositoryRootForRun(cwd) {
7434
7747
  throw error;
7435
7748
  }
7436
7749
  }
7750
+ async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
7751
+ try {
7752
+ const root = await resolveBasouRootForCommand(cwd, "run");
7753
+ const paths = basouPaths15(root);
7754
+ const rendered = await renderOrientationToCodexChannel({
7755
+ orientationPath: paths.files.orientation,
7756
+ ...ctx.codexChannelPath !== void 0 ? { channelPath: ctx.codexChannelPath } : {}
7757
+ });
7758
+ return rendered === null ? null : rendered.line;
7759
+ } catch {
7760
+ return null;
7761
+ }
7762
+ }
7437
7763
 
7438
7764
  // src/commands/session.ts
7439
- import { readFile as readFile5 } from "fs/promises";
7440
- import { basename as basename6, isAbsolute as isAbsolute6, join as join13, relative as relative3 } from "path";
7765
+ import { readFile as readFile6 } from "fs/promises";
7766
+ import { basename as basename6, isAbsolute as isAbsolute6, join as join14, relative as relative3 } from "path";
7441
7767
  import {
7442
7768
  acquireLock as acquireLock6,
7443
7769
  appendEventToExistingSession as appendEventToExistingSession3,
7444
- assertBasouRootSafe as assertBasouRootSafe12,
7445
- basouPaths as basouPaths15,
7770
+ assertBasouRootSafe as assertBasouRootSafe13,
7771
+ basouPaths as basouPaths16,
7446
7772
  enumerateSessionDirs as enumerateSessionDirs2,
7447
- findErrorCode as findErrorCode11,
7773
+ findErrorCode as findErrorCode12,
7448
7774
  importSessionFromJson as importSessionFromJson2,
7449
7775
  loadSessionEntries as loadSessionEntries2,
7450
7776
  readAllEvents,
7451
- readManifest as readManifest8,
7777
+ readManifest as readManifest9,
7452
7778
  readYamlFile as readYamlFile7,
7453
7779
  rechainSessionInPlace,
7454
7780
  resolveSessionId as resolveSessionId3,
@@ -7507,8 +7833,8 @@ async function runSessionList(options, ctx = {}) {
7507
7833
  async function doRunSessionList(options, ctx) {
7508
7834
  const cwd = ctx.cwd ?? process.cwd();
7509
7835
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
7510
- const paths = basouPaths15(repositoryRoot);
7511
- await assertWorkspaceInitialized10(paths.root);
7836
+ const paths = basouPaths16(repositoryRoot);
7837
+ await assertWorkspaceInitialized11(paths.root);
7512
7838
  const now = /* @__PURE__ */ new Date();
7513
7839
  const records = (await loadSessionEntries2(paths, {
7514
7840
  now,
@@ -7559,17 +7885,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
7559
7885
  async function doRunSessionShow(idInput, options, ctx) {
7560
7886
  const cwd = ctx.cwd ?? process.cwd();
7561
7887
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
7562
- const paths = basouPaths15(repositoryRoot);
7563
- await assertWorkspaceInitialized10(paths.root);
7888
+ const paths = basouPaths16(repositoryRoot);
7889
+ await assertWorkspaceInitialized11(paths.root);
7564
7890
  const sessionId = await resolveSessionId3(paths, idInput);
7565
- const sessionDir = join13(paths.sessions, sessionId);
7566
- const sessionYamlPath = join13(sessionDir, "session.yaml");
7891
+ const sessionDir = join14(paths.sessions, sessionId);
7892
+ const sessionYamlPath = join14(sessionDir, "session.yaml");
7567
7893
  let session;
7568
7894
  try {
7569
7895
  const raw = await readYamlFile7(sessionYamlPath);
7570
7896
  session = SessionSchema3.parse(raw);
7571
7897
  } catch (error) {
7572
- if (findErrorCode11(error, "ENOENT")) {
7898
+ if (findErrorCode12(error, "ENOENT")) {
7573
7899
  throw new Error(`Session not found: ${idInput}`);
7574
7900
  }
7575
7901
  throw new Error("Failed to read session", { cause: error });
@@ -7763,6 +8089,11 @@ function eventVariantSummary(ev) {
7763
8089
  return `task ${shortTaskId2(ev.task_id)}: ${ev.title} (archived)`;
7764
8090
  case "note_added":
7765
8091
  return ev.body.length > 80 ? `${ev.body.slice(0, 77)}...` : ev.body;
8092
+ case "review_recorded": {
8093
+ const verdict = ev.verdict !== void 0 ? ` (${ev.verdict})` : "";
8094
+ const blocked = ev.blocked !== void 0 && ev.blocked.length > 0 ? ` ${ev.blocked.length} blocked` : "";
8095
+ return `${ev.reviewer} -> ${ev.target}${verdict}${blocked}`;
8096
+ }
7766
8097
  case "adapter_output":
7767
8098
  return `${ev.stream} "${ev.summary}" raw_ref=${ev.raw_ref}`;
7768
8099
  }
@@ -7810,11 +8141,11 @@ function maxLen2(values, floor) {
7810
8141
  async function resolveRepositoryRootForSession(cwd, subcmd) {
7811
8142
  return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
7812
8143
  }
7813
- async function assertWorkspaceInitialized10(basouRoot) {
8144
+ async function assertWorkspaceInitialized11(basouRoot) {
7814
8145
  try {
7815
- await assertBasouRootSafe12(basouRoot);
8146
+ await assertBasouRootSafe13(basouRoot);
7816
8147
  } catch (error) {
7817
- if (findErrorCode11(error, "ENOENT")) {
8148
+ if (findErrorCode12(error, "ENOENT")) {
7818
8149
  throw new Error("Workspace not initialized. Run 'basou init' first.");
7819
8150
  }
7820
8151
  throw error;
@@ -7852,9 +8183,9 @@ async function runSessionImport(options, ctx = {}) {
7852
8183
  async function doRunSessionImport(options, ctx) {
7853
8184
  const cwd = ctx.cwd ?? process.cwd();
7854
8185
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
7855
- const paths = basouPaths15(repositoryRoot);
7856
- await assertWorkspaceInitialized10(paths.root);
7857
- const manifest = await readManifest8(paths);
8186
+ const paths = basouPaths16(repositoryRoot);
8187
+ await assertWorkspaceInitialized11(paths.root);
8188
+ const manifest = await readManifest9(paths);
7858
8189
  const rawBody = await readInputFile(options.from);
7859
8190
  const json = parseJsonStrict(rawBody);
7860
8191
  const parsed = SessionImportPayloadSchema2.safeParse(json);
@@ -7881,12 +8212,12 @@ async function doRunSessionImport(options, ctx) {
7881
8212
  }
7882
8213
  async function readInputFile(path) {
7883
8214
  try {
7884
- return await readFile5(path, "utf8");
8215
+ return await readFile6(path, "utf8");
7885
8216
  } catch (error) {
7886
- if (findErrorCode11(error, "ENOENT")) {
8217
+ if (findErrorCode12(error, "ENOENT")) {
7887
8218
  throw new Error("Import source not found", { cause: error });
7888
8219
  }
7889
- if (findErrorCode11(error, "EISDIR")) {
8220
+ if (findErrorCode12(error, "EISDIR")) {
7890
8221
  throw new Error("Import source is not a file", { cause: error });
7891
8222
  }
7892
8223
  throw new Error("Failed to read import source", { cause: error });
@@ -7966,8 +8297,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
7966
8297
  }
7967
8298
  const cwd = ctx.cwd ?? process.cwd();
7968
8299
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
7969
- const paths = basouPaths15(repositoryRoot);
7970
- await assertWorkspaceInitialized10(paths.root);
8300
+ const paths = basouPaths16(repositoryRoot);
8301
+ await assertWorkspaceInitialized11(paths.root);
7971
8302
  const sessionId = await resolveSessionId3(paths, sessionIdInput);
7972
8303
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
7973
8304
  if (body.length === 0) {
@@ -7998,12 +8329,12 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
7998
8329
  }
7999
8330
  async function readNoteFile(path) {
8000
8331
  try {
8001
- return await readFile5(path, "utf8");
8332
+ return await readFile6(path, "utf8");
8002
8333
  } catch (error) {
8003
- if (findErrorCode11(error, "ENOENT")) {
8334
+ if (findErrorCode12(error, "ENOENT")) {
8004
8335
  throw new Error("Note source not found", { cause: error });
8005
8336
  }
8006
- if (findErrorCode11(error, "EISDIR")) {
8337
+ if (findErrorCode12(error, "EISDIR")) {
8007
8338
  throw new Error("Note source is not a file", { cause: error });
8008
8339
  }
8009
8340
  throw new Error("Failed to read note source", { cause: error });
@@ -8048,8 +8379,8 @@ async function doRunSessionRechain(options, ctx) {
8048
8379
  }
8049
8380
  const cwd = ctx.cwd ?? process.cwd();
8050
8381
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
8051
- const paths = basouPaths15(repositoryRoot);
8052
- await assertWorkspaceInitialized10(paths.root);
8382
+ const paths = basouPaths16(repositoryRoot);
8383
+ await assertWorkspaceInitialized11(paths.root);
8053
8384
  const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
8054
8385
  const dryRun = options.dryRun === true;
8055
8386
  const rows = [];
@@ -8102,10 +8433,10 @@ function renderRechainRow(row, dryRun) {
8102
8433
 
8103
8434
  // src/commands/stats.ts
8104
8435
  import {
8105
- assertBasouRootSafe as assertBasouRootSafe13,
8106
- basouPaths as basouPaths16,
8436
+ assertBasouRootSafe as assertBasouRootSafe14,
8437
+ basouPaths as basouPaths17,
8107
8438
  computeWorkStats,
8108
- findErrorCode as findErrorCode12,
8439
+ findErrorCode as findErrorCode13,
8109
8440
  resolveRepositoryRoot as resolveRepositoryRoot11
8110
8441
  } from "@basou/core";
8111
8442
  function registerStatsCommand(program) {
@@ -8124,8 +8455,8 @@ async function runStats(options, ctx = {}) {
8124
8455
  async function doRunStats(options, ctx) {
8125
8456
  const cwd = ctx.cwd ?? process.cwd();
8126
8457
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
8127
- const paths = basouPaths16(repositoryRoot);
8128
- await assertWorkspaceInitialized11(paths.root);
8458
+ const paths = basouPaths17(repositoryRoot);
8459
+ await assertWorkspaceInitialized12(paths.root);
8129
8460
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
8130
8461
  const result = await computeWorkStats({
8131
8462
  paths,
@@ -8219,11 +8550,11 @@ async function resolveRepositoryRootForStats(cwd) {
8219
8550
  throw error;
8220
8551
  }
8221
8552
  }
8222
- async function assertWorkspaceInitialized11(basouRoot) {
8553
+ async function assertWorkspaceInitialized12(basouRoot) {
8223
8554
  try {
8224
- await assertBasouRootSafe13(basouRoot);
8555
+ await assertBasouRootSafe14(basouRoot);
8225
8556
  } catch (error) {
8226
- if (findErrorCode12(error, "ENOENT")) {
8557
+ if (findErrorCode13(error, "ENOENT")) {
8227
8558
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8228
8559
  }
8229
8560
  throw error;
@@ -8232,11 +8563,11 @@ async function assertWorkspaceInitialized11(basouRoot) {
8232
8563
 
8233
8564
  // src/commands/status.ts
8234
8565
  import {
8235
- assertBasouRootSafe as assertBasouRootSafe14,
8236
- basouPaths as basouPaths17,
8566
+ assertBasouRootSafe as assertBasouRootSafe15,
8567
+ basouPaths as basouPaths18,
8237
8568
  buildStatusSnapshot,
8238
- findErrorCode as findErrorCode13,
8239
- readManifest as readManifest9,
8569
+ findErrorCode as findErrorCode14,
8570
+ readManifest as readManifest10,
8240
8571
  resolveRepositoryRoot as resolveRepositoryRoot12,
8241
8572
  writeStatus
8242
8573
  } from "@basou/core";
@@ -8256,22 +8587,24 @@ async function runStatus(options, ctx = {}) {
8256
8587
  async function doRunStatus(options, ctx) {
8257
8588
  const cwd = ctx.cwd ?? process.cwd();
8258
8589
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
8259
- const paths = basouPaths17(repositoryRoot);
8590
+ const paths = basouPaths18(repositoryRoot);
8260
8591
  try {
8261
- await assertBasouRootSafe14(paths.root);
8592
+ await assertBasouRootSafe15(paths.root);
8262
8593
  } catch (error) {
8263
- if (findErrorCode13(error, "ENOENT")) {
8594
+ if (findErrorCode14(error, "ENOENT")) {
8264
8595
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8265
8596
  }
8266
8597
  throw error;
8267
8598
  }
8268
8599
  let manifest;
8269
8600
  try {
8270
- manifest = await readManifest9(paths);
8601
+ manifest = await readManifest10(paths);
8271
8602
  } catch (error) {
8272
- if (findErrorCode13(error, "ENOENT")) {
8603
+ if (findErrorCode14(error, "ENOENT")) {
8273
8604
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8274
8605
  }
8606
+ const gateMessage = formatVersionGateMessage(error);
8607
+ if (gateMessage !== void 0) throw new Error(gateMessage, { cause: error });
8275
8608
  throw new Error("Failed to read workspace manifest", { cause: error });
8276
8609
  }
8277
8610
  const snapshot = await buildStatusSnapshot({ manifest, paths });
@@ -8303,23 +8636,35 @@ async function resolveRepositoryRootForStatus(cwd) {
8303
8636
  throw error;
8304
8637
  }
8305
8638
  }
8639
+ function formatVersionGateMessage(error) {
8640
+ const issues = error.issues;
8641
+ if (!Array.isArray(issues)) return void 0;
8642
+ for (const issue of issues) {
8643
+ const path = issue.path;
8644
+ const message = issue.message;
8645
+ if (Array.isArray(path) && (path.includes("schema_version") || path.includes("basou_version")) && typeof message === "string" && message.startsWith("unsupported .basou format version")) {
8646
+ return message;
8647
+ }
8648
+ }
8649
+ return void 0;
8650
+ }
8306
8651
 
8307
8652
  // src/commands/task.ts
8308
- import { readFile as readFile6 } from "fs/promises";
8309
- import { join as join14 } from "path";
8653
+ import { readFile as readFile7 } from "fs/promises";
8654
+ import { join as join15 } from "path";
8310
8655
  import {
8311
8656
  archiveTask,
8312
- assertBasouRootSafe as assertBasouRootSafe15,
8313
- basouPaths as basouPaths18,
8657
+ assertBasouRootSafe as assertBasouRootSafe16,
8658
+ basouPaths as basouPaths19,
8314
8659
  createTaskWithEvent,
8315
8660
  deleteTask,
8316
8661
  editTask,
8317
8662
  enumerateArchivedTaskIds,
8318
- findErrorCode as findErrorCode14,
8663
+ findErrorCode as findErrorCode15,
8319
8664
  loadSessionEntries as loadSessionEntries3,
8320
8665
  loadTaskEntries,
8321
8666
  prefixedUlid as prefixedUlid5,
8322
- readManifest as readManifest10,
8667
+ readManifest as readManifest11,
8323
8668
  readTaskFile,
8324
8669
  readTaskFileWithArchiveFallback,
8325
8670
  reconcileAllTasks,
@@ -8411,8 +8756,8 @@ async function doRunTaskNew(options, ctx) {
8411
8756
  }
8412
8757
  const cwd = ctx.cwd ?? process.cwd();
8413
8758
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
8414
- const paths = basouPaths18(repositoryRoot);
8415
- await assertWorkspaceInitialized12(paths.root);
8759
+ const paths = basouPaths19(repositoryRoot);
8760
+ await assertWorkspaceInitialized13(paths.root);
8416
8761
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
8417
8762
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
8418
8763
  const occurredAt = now.toISOString();
@@ -8446,7 +8791,7 @@ async function doRunTaskNew(options, ctx) {
8446
8791
  });
8447
8792
  return;
8448
8793
  }
8449
- const manifest = await readManifest10(paths);
8794
+ const manifest = await readManifest11(paths);
8450
8795
  const result = await createTaskWithEvent({
8451
8796
  mode: "ad-hoc",
8452
8797
  paths,
@@ -8520,8 +8865,8 @@ async function runTaskList(options, ctx = {}) {
8520
8865
  async function doRunTaskList(options, ctx) {
8521
8866
  const cwd = ctx.cwd ?? process.cwd();
8522
8867
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
8523
- const paths = basouPaths18(repositoryRoot);
8524
- await assertWorkspaceInitialized12(paths.root);
8868
+ const paths = basouPaths19(repositoryRoot);
8869
+ await assertWorkspaceInitialized13(paths.root);
8525
8870
  const entries = await loadTaskEntries(paths, {
8526
8871
  onSkip: (id, reason) => printTaskSkip(id, reason)
8527
8872
  });
@@ -8624,15 +8969,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
8624
8969
  async function doRunTaskShow(idInput, options, ctx) {
8625
8970
  const cwd = ctx.cwd ?? process.cwd();
8626
8971
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
8627
- const paths = basouPaths18(repositoryRoot);
8628
- await assertWorkspaceInitialized12(paths.root);
8972
+ const paths = basouPaths19(repositoryRoot);
8973
+ await assertWorkspaceInitialized13(paths.root);
8629
8974
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
8630
8975
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
8631
8976
  const sessions = await loadSessionEntries3(paths, { now: /* @__PURE__ */ new Date() });
8632
8977
  const events = [];
8633
8978
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
8634
8979
  for (const s of sessions) {
8635
- const sessionDir = join14(paths.sessions, s.sessionId);
8980
+ const sessionDir = join15(paths.sessions, s.sessionId);
8636
8981
  try {
8637
8982
  for await (const ev of replayEvents3(sessionDir, {
8638
8983
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -8768,8 +9113,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
8768
9113
  const newStatus = parseTaskStatusPositional(newStatusInput);
8769
9114
  const cwd = ctx.cwd ?? process.cwd();
8770
9115
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
8771
- const paths = basouPaths18(repositoryRoot);
8772
- await assertWorkspaceInitialized12(paths.root);
9116
+ const paths = basouPaths19(repositoryRoot);
9117
+ await assertWorkspaceInitialized13(paths.root);
8773
9118
  const taskId = await resolveTaskId2(paths, taskIdInput);
8774
9119
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
8775
9120
  const occurredAt = now.toISOString();
@@ -8794,7 +9139,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
8794
9139
  });
8795
9140
  return;
8796
9141
  }
8797
- const manifest = await readManifest10(paths);
9142
+ const manifest = await readManifest11(paths);
8798
9143
  const result = await updateTaskStatusWithEvent({
8799
9144
  mode: "ad-hoc",
8800
9145
  paths,
@@ -8845,9 +9190,9 @@ async function runTaskReconcile(options, ctx = {}) {
8845
9190
  async function doRunTaskReconcile(options, ctx) {
8846
9191
  const cwd = ctx.cwd ?? process.cwd();
8847
9192
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
8848
- const paths = basouPaths18(repositoryRoot);
8849
- await assertWorkspaceInitialized12(paths.root);
8850
- const manifest = await readManifest10(paths);
9193
+ const paths = basouPaths19(repositoryRoot);
9194
+ await assertWorkspaceInitialized13(paths.root);
9195
+ const manifest = await readManifest11(paths);
8851
9196
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
8852
9197
  const write = options.write === true;
8853
9198
  const verbose = isVerbose(options);
@@ -9025,9 +9370,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
9025
9370
  }
9026
9371
  const cwd = ctx.cwd ?? process.cwd();
9027
9372
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
9028
- const paths = basouPaths18(repositoryRoot);
9029
- await assertWorkspaceInitialized12(paths.root);
9030
- const manifest = await readManifest10(paths);
9373
+ const paths = basouPaths19(repositoryRoot);
9374
+ await assertWorkspaceInitialized13(paths.root);
9375
+ const manifest = await readManifest11(paths);
9031
9376
  const taskId = await resolveTaskId2(paths, taskIdInput);
9032
9377
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
9033
9378
  const write = options.write === true;
@@ -9105,9 +9450,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
9105
9450
  }
9106
9451
  const cwd = ctx.cwd ?? process.cwd();
9107
9452
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
9108
- const paths = basouPaths18(repositoryRoot);
9109
- await assertWorkspaceInitialized12(paths.root);
9110
- const manifest = await readManifest10(paths);
9453
+ const paths = basouPaths19(repositoryRoot);
9454
+ await assertWorkspaceInitialized13(paths.root);
9455
+ const manifest = await readManifest11(paths);
9111
9456
  const taskId = await resolveTaskId2(paths, taskIdInput);
9112
9457
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
9113
9458
  const occurredAt = now.toISOString();
@@ -9161,9 +9506,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
9161
9506
  }
9162
9507
  const cwd = ctx.cwd ?? process.cwd();
9163
9508
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
9164
- const paths = basouPaths18(repositoryRoot);
9165
- await assertWorkspaceInitialized12(paths.root);
9166
- const manifest = await readManifest10(paths);
9509
+ const paths = basouPaths19(repositoryRoot);
9510
+ await assertWorkspaceInitialized13(paths.root);
9511
+ const manifest = await readManifest11(paths);
9167
9512
  const taskId = await resolveTaskId2(paths, taskIdInput);
9168
9513
  if (options.yes !== true) {
9169
9514
  await confirmDestructiveAction("delete", taskId);
@@ -9206,9 +9551,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
9206
9551
  }
9207
9552
  const cwd = ctx.cwd ?? process.cwd();
9208
9553
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
9209
- const paths = basouPaths18(repositoryRoot);
9210
- await assertWorkspaceInitialized12(paths.root);
9211
- const manifest = await readManifest10(paths);
9554
+ const paths = basouPaths19(repositoryRoot);
9555
+ await assertWorkspaceInitialized13(paths.root);
9556
+ const manifest = await readManifest11(paths);
9212
9557
  const taskId = await resolveTaskId2(paths, taskIdInput);
9213
9558
  if (options.yes !== true) {
9214
9559
  await confirmDestructiveAction("archive", taskId);
@@ -9320,12 +9665,12 @@ function parsePositiveInt2(raw) {
9320
9665
  }
9321
9666
  async function readDescriptionFile(path) {
9322
9667
  try {
9323
- return await readFile6(path, "utf8");
9668
+ return await readFile7(path, "utf8");
9324
9669
  } catch (error) {
9325
- if (findErrorCode14(error, "ENOENT")) {
9670
+ if (findErrorCode15(error, "ENOENT")) {
9326
9671
  throw new Error("Description source not found", { cause: error });
9327
9672
  }
9328
- if (findErrorCode14(error, "EISDIR")) {
9673
+ if (findErrorCode15(error, "EISDIR")) {
9329
9674
  throw new Error("Description source is not a file", { cause: error });
9330
9675
  }
9331
9676
  throw new Error("Failed to read description source", { cause: error });
@@ -9334,11 +9679,11 @@ async function readDescriptionFile(path) {
9334
9679
  async function resolveRepositoryRootForTask(cwd, subcmd) {
9335
9680
  return resolveBasouRootForCommand(cwd, `task ${subcmd}`);
9336
9681
  }
9337
- async function assertWorkspaceInitialized12(basouRoot) {
9682
+ async function assertWorkspaceInitialized13(basouRoot) {
9338
9683
  try {
9339
- await assertBasouRootSafe15(basouRoot);
9684
+ await assertBasouRootSafe16(basouRoot);
9340
9685
  } catch (error) {
9341
- if (findErrorCode14(error, "ENOENT")) {
9686
+ if (findErrorCode15(error, "ENOENT")) {
9342
9687
  throw new Error("Workspace not initialized. Run 'basou init' first.");
9343
9688
  }
9344
9689
  throw error;
@@ -9426,10 +9771,10 @@ function maxLen3(values, floor) {
9426
9771
 
9427
9772
  // src/commands/verify.ts
9428
9773
  import {
9429
- assertBasouRootSafe as assertBasouRootSafe16,
9430
- basouPaths as basouPaths19,
9774
+ assertBasouRootSafe as assertBasouRootSafe17,
9775
+ basouPaths as basouPaths20,
9431
9776
  enumerateSessionDirs as enumerateSessionDirs3,
9432
- findErrorCode as findErrorCode15,
9777
+ findErrorCode as findErrorCode16,
9433
9778
  resolveRepositoryRoot as resolveRepositoryRoot13,
9434
9779
  resolveSessionId as resolveSessionId5,
9435
9780
  verifyEventsChain
@@ -9453,8 +9798,8 @@ async function doRunVerify(options, ctx) {
9453
9798
  }
9454
9799
  const cwd = ctx.cwd ?? process.cwd();
9455
9800
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
9456
- const paths = basouPaths19(repositoryRoot);
9457
- await assertWorkspaceInitialized13(paths.root);
9801
+ const paths = basouPaths20(repositoryRoot);
9802
+ await assertWorkspaceInitialized14(paths.root);
9458
9803
  const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
9459
9804
  const rows = [];
9460
9805
  for (const sessionId of sessionIds) {
@@ -9511,11 +9856,11 @@ async function resolveRepositoryRootForVerify(cwd) {
9511
9856
  throw error;
9512
9857
  }
9513
9858
  }
9514
- async function assertWorkspaceInitialized13(basouRoot) {
9859
+ async function assertWorkspaceInitialized14(basouRoot) {
9515
9860
  try {
9516
- await assertBasouRootSafe16(basouRoot);
9861
+ await assertBasouRootSafe17(basouRoot);
9517
9862
  } catch (error) {
9518
- if (findErrorCode15(error, "ENOENT")) {
9863
+ if (findErrorCode16(error, "ENOENT")) {
9519
9864
  throw new Error("Workspace not initialized. Run 'basou init' first.");
9520
9865
  }
9521
9866
  throw error;
@@ -9525,12 +9870,12 @@ async function assertWorkspaceInitialized13(basouRoot) {
9525
9870
  // src/commands/view.ts
9526
9871
  import { spawn } from "child_process";
9527
9872
  import { createHash } from "crypto";
9528
- import { basename as basename7, resolve as resolve11 } from "path";
9873
+ import { basename as basename8, resolve as resolve13 } from "path";
9529
9874
  import {
9530
- assertBasouRootSafe as assertBasouRootSafe17,
9531
- basouPaths as basouPaths20,
9532
- findErrorCode as findErrorCode17,
9533
- readManifest as readManifest13,
9875
+ assertBasouRootSafe as assertBasouRootSafe18,
9876
+ basouPaths as basouPaths21,
9877
+ findErrorCode as findErrorCode18,
9878
+ readManifest as readManifest14,
9534
9879
  resolveRepositoryRoot as resolveRepositoryRoot14
9535
9880
  } from "@basou/core";
9536
9881
  import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
@@ -9538,9 +9883,9 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
9538
9883
  // src/lib/portfolio-safety.ts
9539
9884
  import { execFile } from "child_process";
9540
9885
  import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
9541
- import { isAbsolute as isAbsolute7, join as join15, relative as relative4, resolve as resolve10 } from "path";
9886
+ import { isAbsolute as isAbsolute7, join as join16, relative as relative4, resolve as resolve11 } from "path";
9542
9887
  import { promisify } from "util";
9543
- import { readManifest as readManifest11 } from "@basou/core";
9888
+ import { readManifest as readManifest12 } from "@basou/core";
9544
9889
  var execFileAsync = promisify(execFile);
9545
9890
  function errorCode(error) {
9546
9891
  return error instanceof Error ? error.code : void 0;
@@ -9549,7 +9894,7 @@ async function canonical(p) {
9549
9894
  try {
9550
9895
  return await realpath2(p);
9551
9896
  } catch {
9552
- return resolve10(p);
9897
+ return resolve11(p);
9553
9898
  }
9554
9899
  }
9555
9900
  function isInside(child, parent) {
@@ -9562,7 +9907,7 @@ function isBasouPath(p) {
9562
9907
  async function inspectRepo(repoPath) {
9563
9908
  let hasEntry = false;
9564
9909
  try {
9565
- await lstat2(join15(repoPath, ".basou"));
9910
+ await lstat2(join16(repoPath, ".basou"));
9566
9911
  hasEntry = true;
9567
9912
  } catch (error) {
9568
9913
  if (errorCode(error) !== "ENOENT") {
@@ -9593,7 +9938,7 @@ async function checkPortfolioSafety(workspaces) {
9593
9938
  const wsReal = await canonical(ws.repoRoot);
9594
9939
  let sourceRoots = [];
9595
9940
  try {
9596
- const manifest = await readManifest11(ws.paths);
9941
+ const manifest = await readManifest12(ws.paths);
9597
9942
  sourceRoots = manifest.import?.source_roots ?? [];
9598
9943
  } catch (error) {
9599
9944
  if (error instanceof Error && error.message === "YAML file not found") {
@@ -9611,7 +9956,7 @@ async function checkPortfolioSafety(workspaces) {
9611
9956
  }
9612
9957
  const monitored = /* @__PURE__ */ new Map();
9613
9958
  for (const root of sourceRoots) {
9614
- const display = resolve10(ws.repoRoot, root);
9959
+ const display = resolve11(ws.repoRoot, root);
9615
9960
  const real = await canonical(display);
9616
9961
  if (real !== wsReal) monitored.set(real, display);
9617
9962
  }
@@ -9663,25 +10008,58 @@ function formatSafetyReport(result) {
9663
10008
 
9664
10009
  // src/lib/view-server.ts
9665
10010
  import { createServer } from "http";
9666
- import { join as join16 } from "path";
10011
+ import { basename as basename7, join as join17, resolve as resolve12 } from "path";
9667
10012
  import {
9668
10013
  computeWorkStats as computeWorkStats2,
9669
10014
  enumerateApprovals as enumerateApprovals2,
9670
- findErrorCode as findErrorCode16,
10015
+ findErrorCode as findErrorCode17,
9671
10016
  isLazyExpired as isLazyExpired2,
9672
10017
  loadApproval as loadApproval2,
9673
10018
  loadSessionEntries as loadSessionEntries4,
9674
10019
  loadTaskEntries as loadTaskEntries2,
9675
10020
  readAllEvents as readAllEvents2,
9676
- readManifest as readManifest12,
9677
- readMarkdownFile as readMarkdownFile6,
10021
+ readManifest as readManifest13,
10022
+ readMarkdownFile as readMarkdownFile7,
9678
10023
  readSessionYaml as readSessionYaml3,
9679
10024
  readTaskFile as readTaskFile2,
9680
10025
  renderDecisions as renderDecisions3,
9681
10026
  renderHandoff as renderHandoff3,
9682
- summarizeOrientation
10027
+ summarizeOrientation,
10028
+ tryRemoteUrl
9683
10029
  } from "@basou/core";
9684
10030
 
10031
+ // src/lib/repo-url.ts
10032
+ function toBrowserUrl(remote) {
10033
+ const raw = remote.trim();
10034
+ if (raw.length === 0) return null;
10035
+ let host;
10036
+ let path;
10037
+ if (raw.includes("://")) {
10038
+ let parsed;
10039
+ try {
10040
+ parsed = new URL(raw);
10041
+ } catch {
10042
+ return null;
10043
+ }
10044
+ const scheme = parsed.protocol;
10045
+ if (scheme !== "ssh:" && scheme !== "git:" && scheme !== "http:" && scheme !== "https:") {
10046
+ return null;
10047
+ }
10048
+ host = scheme === "http:" || scheme === "https:" ? parsed.host : parsed.hostname;
10049
+ path = parsed.pathname;
10050
+ } else {
10051
+ const match = /^[^@/\s]+@([^:/\s@]+):(.+)$/.exec(raw);
10052
+ if (match === null || match[1] === void 0 || match[2] === void 0) return null;
10053
+ host = match[1];
10054
+ path = match[2];
10055
+ }
10056
+ const cleanPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
10057
+ if (host.length === 0 || cleanPath.length === 0) return null;
10058
+ if (/\s/.test(host) || /\s/.test(cleanPath)) return null;
10059
+ if (cleanPath.split("/").some((seg) => seg === "." || seg === "..")) return null;
10060
+ return `https://${host}/${cleanPath}`;
10061
+ }
10062
+
9685
10063
  // src/lib/view-ui.ts
9686
10064
  var VIEW_HTML = `<!doctype html>
9687
10065
  <html lang="en">
@@ -10083,9 +10461,30 @@ var VIEW_HTML = `<!doctype html>
10083
10461
  card(c.approvalsPending, 'approvals pending')
10084
10462
  ]);
10085
10463
  detail.appendChild(cards);
10464
+ renderRepos(detail, d.repos || []);
10086
10465
  detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
10087
10466
  }).catch(fail);
10088
10467
  }
10468
+ // The declared roster repos, each with a LIVE git link (derived server-side
10469
+ // from the repo's local git config at request time, never stored). A repo
10470
+ // with no remote renders as "local only". Links open in a new tab.
10471
+ function renderRepos(detail, repos) {
10472
+ if (!repos.length) return;
10473
+ detail.appendChild(el('h3', { text: 'Repos' }));
10474
+ var rows = el('div', { class: 'repos' }, []);
10475
+ repos.forEach(function (r) {
10476
+ var vis = r.visibility ? (' (' + r.visibility + ')') : '';
10477
+ var link = r.url
10478
+ ? el('a', { href: r.url, target: '_blank', rel: 'noopener noreferrer', text: r.url })
10479
+ : el('span', { class: 'muted', text: 'local only' });
10480
+ rows.appendChild(el('div', { class: 'f' }, [
10481
+ el('strong', { text: r.name }),
10482
+ el('span', { class: 'muted', text: vis + ' ' }),
10483
+ link
10484
+ ]));
10485
+ });
10486
+ detail.appendChild(rows);
10487
+ }
10089
10488
  function card(n, label) {
10090
10489
  return el('div', { class: 'card' }, [
10091
10490
  el('div', { class: 'n', text: String(n) }),
@@ -10324,7 +10723,7 @@ function startViewServer(opts) {
10324
10723
  };
10325
10724
  let boundPort = port;
10326
10725
  const getPort = () => boundPort;
10327
- return new Promise((resolve12, reject) => {
10726
+ return new Promise((resolve14, reject) => {
10328
10727
  const server = createServer((req, res) => {
10329
10728
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
10330
10729
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -10335,7 +10734,7 @@ function startViewServer(opts) {
10335
10734
  const address = server.address();
10336
10735
  boundPort = isAddressInfo(address) ? address.port : port;
10337
10736
  server.off("error", reject);
10338
- resolve12({
10737
+ resolve14({
10339
10738
  url: `http://${host}:${boundPort}`,
10340
10739
  port: boundPort,
10341
10740
  close: () => closeServer(server)
@@ -10347,8 +10746,8 @@ function isAddressInfo(value) {
10347
10746
  return value !== null && typeof value === "object";
10348
10747
  }
10349
10748
  function closeServer(server) {
10350
- return new Promise((resolve12) => {
10351
- server.close(() => resolve12());
10749
+ return new Promise((resolve14) => {
10750
+ server.close(() => resolve14());
10352
10751
  server.closeAllConnections();
10353
10752
  });
10354
10753
  }
@@ -10391,20 +10790,29 @@ async function handleGet(res, pathname, deps) {
10391
10790
  sendError(res, 404, "Unknown workspace");
10392
10791
  return;
10393
10792
  }
10394
- if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
10793
+ if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider, remoteUrlOf(deps))) {
10395
10794
  sendError(res, 404, "Not found");
10396
10795
  }
10397
10796
  return;
10398
10797
  }
10399
10798
  if (pathname.startsWith(API_PREFIX)) {
10400
10799
  const sub = pathname.slice(API_PREFIX.length);
10401
- if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
10800
+ if (!await handleWorkspaceGet(
10801
+ res,
10802
+ sub,
10803
+ primaryWorkspace(deps),
10804
+ deps.nowProvider,
10805
+ remoteUrlOf(deps)
10806
+ )) {
10402
10807
  sendError(res, 404, "Not found");
10403
10808
  }
10404
10809
  return;
10405
10810
  }
10406
10811
  sendError(res, 404, "Not found");
10407
10812
  }
10813
+ function remoteUrlOf(deps) {
10814
+ return deps.remoteUrlOf ?? tryRemoteUrl;
10815
+ }
10408
10816
  async function handlePost(res, pathname, body, deps, runExclusive) {
10409
10817
  const scoped = matchWsRoute(pathname);
10410
10818
  if (scoped !== null) {
@@ -10427,9 +10835,9 @@ async function handlePost(res, pathname, body, deps, runExclusive) {
10427
10835
  }
10428
10836
  sendError(res, 404, "Not found");
10429
10837
  }
10430
- async function handleWorkspaceGet(res, sub, ws, nowProvider) {
10838
+ async function handleWorkspaceGet(res, sub, ws, nowProvider, resolveRemoteUrl) {
10431
10839
  if (sub === "overview") {
10432
- sendJson(res, 200, await overview(ws, nowProvider));
10840
+ sendJson(res, 200, await overview(ws, nowProvider, resolveRemoteUrl));
10433
10841
  return true;
10434
10842
  }
10435
10843
  if (sub === "sessions") {
@@ -10562,12 +10970,12 @@ async function captureStaleness(ws, nowIso) {
10562
10970
  const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
10563
10971
  return probe === null ? { checked: false } : { checked: true, ...probe };
10564
10972
  }
10565
- async function overview(ws, nowProvider) {
10973
+ async function overview(ws, nowProvider, resolveRemoteUrl) {
10566
10974
  let manifest;
10567
10975
  try {
10568
- manifest = await readManifest12(ws.paths);
10976
+ manifest = await readManifest13(ws.paths);
10569
10977
  } catch (error) {
10570
- if (findErrorCode16(error, "ENOENT")) {
10978
+ if (findErrorCode17(error, "ENOENT")) {
10571
10979
  return { initialized: false, repoRoot: ws.repoRoot };
10572
10980
  }
10573
10981
  throw error;
@@ -10575,6 +10983,7 @@ async function overview(ws, nowProvider) {
10575
10983
  const nowIso = nowProvider().toISOString();
10576
10984
  const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
10577
10985
  const approvals = await enumerateApprovals2(ws.paths);
10986
+ const repos = await rosterRepos(ws.repoRoot, manifest, resolveRemoteUrl);
10578
10987
  return {
10579
10988
  initialized: true,
10580
10989
  repoRoot: ws.repoRoot,
@@ -10592,9 +11001,26 @@ async function overview(ws, nowProvider) {
10592
11001
  approvalsPending: approvals.pending.length,
10593
11002
  approvalsResolved: approvals.resolved.length
10594
11003
  },
11004
+ repos,
10595
11005
  generatedAt: nowIso
10596
11006
  };
10597
11007
  }
11008
+ async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
11009
+ const roster = manifest.repos ?? [];
11010
+ return Promise.all(
11011
+ roster.map(async (repo) => {
11012
+ const abs = resolve12(repoRoot, repo.path);
11013
+ const remote = await resolveRemoteUrl(abs);
11014
+ const url = remote !== void 0 ? toBrowserUrl(remote) : null;
11015
+ return {
11016
+ name: basename7(abs),
11017
+ path: repo.path,
11018
+ ...url !== null ? { url } : {},
11019
+ ...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
11020
+ };
11021
+ })
11022
+ );
11023
+ }
10598
11024
  async function sessionsList(ws, nowProvider) {
10599
11025
  const entries = await loadSessionEntries4(ws.paths, { now: nowProvider() });
10600
11026
  const sessions = entries.map((entry) => ({
@@ -10622,7 +11048,7 @@ async function sessionDetail(ws, sessionId) {
10622
11048
  throw error;
10623
11049
  }
10624
11050
  try {
10625
- const events = await readAllEvents2(join16(ws.paths.sessions, sessionId));
11051
+ const events = await readAllEvents2(join17(ws.paths.sessions, sessionId));
10626
11052
  return { session, events };
10627
11053
  } catch {
10628
11054
  return { session, events: [], degraded: true };
@@ -10644,7 +11070,7 @@ async function taskDetail(ws, taskId) {
10644
11070
  }
10645
11071
  }
10646
11072
  async function decisionsView(ws, nowProvider) {
10647
- const fromDisk = await readMarkdownFile6(ws.paths.files.decisions);
11073
+ const fromDisk = await readMarkdownFile7(ws.paths.files.decisions);
10648
11074
  if (fromDisk !== null) {
10649
11075
  return { body: fromDisk, fromDisk: true };
10650
11076
  }
@@ -10667,7 +11093,7 @@ async function approvalsView(ws, nowProvider) {
10667
11093
  return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
10668
11094
  }
10669
11095
  async function handoffView(ws, nowProvider) {
10670
- const fromDisk = await readMarkdownFile6(ws.paths.files.handoff);
11096
+ const fromDisk = await readMarkdownFile7(ws.paths.files.handoff);
10671
11097
  if (fromDisk !== null) {
10672
11098
  return { body: fromDisk, fromDisk: true };
10673
11099
  }
@@ -10836,18 +11262,23 @@ async function doRunView(options, ctx) {
10836
11262
  }
10837
11263
  async function buildSingleDeps(ctx, cwd) {
10838
11264
  const repositoryRoot = await resolveRepositoryRootForView(cwd);
10839
- const paths = basouPaths20(repositoryRoot);
10840
- await assertWorkspaceInitialized14(paths.root);
11265
+ const paths = basouPaths21(repositoryRoot);
11266
+ await assertWorkspaceInitialized15(paths.root);
10841
11267
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
10842
- return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
11268
+ return {
11269
+ workspaces: [entry],
11270
+ mode: "single",
11271
+ nowProvider: nowProviderOf(ctx),
11272
+ ...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
11273
+ };
10843
11274
  }
10844
11275
  async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
10845
- const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve11(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
11276
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve13(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
10846
11277
  const entries = [];
10847
11278
  const seenPath = /* @__PURE__ */ new Set();
10848
11279
  const seenKey = /* @__PURE__ */ new Set();
10849
11280
  for (const spec of specs) {
10850
- const repoRoot = resolve11(spec.path);
11281
+ const repoRoot = resolve13(spec.path);
10851
11282
  if (seenPath.has(repoRoot)) continue;
10852
11283
  seenPath.add(repoRoot);
10853
11284
  const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
@@ -10857,17 +11288,22 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
10857
11288
  entries.push({ ...entry, key });
10858
11289
  }
10859
11290
  if (entries.length === 0) throw new Error("No workspaces to show.");
10860
- return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
11291
+ return {
11292
+ workspaces: entries,
11293
+ mode: "portfolio",
11294
+ nowProvider: nowProviderOf(ctx),
11295
+ ...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
11296
+ };
10861
11297
  }
10862
11298
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
10863
- const paths = basouPaths20(repoRoot);
11299
+ const paths = basouPaths21(repoRoot);
10864
11300
  const importCtx = {
10865
11301
  cwd: repoRoot,
10866
11302
  ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
10867
11303
  ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
10868
11304
  };
10869
11305
  try {
10870
- const manifest = await readManifest13(paths);
11306
+ const manifest = await readManifest14(paths);
10871
11307
  return {
10872
11308
  key: manifest.workspace.id,
10873
11309
  label: labelOverride ?? manifest.workspace.name,
@@ -10880,7 +11316,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
10880
11316
  const notFound = error instanceof Error && error.message === "YAML file not found";
10881
11317
  return {
10882
11318
  key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
10883
- label: labelOverride ?? basename7(repoRoot),
11319
+ label: labelOverride ?? basename8(repoRoot),
10884
11320
  paths,
10885
11321
  repoRoot,
10886
11322
  importCtx,
@@ -10896,7 +11332,7 @@ async function startListening(port, deps) {
10896
11332
  try {
10897
11333
  return await startViewServer({ port, deps });
10898
11334
  } catch (error) {
10899
- if (findErrorCode17(error, "EADDRINUSE")) {
11335
+ if (findErrorCode18(error, "EADDRINUSE")) {
10900
11336
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
10901
11337
  cause: error
10902
11338
  });
@@ -10919,7 +11355,7 @@ function openInBrowser(url, override) {
10919
11355
  }
10920
11356
  }
10921
11357
  function waitForShutdown(signal) {
10922
- return new Promise((resolve12) => {
11358
+ return new Promise((resolve14) => {
10923
11359
  const cleanup = () => {
10924
11360
  process.off("SIGINT", onSignal);
10925
11361
  process.off("SIGTERM", onSignal);
@@ -10927,18 +11363,18 @@ function waitForShutdown(signal) {
10927
11363
  };
10928
11364
  const onSignal = () => {
10929
11365
  cleanup();
10930
- resolve12();
11366
+ resolve14();
10931
11367
  };
10932
11368
  const onAbort = () => {
10933
11369
  cleanup();
10934
- resolve12();
11370
+ resolve14();
10935
11371
  };
10936
11372
  process.on("SIGINT", onSignal);
10937
11373
  process.on("SIGTERM", onSignal);
10938
11374
  if (signal !== void 0) {
10939
11375
  if (signal.aborted) {
10940
11376
  cleanup();
10941
- resolve12();
11377
+ resolve14();
10942
11378
  return;
10943
11379
  }
10944
11380
  signal.addEventListener("abort", onAbort);
@@ -10957,11 +11393,11 @@ async function resolveRepositoryRootForView(cwd) {
10957
11393
  throw error;
10958
11394
  }
10959
11395
  }
10960
- async function assertWorkspaceInitialized14(basouRoot) {
11396
+ async function assertWorkspaceInitialized15(basouRoot) {
10961
11397
  try {
10962
- await assertBasouRootSafe17(basouRoot);
11398
+ await assertBasouRootSafe18(basouRoot);
10963
11399
  } catch (error) {
10964
- if (findErrorCode17(error, "ENOENT")) {
11400
+ if (findErrorCode18(error, "ENOENT")) {
10965
11401
  throw new Error("Workspace not initialized. Run 'basou init' first.");
10966
11402
  }
10967
11403
  throw error;
@@ -10993,6 +11429,7 @@ function buildProgram() {
10993
11429
  registerDecisionsCommand(program);
10994
11430
  registerReportCommand(program);
10995
11431
  registerOrientCommand(program);
11432
+ registerReviewCommand(program);
10996
11433
  registerReviewGapsCommand(program);
10997
11434
  registerProjectCommand(program);
10998
11435
  registerProtocolCommand(program);