@basou/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1512,7 +1512,8 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1512
1512
  reimported: 0,
1513
1513
  skippedLegacy: 0,
1514
1514
  skippedDecreased: 0,
1515
- skippedDuplicate: 0
1515
+ skippedDuplicate: 0,
1516
+ skippedUnverifiable: 0
1516
1517
  };
1517
1518
  let sanitizedPaths = 0;
1518
1519
  const validate = (payload) => {
@@ -1554,7 +1555,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1554
1555
  if (outcome.status === "skipped") {
1555
1556
  const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : outcome.reason === "prior_chain_broken" ? "prior events.jsonl failed hash-chain verification (run 'basou verify')" : "source changed in a non-append way (derived events would be dropped)";
1556
1557
  console.error(`Import: ${externalId} ${detail}; re-import skipped`);
1557
- counts.skippedNoAction++;
1558
+ counts.skippedUnverifiable++;
1558
1559
  continue;
1559
1560
  }
1560
1561
  counts.reimported++;
@@ -1825,7 +1826,8 @@ function printImportResult(options, results, counts) {
1825
1826
  reimported,
1826
1827
  skippedLegacy,
1827
1828
  skippedDecreased,
1828
- skippedDuplicate
1829
+ skippedDuplicate,
1830
+ skippedUnverifiable
1829
1831
  } = counts;
1830
1832
  if (options.json === true) {
1831
1833
  console.log(
@@ -1844,6 +1846,7 @@ function printImportResult(options, results, counts) {
1844
1846
  skipped_legacy_untracked: skippedLegacy,
1845
1847
  skipped_decreased: skippedDecreased,
1846
1848
  skipped_duplicate: skippedDuplicate,
1849
+ skipped_unverifiable: skippedUnverifiable,
1847
1850
  event_total: eventTotal,
1848
1851
  dry_run: isDry
1849
1852
  })
@@ -1856,6 +1859,8 @@ function printImportResult(options, results, counts) {
1856
1859
  if (skippedLegacy > 0) skipParts.push(`${skippedLegacy} legacy (untracked size)`);
1857
1860
  if (skippedDecreased > 0) skipParts.push(`${skippedDecreased} shrank`);
1858
1861
  if (skippedDuplicate > 0) skipParts.push(`${skippedDuplicate} duplicated`);
1862
+ if (skippedUnverifiable > 0)
1863
+ skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
1859
1864
  const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
1860
1865
  const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
1861
1866
  if (isDry) {
@@ -1935,6 +1940,9 @@ function registerInitCommand(program2) {
1935
1940
  "Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
1936
1941
  collectValue,
1937
1942
  []
1943
+ ).option(
1944
+ "--local-only",
1945
+ "Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
1938
1946
  ).option("-f, --force", "Overwrite an existing manifest").option("-v, --verbose", "Show error causes").action(async (options) => {
1939
1947
  await runInit(options);
1940
1948
  });
@@ -1971,7 +1979,7 @@ async function doRunInit(options, ctx) {
1971
1979
  });
1972
1980
  await writeManifest(paths, manifest, { force: options.force === true });
1973
1981
  try {
1974
- await appendBasouGitignore(repositoryRoot);
1982
+ await appendBasouGitignore(repositoryRoot, { localOnly: options.localOnly === true });
1975
1983
  } catch (error) {
1976
1984
  renderGitignoreWarning(error, isVerbose(options));
1977
1985
  }
@@ -2000,15 +2008,21 @@ async function resolveRepositoryRootForInit(cwd) {
2000
2008
  }
2001
2009
  }
2002
2010
 
2003
- // src/commands/refresh.ts
2004
- import { assertBasouRootSafe as assertBasouRootSafe7, basouPaths as basouPaths7, findErrorCode as findErrorCode7, resolveRepositoryRoot as resolveRepositoryRoot8 } from "@basou/core";
2005
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
2011
+ // src/commands/orient.ts
2012
+ import {
2013
+ assertBasouRootSafe as assertBasouRootSafe7,
2014
+ basouPaths as basouPaths7,
2015
+ findErrorCode as findErrorCode6,
2016
+ renderOrientation as renderOrientation2,
2017
+ writeMarkdownFile as writeMarkdownFile4
2018
+ } from "@basou/core";
2006
2019
 
2007
2020
  // src/lib/provenance-actions.ts
2008
2021
  import {
2009
2022
  readMarkdownFile as readMarkdownFile3,
2010
2023
  renderDecisions as renderDecisions2,
2011
2024
  renderHandoff as renderHandoff2,
2025
+ renderOrientation,
2012
2026
  renderWithMarkers as renderWithMarkers3,
2013
2027
  writeMarkdownFile as writeMarkdownFile3
2014
2028
  } from "@basou/core";
@@ -2059,6 +2073,7 @@ async function runImport(adapter, fn) {
2059
2073
  skippedNoAction: readCount(json.skipped_no_action),
2060
2074
  skippedAlreadyImported: readCount(json.skipped_already_imported),
2061
2075
  skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
2076
+ skippedUnverifiable: readCount(json.skipped_unverifiable),
2062
2077
  eventTotal: readCount(json.event_total),
2063
2078
  dryRun: json.dry_run === true
2064
2079
  };
@@ -2107,6 +2122,17 @@ async function regenerateDecisions(paths, nowIso, callbacks) {
2107
2122
  );
2108
2123
  return { decisionCount: result.decisionCount };
2109
2124
  }
2125
+ async function regenerateOrientation(paths, nowIso, callbacks) {
2126
+ const result = await renderOrientation({ paths, nowIso, ...callbacks });
2127
+ await writeMarkdownFile3(paths.files.orientation, `${result.body}
2128
+ `);
2129
+ return {
2130
+ sessionCount: result.sessionCount,
2131
+ inFlightTaskCount: result.inFlightTaskCount,
2132
+ pendingApprovalsCount: result.pendingApprovalsCount,
2133
+ suspectCount: result.suspectCount
2134
+ };
2135
+ }
2110
2136
  async function refreshAll(args) {
2111
2137
  const { options, ctx, paths, nowIso } = args;
2112
2138
  const dryRun = options.dryRun === true;
@@ -2114,31 +2140,210 @@ async function refreshAll(args) {
2114
2140
  const codex = await importCodex(options, ctx);
2115
2141
  if (dryRun) {
2116
2142
  const skipped = { status: "skipped", reason: "dry-run" };
2117
- return { claudeCode, codex, handoff: skipped, decisions: skipped, dryRun };
2143
+ return {
2144
+ claudeCode,
2145
+ codex,
2146
+ handoff: skipped,
2147
+ decisions: skipped,
2148
+ orientation: skipped,
2149
+ dryRun
2150
+ };
2118
2151
  }
2119
2152
  const handoffCounts = await regenerateHandoff(paths, nowIso);
2120
2153
  const decisionCounts = await regenerateDecisions(paths, nowIso);
2154
+ const scoped = options.project !== void 0 && options.project.length > 0;
2155
+ const orientationCounts = await regenerateOrientation(
2156
+ paths,
2157
+ nowIso,
2158
+ scoped ? {} : {
2159
+ staleness: {
2160
+ newSessions: 0,
2161
+ updatedSessions: 0,
2162
+ unverifiableSessions: wouldBlock(claudeCode) + wouldBlock(codex)
2163
+ }
2164
+ }
2165
+ );
2121
2166
  return {
2122
2167
  claudeCode,
2123
2168
  codex,
2124
2169
  handoff: { status: "generated", ...handoffCounts },
2125
2170
  decisions: { status: "generated", ...decisionCounts },
2171
+ orientation: { status: "generated", ...orientationCounts },
2126
2172
  dryRun
2127
2173
  };
2128
2174
  }
2175
+ function wouldImport(outcome) {
2176
+ return outcome.status === "ran" ? outcome.importedCount : 0;
2177
+ }
2178
+ function wouldUpdate(outcome) {
2179
+ return outcome.status === "ran" ? outcome.reimportedCount + outcome.replacedCount : 0;
2180
+ }
2181
+ function wouldBlock(outcome) {
2182
+ return outcome.status === "ran" ? outcome.skippedUnverifiable : 0;
2183
+ }
2184
+ async function probeStaleness(args) {
2185
+ try {
2186
+ const dry = await refreshAll({
2187
+ options: { dryRun: true },
2188
+ ctx: args.ctx,
2189
+ paths: args.paths,
2190
+ nowIso: args.nowIso
2191
+ });
2192
+ return {
2193
+ newSessions: wouldImport(dry.claudeCode) + wouldImport(dry.codex),
2194
+ updatedSessions: wouldUpdate(dry.claudeCode) + wouldUpdate(dry.codex),
2195
+ unverifiableSessions: wouldBlock(dry.claudeCode) + wouldBlock(dry.codex)
2196
+ };
2197
+ } catch {
2198
+ return null;
2199
+ }
2200
+ }
2201
+
2202
+ // src/lib/repo-root.ts
2203
+ import { resolveBasouRepositoryRoot } from "@basou/core";
2204
+ async function resolveBasouRootForCommand(cwd, commandName) {
2205
+ try {
2206
+ return await resolveBasouRepositoryRoot(cwd, {
2207
+ onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
2208
+ });
2209
+ } catch (error) {
2210
+ if (error instanceof Error && error.message === "Not a git repository") {
2211
+ throw new Error(
2212
+ `Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
2213
+ { cause: error }
2214
+ );
2215
+ }
2216
+ throw error;
2217
+ }
2218
+ }
2219
+
2220
+ // src/commands/orient.ts
2221
+ function registerOrientCommand(program2) {
2222
+ program2.command("orient").description("Show the workspace's current position (also writes .basou/orientation.md)").option("-q, --quiet", "Write the file without printing the body").option("-v, --verbose", "Show error causes").action(async (opts) => {
2223
+ await runOrient(opts);
2224
+ });
2225
+ }
2226
+ async function runOrient(options, ctx = {}) {
2227
+ try {
2228
+ await doRunOrient(options, ctx);
2229
+ } catch (error) {
2230
+ renderCliError(error, { verbose: isVerbose(options) });
2231
+ process.exitCode = 1;
2232
+ }
2233
+ }
2234
+ async function doRunOrient(options, ctx) {
2235
+ const cwd = ctx.cwd ?? process.cwd();
2236
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "orient");
2237
+ const paths = basouPaths7(repositoryRoot);
2238
+ await assertWorkspaceInitialized6(paths.root);
2239
+ const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2240
+ const probeCtx = { cwd: repositoryRoot };
2241
+ if (ctx.claudeProjectsDir !== void 0) probeCtx.claudeProjectsDir = ctx.claudeProjectsDir;
2242
+ if (ctx.codexSessionsDir !== void 0) probeCtx.codexSessionsDir = ctx.codexSessionsDir;
2243
+ const staleness = await probeStaleness({ ctx: probeCtx, paths, nowIso });
2244
+ const result = await renderOrientation2({
2245
+ paths,
2246
+ nowIso,
2247
+ staleness,
2248
+ verbose: options.verbose === true,
2249
+ onWarning: (w, sid) => printReplayWarning(w, sid),
2250
+ onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
2251
+ onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2252
+ });
2253
+ await writeMarkdownFile4(paths.files.orientation, `${result.body}
2254
+ `);
2255
+ if (options.quiet === true) {
2256
+ console.log(
2257
+ `Generated .basou/orientation.md (sessions: ${result.sessionCount}, in-flight tasks: ${result.inFlightTaskCount}, pending approvals: ${result.pendingApprovalsCount}, suspect: ${result.suspectCount})`
2258
+ );
2259
+ } else {
2260
+ console.log(result.body);
2261
+ }
2262
+ }
2263
+ async function assertWorkspaceInitialized6(basouRoot) {
2264
+ try {
2265
+ await assertBasouRootSafe7(basouRoot);
2266
+ } catch (error) {
2267
+ if (findErrorCode6(error, "ENOENT")) {
2268
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
2269
+ }
2270
+ throw error;
2271
+ }
2272
+ }
2273
+
2274
+ // src/commands/refresh.ts
2275
+ import { assertBasouRootSafe as assertBasouRootSafe8, basouPaths as basouPaths8, findErrorCode as findErrorCode8 } from "@basou/core";
2276
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
2277
+
2278
+ // src/lib/portfolio-config.ts
2279
+ import { homedir as homedir3 } from "os";
2280
+ import { isAbsolute, join as join4, resolve as resolve3 } from "path";
2281
+ import { readYamlFile as readYamlFile3 } from "@basou/core";
2282
+ var DEFAULT_PORTFOLIO_CONFIG_PATH = join4(homedir3(), ".basou", "portfolio.yaml");
2283
+ function expandTilde(p) {
2284
+ if (p === "~") return homedir3();
2285
+ if (p.startsWith("~/")) return join4(homedir3(), p.slice(2));
2286
+ return p;
2287
+ }
2288
+ function isRecord(value) {
2289
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2290
+ }
2291
+ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
2292
+ let raw;
2293
+ try {
2294
+ raw = await readYamlFile3(configPath);
2295
+ } catch (error) {
2296
+ if (error instanceof Error && error.message === "YAML file not found") {
2297
+ throw new Error(
2298
+ "No portfolio config at ~/.basou/portfolio.yaml. Create one (a 'workspaces:' list of repo paths) or pass --workspace <path>."
2299
+ );
2300
+ }
2301
+ if (error instanceof Error && error.message === "Failed to parse YAML content") {
2302
+ throw new Error("~/.basou/portfolio.yaml is not valid YAML.");
2303
+ }
2304
+ throw error;
2305
+ }
2306
+ if (!isRecord(raw) || !Array.isArray(raw.workspaces)) {
2307
+ throw new Error("~/.basou/portfolio.yaml must contain a 'workspaces:' list.");
2308
+ }
2309
+ const seen = /* @__PURE__ */ new Set();
2310
+ const result = [];
2311
+ for (const entry of raw.workspaces) {
2312
+ if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) {
2313
+ throw new Error("Each portfolio workspace needs a non-empty string 'path'.");
2314
+ }
2315
+ if (entry.label !== void 0 && typeof entry.label !== "string") {
2316
+ throw new Error("A portfolio workspace 'label' must be a string when present.");
2317
+ }
2318
+ const expanded = expandTilde(entry.path.trim());
2319
+ if (!isAbsolute(expanded)) {
2320
+ throw new Error(
2321
+ "Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
2322
+ );
2323
+ }
2324
+ const abs = resolve3(expanded);
2325
+ if (seen.has(abs)) continue;
2326
+ seen.add(abs);
2327
+ result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
2328
+ }
2329
+ if (result.length === 0) {
2330
+ throw new Error("~/.basou/portfolio.yaml has no workspaces.");
2331
+ }
2332
+ return result;
2333
+ }
2129
2334
 
2130
2335
  // src/commands/refresh-watch.ts
2131
2336
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
2132
- import { homedir as homedir3 } from "os";
2133
- import { join as join4 } from "path";
2134
- import { findErrorCode as findErrorCode6 } from "@basou/core";
2337
+ import { homedir as homedir4 } from "os";
2338
+ import { join as join5 } from "path";
2339
+ import { findErrorCode as findErrorCode7 } from "@basou/core";
2135
2340
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
2136
2341
  var MIN_WATCH_INTERVAL_SEC = 5;
2137
2342
  var MAX_WATCH_INTERVAL_SEC = 86400;
2138
2343
  function watchedRoots(ctx) {
2139
2344
  return [
2140
- ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
2141
- ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
2345
+ ctx.codexSessionsDir ?? join5(homedir4(), ".codex", "sessions"),
2346
+ ctx.claudeProjectsDir ?? join5(homedir4(), ".claude", "projects")
2142
2347
  ];
2143
2348
  }
2144
2349
  async function scanSourceLogs(roots) {
@@ -2148,11 +2353,11 @@ async function scanSourceLogs(roots) {
2148
2353
  try {
2149
2354
  entries = await readdir2(dir, { withFileTypes: true });
2150
2355
  } catch (error) {
2151
- if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
2356
+ if (findErrorCode7(error, "ENOENT") || findErrorCode7(error, "ENOTDIR")) return;
2152
2357
  throw new Error("Failed to read a source log directory", { cause: error });
2153
2358
  }
2154
2359
  for (const entry of entries) {
2155
- const full = join4(dir, entry.name);
2360
+ const full = join5(dir, entry.name);
2156
2361
  if (entry.isDirectory()) {
2157
2362
  await walk(full);
2158
2363
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -2160,7 +2365,7 @@ async function scanSourceLogs(roots) {
2160
2365
  const info = await stat2(full);
2161
2366
  out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
2162
2367
  } catch (error) {
2163
- if (findErrorCode6(error, "ENOENT")) continue;
2368
+ if (findErrorCode7(error, "ENOENT")) continue;
2164
2369
  throw new Error("Failed to stat a source log file", { cause: error });
2165
2370
  }
2166
2371
  }
@@ -2259,19 +2464,19 @@ function parseInterval(value) {
2259
2464
  return seconds;
2260
2465
  }
2261
2466
  function abortableSleep(ms, signal) {
2262
- return new Promise((resolve4) => {
2467
+ return new Promise((resolve7) => {
2263
2468
  if (signal.aborted) {
2264
- resolve4();
2469
+ resolve7();
2265
2470
  return;
2266
2471
  }
2267
2472
  let timer;
2268
2473
  const onAbort = () => {
2269
2474
  clearTimeout(timer);
2270
- resolve4();
2475
+ resolve7();
2271
2476
  };
2272
2477
  timer = setTimeout(() => {
2273
2478
  signal.removeEventListener("abort", onAbort);
2274
- resolve4();
2479
+ resolve7();
2275
2480
  }, ms);
2276
2481
  signal.addEventListener("abort", onAbort, { once: true });
2277
2482
  });
@@ -2285,6 +2490,9 @@ function registerRefreshCommand(program2) {
2285
2490
  collectPath2,
2286
2491
  []
2287
2492
  ).option("--force", "Re-import sessions already imported instead of skipping").option("--dry-run", "Preview imports and skip writing handoff / decisions").option("--json", "Output the result as JSON").option(
2493
+ "--portfolio",
2494
+ "Refresh every workspace listed in ~/.basou/portfolio.yaml (each with its own source roots)"
2495
+ ).option(
2288
2496
  "--watch",
2289
2497
  "Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
2290
2498
  ).option(
@@ -2297,7 +2505,9 @@ function registerRefreshCommand(program2) {
2297
2505
  }
2298
2506
  async function runRefresh(options, ctx = {}) {
2299
2507
  try {
2300
- if (options.watch === true) {
2508
+ if (options.portfolio === true) {
2509
+ await doRunRefreshPortfolio(options, ctx);
2510
+ } else if (options.watch === true) {
2301
2511
  await doRunRefreshWatch(options, ctx);
2302
2512
  } else {
2303
2513
  await doRunRefresh(options, ctx);
@@ -2307,14 +2517,58 @@ async function runRefresh(options, ctx = {}) {
2307
2517
  process.exitCode = 1;
2308
2518
  }
2309
2519
  }
2520
+ async function doRunRefreshPortfolio(options, ctx) {
2521
+ if (options.watch === true) throw new Error("--portfolio cannot be combined with --watch.");
2522
+ if (options.project !== void 0 && options.project.length > 0) {
2523
+ throw new Error(
2524
+ "--portfolio refreshes each workspace with its own source roots; remove --project."
2525
+ );
2526
+ }
2527
+ const workspaces = await loadPortfolioConfig(ctx.portfolioConfigPath);
2528
+ const rollup = [];
2529
+ for (const ws of workspaces) {
2530
+ const label = ws.label ?? ws.path;
2531
+ try {
2532
+ const result = await computeRefresh(
2533
+ { ...options, portfolio: false },
2534
+ { ...ctx, cwd: ws.path }
2535
+ );
2536
+ rollup.push({ label, path: ws.path, status: "ok", result });
2537
+ if (options.json !== true) {
2538
+ console.log(`
2539
+ ## ${label} (${ws.path})`);
2540
+ printRefreshSummary(result);
2541
+ }
2542
+ } catch (error) {
2543
+ const message = error instanceof Error ? error.message : String(error);
2544
+ rollup.push({ label, path: ws.path, status: "failed", error: message });
2545
+ if (options.json !== true) {
2546
+ console.log(`
2547
+ ## ${label} (${ws.path})`);
2548
+ console.log(` failed: ${message}`);
2549
+ }
2550
+ }
2551
+ }
2552
+ if (options.json === true) {
2553
+ console.log(JSON.stringify({ portfolio: true, workspaces: rollup }));
2554
+ } else {
2555
+ const failed = rollup.filter((r) => r.status === "failed").length;
2556
+ const ok = rollup.length - failed;
2557
+ console.log(
2558
+ `
2559
+ portfolio: ${ok}/${rollup.length} refreshed${failed > 0 ? `, ${failed} failed` : ""}.`
2560
+ );
2561
+ }
2562
+ if (rollup.some((r) => r.status === "failed")) process.exitCode = 1;
2563
+ }
2310
2564
  async function doRunRefreshWatch(options, ctx) {
2311
2565
  if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
2312
2566
  if (options.json === true) throw new Error("--watch cannot be combined with --json.");
2313
2567
  if (options.force === true) throw new Error("--watch cannot be combined with --force.");
2314
2568
  const cwd = ctx.cwd ?? process.cwd();
2315
- const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
2316
- const paths = basouPaths7(repositoryRoot);
2317
- await assertWorkspaceInitialized6(paths.root);
2569
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
2570
+ const paths = basouPaths8(repositoryRoot);
2571
+ await assertWorkspaceInitialized7(paths.root);
2318
2572
  const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
2319
2573
  const controller = new AbortController();
2320
2574
  const onSignal = () => controller.abort();
@@ -2322,7 +2576,9 @@ async function doRunRefreshWatch(options, ctx) {
2322
2576
  process.on("SIGTERM", onSignal);
2323
2577
  try {
2324
2578
  await runRefreshWatch({
2325
- ctx,
2579
+ // Watch from a workspace view: import from the resolved planning repo, not
2580
+ // the raw (non-git) view cwd — mirrors the redirect in computeRefresh.
2581
+ ctx: { ...ctx, cwd: repositoryRoot },
2326
2582
  paths,
2327
2583
  intervalMs,
2328
2584
  importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
@@ -2336,22 +2592,27 @@ async function doRunRefreshWatch(options, ctx) {
2336
2592
  process.off("SIGTERM", onSignal);
2337
2593
  }
2338
2594
  }
2339
- async function doRunRefresh(options, ctx) {
2595
+ async function computeRefresh(options, ctx) {
2340
2596
  const cwd = ctx.cwd ?? process.cwd();
2341
- const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
2342
- const paths = basouPaths7(repositoryRoot);
2343
- await assertWorkspaceInitialized6(paths.root);
2597
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
2598
+ const paths = basouPaths8(repositoryRoot);
2599
+ await assertWorkspaceInitialized7(paths.root);
2344
2600
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2345
- const result = await refreshAll({
2601
+ return refreshAll({
2346
2602
  options: {
2347
2603
  ...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
2348
2604
  ...options.force === true ? { force: true } : {},
2349
2605
  ...options.dryRun === true ? { dryRun: true } : {}
2350
2606
  },
2351
- ctx,
2607
+ // Import from the resolved repo root, not the raw cwd: a workspace-view cwd
2608
+ // redirects to its planning repo, and the import must run there too.
2609
+ ctx: { ...ctx, cwd: repositoryRoot },
2352
2610
  paths,
2353
2611
  nowIso
2354
2612
  });
2613
+ }
2614
+ async function doRunRefresh(options, ctx) {
2615
+ const result = await computeRefresh(options, ctx);
2355
2616
  if (options.json === true) {
2356
2617
  console.log(JSON.stringify(result));
2357
2618
  } else {
@@ -2387,24 +2648,19 @@ function printRefreshSummary(result) {
2387
2648
  } else {
2388
2649
  console.log(`decisions: skipped (${result.decisions.reason})`);
2389
2650
  }
2390
- }
2391
- async function resolveRepositoryRootForRefresh(cwd) {
2392
- try {
2393
- return await resolveRepositoryRoot8(cwd);
2394
- } catch (error) {
2395
- if (error instanceof Error && error.message === "Not a git repository") {
2396
- throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou refresh'.", {
2397
- cause: error
2398
- });
2399
- }
2400
- throw error;
2651
+ if (result.orientation.status === "generated") {
2652
+ console.log(
2653
+ `orientation: regenerated (in-flight: ${result.orientation.inFlightTaskCount}, pending approvals: ${result.orientation.pendingApprovalsCount}, suspect: ${result.orientation.suspectCount})`
2654
+ );
2655
+ } else {
2656
+ console.log(`orientation: skipped (${result.orientation.reason})`);
2401
2657
  }
2402
2658
  }
2403
- async function assertWorkspaceInitialized6(basouRoot) {
2659
+ async function assertWorkspaceInitialized7(basouRoot) {
2404
2660
  try {
2405
- await assertBasouRootSafe7(basouRoot);
2661
+ await assertBasouRootSafe8(basouRoot);
2406
2662
  } catch (error) {
2407
- if (findErrorCode7(error, "ENOENT")) {
2663
+ if (findErrorCode8(error, "ENOENT")) {
2408
2664
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2409
2665
  }
2410
2666
  throw error;
@@ -2412,14 +2668,14 @@ async function assertWorkspaceInitialized6(basouRoot) {
2412
2668
  }
2413
2669
 
2414
2670
  // src/commands/report.ts
2415
- import { isAbsolute, resolve as resolve3 } from "path";
2671
+ import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
2416
2672
  import {
2417
- assertBasouRootSafe as assertBasouRootSafe8,
2418
- basouPaths as basouPaths8,
2419
- findErrorCode as findErrorCode8,
2673
+ assertBasouRootSafe as assertBasouRootSafe9,
2674
+ basouPaths as basouPaths9,
2675
+ findErrorCode as findErrorCode9,
2420
2676
  renderReport,
2421
- resolveRepositoryRoot as resolveRepositoryRoot9,
2422
- writeMarkdownFile as writeMarkdownFile4
2677
+ resolveRepositoryRoot as resolveRepositoryRoot8,
2678
+ writeMarkdownFile as writeMarkdownFile5
2423
2679
  } from "@basou/core";
2424
2680
  function registerReportCommand(program2) {
2425
2681
  const report = program2.command("report").description(
@@ -2440,8 +2696,8 @@ async function runReportGenerate(options, ctx = {}) {
2440
2696
  async function doRunReportGenerate(options, ctx) {
2441
2697
  const cwd = ctx.cwd ?? process.cwd();
2442
2698
  const repositoryRoot = await resolveRepositoryRootForReport(cwd);
2443
- const paths = basouPaths8(repositoryRoot);
2444
- await assertWorkspaceInitialized7(paths.root);
2699
+ const paths = basouPaths9(repositoryRoot);
2700
+ await assertWorkspaceInitialized8(paths.root);
2445
2701
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2446
2702
  const result = await renderReport({
2447
2703
  paths,
@@ -2452,8 +2708,8 @@ async function doRunReportGenerate(options, ctx) {
2452
2708
  onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2453
2709
  });
2454
2710
  if (options.out !== void 0) {
2455
- const outPath = isAbsolute(options.out) ? options.out : resolve3(cwd, options.out);
2456
- await writeMarkdownFile4(outPath, result.body);
2711
+ const outPath = isAbsolute2(options.out) ? options.out : resolve4(cwd, options.out);
2712
+ await writeMarkdownFile5(outPath, result.body);
2457
2713
  const { sessions, decisions, tasks } = result.data;
2458
2714
  console.error(
2459
2715
  `Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
@@ -2467,7 +2723,7 @@ async function doRunReportGenerate(options, ctx) {
2467
2723
  }
2468
2724
  async function resolveRepositoryRootForReport(cwd) {
2469
2725
  try {
2470
- return await resolveRepositoryRoot9(cwd);
2726
+ return await resolveRepositoryRoot8(cwd);
2471
2727
  } catch (error) {
2472
2728
  if (error instanceof Error && error.message === "Not a git repository") {
2473
2729
  throw new Error(
@@ -2478,11 +2734,11 @@ async function resolveRepositoryRootForReport(cwd) {
2478
2734
  throw error;
2479
2735
  }
2480
2736
  }
2481
- async function assertWorkspaceInitialized7(basouRoot) {
2737
+ async function assertWorkspaceInitialized8(basouRoot) {
2482
2738
  try {
2483
- await assertBasouRootSafe8(basouRoot);
2739
+ await assertBasouRootSafe9(basouRoot);
2484
2740
  } catch (error) {
2485
- if (findErrorCode8(error, "ENOENT")) {
2741
+ if (findErrorCode9(error, "ENOENT")) {
2486
2742
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2487
2743
  }
2488
2744
  throw error;
@@ -2491,12 +2747,12 @@ async function assertWorkspaceInitialized7(basouRoot) {
2491
2747
 
2492
2748
  // src/commands/run.ts
2493
2749
  import { mkdir as mkdir2 } from "fs/promises";
2494
- import { homedir as homedir4 } from "os";
2495
- import { join as join5 } from "path";
2750
+ import { homedir as homedir5 } from "os";
2751
+ import { join as join6 } from "path";
2496
2752
  import {
2497
2753
  acquireLock as acquireLock4,
2498
- assertBasouRootSafe as assertBasouRootSafe9,
2499
- basouPaths as basouPaths9,
2754
+ assertBasouRootSafe as assertBasouRootSafe10,
2755
+ basouPaths as basouPaths10,
2500
2756
  ChildProcessRunner as ChildProcessRunner2,
2501
2757
  claudeCodeAdapterMetadata,
2502
2758
  appendChainedEvent as coreAppendChainedEvent2,
@@ -2506,9 +2762,9 @@ import {
2506
2762
  overwriteYamlFile as overwriteYamlFile2,
2507
2763
  prefixedUlid as prefixedUlid4,
2508
2764
  readManifest as readManifest4,
2509
- readYamlFile as readYamlFile3,
2765
+ readYamlFile as readYamlFile4,
2510
2766
  resolveClaudeCodeCommand,
2511
- resolveRepositoryRoot as resolveRepositoryRoot10,
2767
+ resolveRepositoryRoot as resolveRepositoryRoot9,
2512
2768
  SessionSchema as SessionSchema2,
2513
2769
  sanitizeRelatedFiles,
2514
2770
  sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
@@ -2541,17 +2797,17 @@ async function runClaudeCode(args, options, ctx = {}) {
2541
2797
  const { command } = await resolveCommand();
2542
2798
  const cwd = options.cwd ?? process.cwd();
2543
2799
  const repoRoot = await resolveRepositoryRootForRun(cwd);
2544
- const paths = basouPaths9(repoRoot);
2545
- await assertBasouRootSafe9(paths.root);
2800
+ const paths = basouPaths10(repoRoot);
2801
+ await assertBasouRootSafe10(paths.root);
2546
2802
  const manifest = await readManifest4(paths);
2547
2803
  const sessionId = prefixedUlid4("ses");
2548
- const sessionDir = join5(paths.sessions, sessionId);
2804
+ const sessionDir = join6(paths.sessions, sessionId);
2549
2805
  await mkdir2(sessionDir, { recursive: true });
2550
2806
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
2551
2807
  await coreAppendChainedEvent2(paths, sessionId, event);
2552
2808
  });
2553
2809
  const startedAt = now().toISOString();
2554
- const sessionYamlPath = join5(sessionDir, "session.yaml");
2810
+ const sessionYamlPath = join6(sessionDir, "session.yaml");
2555
2811
  const session = buildInitialSession2({
2556
2812
  id: sessionId,
2557
2813
  command,
@@ -2677,7 +2933,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2677
2933
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
2678
2934
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
2679
2935
  workingDirectory: repoRoot,
2680
- homedir: homedir4()
2936
+ homedir: homedir5()
2681
2937
  }).sanitized;
2682
2938
  const finalStatus = decideFinalStatus2(result, signalReceived);
2683
2939
  await appendEvent(sessionDir, {
@@ -2821,7 +3077,7 @@ function buildInitialSession2(input) {
2821
3077
  source: { ...claudeCodeAdapterMetadata },
2822
3078
  started_at: input.startedAt,
2823
3079
  status: "initialized",
2824
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
3080
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir5() }),
2825
3081
  invocation: {
2826
3082
  command: input.command,
2827
3083
  args: [...input.args],
@@ -2833,7 +3089,7 @@ function buildInitialSession2(input) {
2833
3089
  };
2834
3090
  }
2835
3091
  async function mutateSessionYaml2(filePath, mutator) {
2836
- const raw = await readYamlFile3(filePath);
3092
+ const raw = await readYamlFile4(filePath);
2837
3093
  const parsed = SessionSchema2.parse(raw);
2838
3094
  mutator(parsed);
2839
3095
  const validated = SessionSchema2.parse(parsed);
@@ -2881,7 +3137,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
2881
3137
  }
2882
3138
  async function resolveRepositoryRootForRun(cwd) {
2883
3139
  try {
2884
- return await resolveRepositoryRoot10(cwd);
3140
+ return await resolveRepositoryRoot9(cwd);
2885
3141
  } catch (error) {
2886
3142
  if (error instanceof Error && error.message === "Not a git repository") {
2887
3143
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -2894,21 +3150,21 @@ async function resolveRepositoryRootForRun(cwd) {
2894
3150
 
2895
3151
  // src/commands/session.ts
2896
3152
  import { readFile as readFile2 } from "fs/promises";
2897
- import { basename as basename3, isAbsolute as isAbsolute2, join as join6, relative as relative2 } from "path";
3153
+ import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2 } from "path";
2898
3154
  import {
2899
3155
  acquireLock as acquireLock5,
2900
3156
  appendEventToExistingSession as appendEventToExistingSession2,
2901
- assertBasouRootSafe as assertBasouRootSafe10,
2902
- basouPaths as basouPaths10,
3157
+ assertBasouRootSafe as assertBasouRootSafe11,
3158
+ basouPaths as basouPaths11,
2903
3159
  enumerateSessionDirs as enumerateSessionDirs2,
2904
- findErrorCode as findErrorCode9,
3160
+ findErrorCode as findErrorCode10,
2905
3161
  importSessionFromJson as importSessionFromJson2,
2906
3162
  loadSessionEntries,
2907
3163
  readAllEvents,
2908
3164
  readManifest as readManifest5,
2909
- readYamlFile as readYamlFile4,
3165
+ readYamlFile as readYamlFile5,
2910
3166
  rechainSessionInPlace,
2911
- resolveRepositoryRoot as resolveRepositoryRoot11,
3167
+ resolveRepositoryRoot as resolveRepositoryRoot10,
2912
3168
  resolveSessionId as resolveSessionId2,
2913
3169
  resolveTaskId,
2914
3170
  SessionImportPayloadSchema as SessionImportPayloadSchema2,
@@ -2965,8 +3221,8 @@ async function runSessionList(options, ctx = {}) {
2965
3221
  async function doRunSessionList(options, ctx) {
2966
3222
  const cwd = ctx.cwd ?? process.cwd();
2967
3223
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
2968
- const paths = basouPaths10(repositoryRoot);
2969
- await assertWorkspaceInitialized8(paths.root);
3224
+ const paths = basouPaths11(repositoryRoot);
3225
+ await assertWorkspaceInitialized9(paths.root);
2970
3226
  const now = /* @__PURE__ */ new Date();
2971
3227
  const records = (await loadSessionEntries(paths, {
2972
3228
  now,
@@ -3017,17 +3273,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
3017
3273
  async function doRunSessionShow(idInput, options, ctx) {
3018
3274
  const cwd = ctx.cwd ?? process.cwd();
3019
3275
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
3020
- const paths = basouPaths10(repositoryRoot);
3021
- await assertWorkspaceInitialized8(paths.root);
3276
+ const paths = basouPaths11(repositoryRoot);
3277
+ await assertWorkspaceInitialized9(paths.root);
3022
3278
  const sessionId = await resolveSessionId2(paths, idInput);
3023
- const sessionDir = join6(paths.sessions, sessionId);
3024
- const sessionYamlPath = join6(sessionDir, "session.yaml");
3279
+ const sessionDir = join7(paths.sessions, sessionId);
3280
+ const sessionYamlPath = join7(sessionDir, "session.yaml");
3025
3281
  let session;
3026
3282
  try {
3027
- const raw = await readYamlFile4(sessionYamlPath);
3283
+ const raw = await readYamlFile5(sessionYamlPath);
3028
3284
  session = SessionSchema3.parse(raw);
3029
3285
  } catch (error) {
3030
- if (findErrorCode9(error, "ENOENT")) {
3286
+ if (findErrorCode10(error, "ENOENT")) {
3031
3287
  throw new Error(`Session not found: ${idInput}`);
3032
3288
  }
3033
3289
  throw new Error("Failed to read session", { cause: error });
@@ -3142,7 +3398,7 @@ function formatSessionWork(session, events, now) {
3142
3398
  }
3143
3399
  function formatWorkingDir(workingDir, repositoryRoot, options) {
3144
3400
  if (options.fullPath === true) return workingDir;
3145
- if (!isAbsolute2(workingDir)) {
3401
+ if (!isAbsolute3(workingDir)) {
3146
3402
  if (workingDir === ".") return "<repository_root>";
3147
3403
  return workingDir;
3148
3404
  }
@@ -3262,7 +3518,7 @@ function maxLen2(values, floor) {
3262
3518
  }
3263
3519
  async function resolveRepositoryRootForSession(cwd, subcmd) {
3264
3520
  try {
3265
- return await resolveRepositoryRoot11(cwd);
3521
+ return await resolveRepositoryRoot10(cwd);
3266
3522
  } catch (error) {
3267
3523
  if (error instanceof Error && error.message === "Not a git repository") {
3268
3524
  throw new Error(
@@ -3273,11 +3529,11 @@ async function resolveRepositoryRootForSession(cwd, subcmd) {
3273
3529
  throw error;
3274
3530
  }
3275
3531
  }
3276
- async function assertWorkspaceInitialized8(basouRoot) {
3532
+ async function assertWorkspaceInitialized9(basouRoot) {
3277
3533
  try {
3278
- await assertBasouRootSafe10(basouRoot);
3534
+ await assertBasouRootSafe11(basouRoot);
3279
3535
  } catch (error) {
3280
- if (findErrorCode9(error, "ENOENT")) {
3536
+ if (findErrorCode10(error, "ENOENT")) {
3281
3537
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3282
3538
  }
3283
3539
  throw error;
@@ -3315,8 +3571,8 @@ async function runSessionImport(options, ctx = {}) {
3315
3571
  async function doRunSessionImport(options, ctx) {
3316
3572
  const cwd = ctx.cwd ?? process.cwd();
3317
3573
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
3318
- const paths = basouPaths10(repositoryRoot);
3319
- await assertWorkspaceInitialized8(paths.root);
3574
+ const paths = basouPaths11(repositoryRoot);
3575
+ await assertWorkspaceInitialized9(paths.root);
3320
3576
  const manifest = await readManifest5(paths);
3321
3577
  const rawBody = await readInputFile(options.from);
3322
3578
  const json = parseJsonStrict(rawBody);
@@ -3346,10 +3602,10 @@ async function readInputFile(path) {
3346
3602
  try {
3347
3603
  return await readFile2(path, "utf8");
3348
3604
  } catch (error) {
3349
- if (findErrorCode9(error, "ENOENT")) {
3605
+ if (findErrorCode10(error, "ENOENT")) {
3350
3606
  throw new Error("Import source not found", { cause: error });
3351
3607
  }
3352
- if (findErrorCode9(error, "EISDIR")) {
3608
+ if (findErrorCode10(error, "EISDIR")) {
3353
3609
  throw new Error("Import source is not a file", { cause: error });
3354
3610
  }
3355
3611
  throw new Error("Failed to read import source", { cause: error });
@@ -3429,8 +3685,8 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
3429
3685
  }
3430
3686
  const cwd = ctx.cwd ?? process.cwd();
3431
3687
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
3432
- const paths = basouPaths10(repositoryRoot);
3433
- await assertWorkspaceInitialized8(paths.root);
3688
+ const paths = basouPaths11(repositoryRoot);
3689
+ await assertWorkspaceInitialized9(paths.root);
3434
3690
  const sessionId = await resolveSessionId2(paths, sessionIdInput);
3435
3691
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
3436
3692
  if (body.length === 0) {
@@ -3463,10 +3719,10 @@ async function readNoteFile(path) {
3463
3719
  try {
3464
3720
  return await readFile2(path, "utf8");
3465
3721
  } catch (error) {
3466
- if (findErrorCode9(error, "ENOENT")) {
3722
+ if (findErrorCode10(error, "ENOENT")) {
3467
3723
  throw new Error("Note source not found", { cause: error });
3468
3724
  }
3469
- if (findErrorCode9(error, "EISDIR")) {
3725
+ if (findErrorCode10(error, "EISDIR")) {
3470
3726
  throw new Error("Note source is not a file", { cause: error });
3471
3727
  }
3472
3728
  throw new Error("Failed to read note source", { cause: error });
@@ -3511,8 +3767,8 @@ async function doRunSessionRechain(options, ctx) {
3511
3767
  }
3512
3768
  const cwd = ctx.cwd ?? process.cwd();
3513
3769
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
3514
- const paths = basouPaths10(repositoryRoot);
3515
- await assertWorkspaceInitialized8(paths.root);
3770
+ const paths = basouPaths11(repositoryRoot);
3771
+ await assertWorkspaceInitialized9(paths.root);
3516
3772
  const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
3517
3773
  const dryRun = options.dryRun === true;
3518
3774
  const rows = [];
@@ -3565,11 +3821,11 @@ function renderRechainRow(row, dryRun) {
3565
3821
 
3566
3822
  // src/commands/stats.ts
3567
3823
  import {
3568
- assertBasouRootSafe as assertBasouRootSafe11,
3569
- basouPaths as basouPaths11,
3824
+ assertBasouRootSafe as assertBasouRootSafe12,
3825
+ basouPaths as basouPaths12,
3570
3826
  computeWorkStats,
3571
- findErrorCode as findErrorCode10,
3572
- resolveRepositoryRoot as resolveRepositoryRoot12
3827
+ findErrorCode as findErrorCode11,
3828
+ resolveRepositoryRoot as resolveRepositoryRoot11
3573
3829
  } from "@basou/core";
3574
3830
  function registerStatsCommand(program2) {
3575
3831
  program2.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
@@ -3587,8 +3843,8 @@ async function runStats(options, ctx = {}) {
3587
3843
  async function doRunStats(options, ctx) {
3588
3844
  const cwd = ctx.cwd ?? process.cwd();
3589
3845
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
3590
- const paths = basouPaths11(repositoryRoot);
3591
- await assertWorkspaceInitialized9(paths.root);
3846
+ const paths = basouPaths12(repositoryRoot);
3847
+ await assertWorkspaceInitialized10(paths.root);
3592
3848
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
3593
3849
  const result = await computeWorkStats({
3594
3850
  paths,
@@ -3672,7 +3928,7 @@ function formatInt(n) {
3672
3928
  }
3673
3929
  async function resolveRepositoryRootForStats(cwd) {
3674
3930
  try {
3675
- return await resolveRepositoryRoot12(cwd);
3931
+ return await resolveRepositoryRoot11(cwd);
3676
3932
  } catch (error) {
3677
3933
  if (error instanceof Error && error.message === "Not a git repository") {
3678
3934
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -3682,11 +3938,11 @@ async function resolveRepositoryRootForStats(cwd) {
3682
3938
  throw error;
3683
3939
  }
3684
3940
  }
3685
- async function assertWorkspaceInitialized9(basouRoot) {
3941
+ async function assertWorkspaceInitialized10(basouRoot) {
3686
3942
  try {
3687
- await assertBasouRootSafe11(basouRoot);
3943
+ await assertBasouRootSafe12(basouRoot);
3688
3944
  } catch (error) {
3689
- if (findErrorCode10(error, "ENOENT")) {
3945
+ if (findErrorCode11(error, "ENOENT")) {
3690
3946
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3691
3947
  }
3692
3948
  throw error;
@@ -3695,12 +3951,12 @@ async function assertWorkspaceInitialized9(basouRoot) {
3695
3951
 
3696
3952
  // src/commands/status.ts
3697
3953
  import {
3698
- assertBasouRootSafe as assertBasouRootSafe12,
3699
- basouPaths as basouPaths12,
3954
+ assertBasouRootSafe as assertBasouRootSafe13,
3955
+ basouPaths as basouPaths13,
3700
3956
  buildStatusSnapshot,
3701
- findErrorCode as findErrorCode11,
3957
+ findErrorCode as findErrorCode12,
3702
3958
  readManifest as readManifest6,
3703
- resolveRepositoryRoot as resolveRepositoryRoot13,
3959
+ resolveRepositoryRoot as resolveRepositoryRoot12,
3704
3960
  writeStatus
3705
3961
  } from "@basou/core";
3706
3962
  function registerStatusCommand(program2) {
@@ -3719,11 +3975,11 @@ async function runStatus(options, ctx = {}) {
3719
3975
  async function doRunStatus(options, ctx) {
3720
3976
  const cwd = ctx.cwd ?? process.cwd();
3721
3977
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
3722
- const paths = basouPaths12(repositoryRoot);
3978
+ const paths = basouPaths13(repositoryRoot);
3723
3979
  try {
3724
- await assertBasouRootSafe12(paths.root);
3980
+ await assertBasouRootSafe13(paths.root);
3725
3981
  } catch (error) {
3726
- if (findErrorCode11(error, "ENOENT")) {
3982
+ if (findErrorCode12(error, "ENOENT")) {
3727
3983
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3728
3984
  }
3729
3985
  throw error;
@@ -3732,7 +3988,7 @@ async function doRunStatus(options, ctx) {
3732
3988
  try {
3733
3989
  manifest = await readManifest6(paths);
3734
3990
  } catch (error) {
3735
- if (findErrorCode11(error, "ENOENT")) {
3991
+ if (findErrorCode12(error, "ENOENT")) {
3736
3992
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3737
3993
  }
3738
3994
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -3756,7 +4012,7 @@ function renderTextStatus(s) {
3756
4012
  }
3757
4013
  async function resolveRepositoryRootForStatus(cwd) {
3758
4014
  try {
3759
- return await resolveRepositoryRoot13(cwd);
4015
+ return await resolveRepositoryRoot12(cwd);
3760
4016
  } catch (error) {
3761
4017
  if (error instanceof Error && error.message === "Not a git repository") {
3762
4018
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -3769,16 +4025,16 @@ async function resolveRepositoryRootForStatus(cwd) {
3769
4025
 
3770
4026
  // src/commands/task.ts
3771
4027
  import { readFile as readFile3 } from "fs/promises";
3772
- import { join as join7 } from "path";
4028
+ import { join as join8 } from "path";
3773
4029
  import {
3774
4030
  archiveTask,
3775
- assertBasouRootSafe as assertBasouRootSafe13,
3776
- basouPaths as basouPaths13,
4031
+ assertBasouRootSafe as assertBasouRootSafe14,
4032
+ basouPaths as basouPaths14,
3777
4033
  createTaskWithEvent,
3778
4034
  deleteTask,
3779
4035
  editTask,
3780
4036
  enumerateArchivedTaskIds,
3781
- findErrorCode as findErrorCode12,
4037
+ findErrorCode as findErrorCode13,
3782
4038
  loadSessionEntries as loadSessionEntries2,
3783
4039
  loadTaskEntries,
3784
4040
  prefixedUlid as prefixedUlid5,
@@ -3789,7 +4045,7 @@ import {
3789
4045
  reconcileTask,
3790
4046
  refreshTaskLinkedSessions,
3791
4047
  replayEvents as replayEvents2,
3792
- resolveRepositoryRoot as resolveRepositoryRoot14,
4048
+ resolveRepositoryRoot as resolveRepositoryRoot13,
3793
4049
  resolveSessionId as resolveSessionId3,
3794
4050
  resolveTaskId as resolveTaskId2,
3795
4051
  TaskStatusSchema,
@@ -3875,8 +4131,8 @@ async function doRunTaskNew(options, ctx) {
3875
4131
  }
3876
4132
  const cwd = ctx.cwd ?? process.cwd();
3877
4133
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
3878
- const paths = basouPaths13(repositoryRoot);
3879
- await assertWorkspaceInitialized10(paths.root);
4134
+ const paths = basouPaths14(repositoryRoot);
4135
+ await assertWorkspaceInitialized11(paths.root);
3880
4136
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
3881
4137
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
3882
4138
  const occurredAt = now.toISOString();
@@ -3984,8 +4240,8 @@ async function runTaskList(options, ctx = {}) {
3984
4240
  async function doRunTaskList(options, ctx) {
3985
4241
  const cwd = ctx.cwd ?? process.cwd();
3986
4242
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
3987
- const paths = basouPaths13(repositoryRoot);
3988
- await assertWorkspaceInitialized10(paths.root);
4243
+ const paths = basouPaths14(repositoryRoot);
4244
+ await assertWorkspaceInitialized11(paths.root);
3989
4245
  const entries = await loadTaskEntries(paths, {
3990
4246
  onSkip: (id, reason) => printTaskSkip(id, reason)
3991
4247
  });
@@ -4088,15 +4344,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
4088
4344
  async function doRunTaskShow(idInput, options, ctx) {
4089
4345
  const cwd = ctx.cwd ?? process.cwd();
4090
4346
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
4091
- const paths = basouPaths13(repositoryRoot);
4092
- await assertWorkspaceInitialized10(paths.root);
4347
+ const paths = basouPaths14(repositoryRoot);
4348
+ await assertWorkspaceInitialized11(paths.root);
4093
4349
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
4094
4350
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
4095
4351
  const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
4096
4352
  const events = [];
4097
4353
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
4098
4354
  for (const s of sessions) {
4099
- const sessionDir = join7(paths.sessions, s.sessionId);
4355
+ const sessionDir = join8(paths.sessions, s.sessionId);
4100
4356
  try {
4101
4357
  for await (const ev of replayEvents2(sessionDir, {
4102
4358
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -4232,8 +4488,8 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
4232
4488
  const newStatus = parseTaskStatusPositional(newStatusInput);
4233
4489
  const cwd = ctx.cwd ?? process.cwd();
4234
4490
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
4235
- const paths = basouPaths13(repositoryRoot);
4236
- await assertWorkspaceInitialized10(paths.root);
4491
+ const paths = basouPaths14(repositoryRoot);
4492
+ await assertWorkspaceInitialized11(paths.root);
4237
4493
  const taskId = await resolveTaskId2(paths, taskIdInput);
4238
4494
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4239
4495
  const occurredAt = now.toISOString();
@@ -4309,8 +4565,8 @@ async function runTaskReconcile(options, ctx = {}) {
4309
4565
  async function doRunTaskReconcile(options, ctx) {
4310
4566
  const cwd = ctx.cwd ?? process.cwd();
4311
4567
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
4312
- const paths = basouPaths13(repositoryRoot);
4313
- await assertWorkspaceInitialized10(paths.root);
4568
+ const paths = basouPaths14(repositoryRoot);
4569
+ await assertWorkspaceInitialized11(paths.root);
4314
4570
  const manifest = await readManifest7(paths);
4315
4571
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
4316
4572
  const write = options.write === true;
@@ -4489,8 +4745,8 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
4489
4745
  }
4490
4746
  const cwd = ctx.cwd ?? process.cwd();
4491
4747
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
4492
- const paths = basouPaths13(repositoryRoot);
4493
- await assertWorkspaceInitialized10(paths.root);
4748
+ const paths = basouPaths14(repositoryRoot);
4749
+ await assertWorkspaceInitialized11(paths.root);
4494
4750
  const manifest = await readManifest7(paths);
4495
4751
  const taskId = await resolveTaskId2(paths, taskIdInput);
4496
4752
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
@@ -4569,8 +4825,8 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
4569
4825
  }
4570
4826
  const cwd = ctx.cwd ?? process.cwd();
4571
4827
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
4572
- const paths = basouPaths13(repositoryRoot);
4573
- await assertWorkspaceInitialized10(paths.root);
4828
+ const paths = basouPaths14(repositoryRoot);
4829
+ await assertWorkspaceInitialized11(paths.root);
4574
4830
  const manifest = await readManifest7(paths);
4575
4831
  const taskId = await resolveTaskId2(paths, taskIdInput);
4576
4832
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
@@ -4625,8 +4881,8 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
4625
4881
  }
4626
4882
  const cwd = ctx.cwd ?? process.cwd();
4627
4883
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
4628
- const paths = basouPaths13(repositoryRoot);
4629
- await assertWorkspaceInitialized10(paths.root);
4884
+ const paths = basouPaths14(repositoryRoot);
4885
+ await assertWorkspaceInitialized11(paths.root);
4630
4886
  const manifest = await readManifest7(paths);
4631
4887
  const taskId = await resolveTaskId2(paths, taskIdInput);
4632
4888
  if (options.yes !== true) {
@@ -4670,8 +4926,8 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
4670
4926
  }
4671
4927
  const cwd = ctx.cwd ?? process.cwd();
4672
4928
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
4673
- const paths = basouPaths13(repositoryRoot);
4674
- await assertWorkspaceInitialized10(paths.root);
4929
+ const paths = basouPaths14(repositoryRoot);
4930
+ await assertWorkspaceInitialized11(paths.root);
4675
4931
  const manifest = await readManifest7(paths);
4676
4932
  const taskId = await resolveTaskId2(paths, taskIdInput);
4677
4933
  if (options.yes !== true) {
@@ -4786,10 +5042,10 @@ async function readDescriptionFile(path) {
4786
5042
  try {
4787
5043
  return await readFile3(path, "utf8");
4788
5044
  } catch (error) {
4789
- if (findErrorCode12(error, "ENOENT")) {
5045
+ if (findErrorCode13(error, "ENOENT")) {
4790
5046
  throw new Error("Description source not found", { cause: error });
4791
5047
  }
4792
- if (findErrorCode12(error, "EISDIR")) {
5048
+ if (findErrorCode13(error, "EISDIR")) {
4793
5049
  throw new Error("Description source is not a file", { cause: error });
4794
5050
  }
4795
5051
  throw new Error("Failed to read description source", { cause: error });
@@ -4797,7 +5053,7 @@ async function readDescriptionFile(path) {
4797
5053
  }
4798
5054
  async function resolveRepositoryRootForTask(cwd, subcmd) {
4799
5055
  try {
4800
- return await resolveRepositoryRoot14(cwd);
5056
+ return await resolveRepositoryRoot13(cwd);
4801
5057
  } catch (error) {
4802
5058
  if (error instanceof Error && error.message === "Not a git repository") {
4803
5059
  throw new Error(
@@ -4808,11 +5064,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
4808
5064
  throw error;
4809
5065
  }
4810
5066
  }
4811
- async function assertWorkspaceInitialized10(basouRoot) {
5067
+ async function assertWorkspaceInitialized11(basouRoot) {
4812
5068
  try {
4813
- await assertBasouRootSafe13(basouRoot);
5069
+ await assertBasouRootSafe14(basouRoot);
4814
5070
  } catch (error) {
4815
- if (findErrorCode12(error, "ENOENT")) {
5071
+ if (findErrorCode13(error, "ENOENT")) {
4816
5072
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4817
5073
  }
4818
5074
  throw error;
@@ -4900,11 +5156,11 @@ function maxLen3(values, floor) {
4900
5156
 
4901
5157
  // src/commands/verify.ts
4902
5158
  import {
4903
- assertBasouRootSafe as assertBasouRootSafe14,
4904
- basouPaths as basouPaths14,
5159
+ assertBasouRootSafe as assertBasouRootSafe15,
5160
+ basouPaths as basouPaths15,
4905
5161
  enumerateSessionDirs as enumerateSessionDirs3,
4906
- findErrorCode as findErrorCode13,
4907
- resolveRepositoryRoot as resolveRepositoryRoot15,
5162
+ findErrorCode as findErrorCode14,
5163
+ resolveRepositoryRoot as resolveRepositoryRoot14,
4908
5164
  resolveSessionId as resolveSessionId4,
4909
5165
  verifyEventsChain
4910
5166
  } from "@basou/core";
@@ -4927,8 +5183,8 @@ async function doRunVerify(options, ctx) {
4927
5183
  }
4928
5184
  const cwd = ctx.cwd ?? process.cwd();
4929
5185
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
4930
- const paths = basouPaths14(repositoryRoot);
4931
- await assertWorkspaceInitialized11(paths.root);
5186
+ const paths = basouPaths15(repositoryRoot);
5187
+ await assertWorkspaceInitialized12(paths.root);
4932
5188
  const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
4933
5189
  const rows = [];
4934
5190
  for (const sessionId of sessionIds) {
@@ -4975,7 +5231,7 @@ function renderVerdict(row) {
4975
5231
  }
4976
5232
  async function resolveRepositoryRootForVerify(cwd) {
4977
5233
  try {
4978
- return await resolveRepositoryRoot15(cwd);
5234
+ return await resolveRepositoryRoot14(cwd);
4979
5235
  } catch (error) {
4980
5236
  if (error instanceof Error && error.message === "Not a git repository") {
4981
5237
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -4985,11 +5241,11 @@ async function resolveRepositoryRootForVerify(cwd) {
4985
5241
  throw error;
4986
5242
  }
4987
5243
  }
4988
- async function assertWorkspaceInitialized11(basouRoot) {
5244
+ async function assertWorkspaceInitialized12(basouRoot) {
4989
5245
  try {
4990
- await assertBasouRootSafe14(basouRoot);
5246
+ await assertBasouRootSafe15(basouRoot);
4991
5247
  } catch (error) {
4992
- if (findErrorCode13(error, "ENOENT")) {
5248
+ if (findErrorCode14(error, "ENOENT")) {
4993
5249
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4994
5250
  }
4995
5251
  throw error;
@@ -4998,27 +5254,162 @@ async function assertWorkspaceInitialized11(basouRoot) {
4998
5254
 
4999
5255
  // src/commands/view.ts
5000
5256
  import { spawn } from "child_process";
5001
- import { assertBasouRootSafe as assertBasouRootSafe15, basouPaths as basouPaths15, findErrorCode as findErrorCode15, resolveRepositoryRoot as resolveRepositoryRoot16 } from "@basou/core";
5257
+ import { createHash } from "crypto";
5258
+ import { basename as basename4, resolve as resolve6 } from "path";
5259
+ import {
5260
+ assertBasouRootSafe as assertBasouRootSafe16,
5261
+ basouPaths as basouPaths16,
5262
+ findErrorCode as findErrorCode16,
5263
+ readManifest as readManifest10,
5264
+ resolveRepositoryRoot as resolveRepositoryRoot15
5265
+ } from "@basou/core";
5002
5266
  import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
5003
5267
 
5268
+ // src/lib/portfolio-safety.ts
5269
+ import { execFile } from "child_process";
5270
+ import { lstat, realpath } from "fs/promises";
5271
+ import { isAbsolute as isAbsolute4, join as join9, relative as relative3, resolve as resolve5 } from "path";
5272
+ import { promisify } from "util";
5273
+ import { readManifest as readManifest8 } from "@basou/core";
5274
+ var execFileAsync = promisify(execFile);
5275
+ function errorCode(error) {
5276
+ return error instanceof Error ? error.code : void 0;
5277
+ }
5278
+ async function canonical(p) {
5279
+ try {
5280
+ return await realpath(p);
5281
+ } catch {
5282
+ return resolve5(p);
5283
+ }
5284
+ }
5285
+ function isInside(child, parent) {
5286
+ const rel = relative3(parent, child);
5287
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
5288
+ }
5289
+ function isBasouPath(p) {
5290
+ return p === ".basou" || p.startsWith(".basou/") || p.includes("/.basou/") || p.endsWith("/.basou");
5291
+ }
5292
+ async function inspectRepo(repoPath) {
5293
+ let hasEntry = false;
5294
+ try {
5295
+ await lstat(join9(repoPath, ".basou"));
5296
+ hasEntry = true;
5297
+ } catch (error) {
5298
+ if (errorCode(error) !== "ENOENT") {
5299
+ return {
5300
+ kind: "unverifiable",
5301
+ detail: `could not check for a .basou here (${errorCode(error) ?? "unknown error"}) \u2014 treat as unsafe`
5302
+ };
5303
+ }
5304
+ }
5305
+ try {
5306
+ const { stdout } = await execFileAsync("git", ["-C", repoPath, "ls-files", "-z"]);
5307
+ const tracked = stdout.split("\0").some((f) => f.length > 0 && isBasouPath(f));
5308
+ if (tracked) {
5309
+ return {
5310
+ kind: "footprint",
5311
+ detail: "a .basou/ entry is tracked by git here and would be pushed"
5312
+ };
5313
+ }
5314
+ } catch {
5315
+ }
5316
+ if (hasEntry) return { kind: "footprint", detail: "a .basou/ entry exists here" };
5317
+ return null;
5318
+ }
5319
+ async function checkPortfolioSafety(workspaces) {
5320
+ const findings = [];
5321
+ let monitoredReposChecked = 0;
5322
+ for (const ws of workspaces) {
5323
+ const wsReal = await canonical(ws.repoRoot);
5324
+ let sourceRoots = [];
5325
+ try {
5326
+ const manifest = await readManifest8(ws.paths);
5327
+ sourceRoots = manifest.import?.source_roots ?? [];
5328
+ } catch (error) {
5329
+ if (error instanceof Error && error.message === "YAML file not found") {
5330
+ sourceRoots = [];
5331
+ } else {
5332
+ findings.push({
5333
+ workspaceLabel: ws.label,
5334
+ workspaceRoot: ws.repoRoot,
5335
+ monitoredRepo: ws.repoRoot,
5336
+ kind: "unverifiable",
5337
+ detail: "the workspace manifest is present but unreadable \u2014 cannot determine which repos it monitors; treat as unsafe"
5338
+ });
5339
+ continue;
5340
+ }
5341
+ }
5342
+ const monitored = /* @__PURE__ */ new Map();
5343
+ for (const root of sourceRoots) {
5344
+ const display = resolve5(ws.repoRoot, root);
5345
+ const real = await canonical(display);
5346
+ if (real !== wsReal) monitored.set(real, display);
5347
+ }
5348
+ for (const [real, display] of monitored) {
5349
+ monitoredReposChecked++;
5350
+ if (isInside(wsReal, real)) {
5351
+ findings.push({
5352
+ workspaceLabel: ws.label,
5353
+ workspaceRoot: ws.repoRoot,
5354
+ monitoredRepo: display,
5355
+ kind: "overlap",
5356
+ detail: "the workspace (where .basou/ is written) is inside this monitored repo"
5357
+ });
5358
+ }
5359
+ const inspection = await inspectRepo(real);
5360
+ if (inspection !== null) {
5361
+ findings.push({
5362
+ workspaceLabel: ws.label,
5363
+ workspaceRoot: ws.repoRoot,
5364
+ monitoredRepo: display,
5365
+ kind: inspection.kind,
5366
+ detail: inspection.detail
5367
+ });
5368
+ }
5369
+ }
5370
+ }
5371
+ return { findings, workspacesChecked: workspaces.length, monitoredReposChecked };
5372
+ }
5373
+ function formatSafetyReport(result) {
5374
+ if (result.findings.length === 0) {
5375
+ if (result.monitoredReposChecked === 0) {
5376
+ return [
5377
+ `Portfolio safety: OK. ${result.workspacesChecked} workspace(s) checked \u2014 no monitored repos configured (portfolio safety applies when a workspace imports from sibling repos via source_roots).`
5378
+ ];
5379
+ }
5380
+ return [
5381
+ `Portfolio safety: OK. ${result.workspacesChecked} workspace(s), ${result.monitoredReposChecked} monitored repo(s) checked \u2014 no .basou footprint, no overlap.`
5382
+ ];
5383
+ }
5384
+ const lines = [`Portfolio safety: DANGER \u2014 ${result.findings.length} finding(s):`];
5385
+ for (const f of result.findings) {
5386
+ lines.push(` [${f.kind}] ${f.monitoredRepo} (workspace "${f.workspaceLabel}"): ${f.detail}`);
5387
+ }
5388
+ lines.push(
5389
+ "A monitored repo must have no basou footprint. Use a separate workspace repo whose source_roots point at the monitored repo as a sibling; never 'basou init' / 'run' / 'exec' inside a monitored repo."
5390
+ );
5391
+ return lines;
5392
+ }
5393
+
5004
5394
  // src/lib/view-server.ts
5005
5395
  import { createServer } from "http";
5006
- import { join as join8 } from "path";
5396
+ import { join as join10 } from "path";
5007
5397
  import {
5008
5398
  computeWorkStats as computeWorkStats2,
5009
5399
  enumerateApprovals as enumerateApprovals2,
5010
- findErrorCode as findErrorCode14,
5400
+ findErrorCode as findErrorCode15,
5011
5401
  isLazyExpired as isLazyExpired2,
5012
5402
  loadApproval as loadApproval2,
5013
5403
  loadSessionEntries as loadSessionEntries3,
5014
5404
  loadTaskEntries as loadTaskEntries2,
5015
5405
  readAllEvents as readAllEvents2,
5016
- readManifest as readManifest8,
5406
+ readManifest as readManifest9,
5017
5407
  readMarkdownFile as readMarkdownFile4,
5018
5408
  readSessionYaml as readSessionYaml3,
5019
5409
  readTaskFile as readTaskFile2,
5020
5410
  renderDecisions as renderDecisions3,
5021
- renderHandoff as renderHandoff3
5411
+ renderHandoff as renderHandoff3,
5412
+ summarizeOrientation
5022
5413
  } from "@basou/core";
5023
5414
 
5024
5415
  // src/lib/view-ui.ts
@@ -5040,8 +5431,13 @@ var VIEW_HTML = `<!doctype html>
5040
5431
  button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
5041
5432
  button:disabled { opacity: .5; cursor: default; }
5042
5433
  label.chk { font-size: 13px; opacity: .85; }
5434
+ /* On the portfolio landing there is no selected workspace, so the per-workspace action bar is hidden. */
5435
+ body.landing #project, body.landing label.chk,
5436
+ body.landing #btn-refresh, body.landing #btn-import-claude, body.landing #btn-import-codex,
5437
+ body.landing #btn-gen-handoff, body.landing #btn-gen-decisions { display: none; }
5043
5438
  #status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
5044
5439
  #status.err { color: #dc2626; }
5440
+ .err { color: #dc2626; }
5045
5441
  nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
5046
5442
  nav button { border: none; border-radius: 6px; background: transparent; }
5047
5443
  nav button.active { background: #2563eb22; font-weight: 600; }
@@ -5055,6 +5451,8 @@ var VIEW_HTML = `<!doctype html>
5055
5451
  #detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
5056
5452
  .badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
5057
5453
  .badge.warn { background: #f59e0b33; }
5454
+ .badge.danger { background: #ef444433; }
5455
+ .badge.ok { background: #22c55e33; }
5058
5456
  pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
5059
5457
  table.kv { border-collapse: collapse; }
5060
5458
  table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
@@ -5063,6 +5461,11 @@ var VIEW_HTML = `<!doctype html>
5063
5461
  .card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
5064
5462
  .card .n { font-size: 22px; font-weight: 700; }
5065
5463
  .card .l { font-size: 12px; opacity: .7; }
5464
+ .pcard { min-width: 240px; max-width: 340px; }
5465
+ .pcard.open { cursor: pointer; }
5466
+ .pcard.open:hover { background: #8881; }
5467
+ .pcard .l { font-size: 14px; font-weight: 700; opacity: 1; margin-bottom: 4px; }
5468
+ .pcard .f { font-size: 13px; }
5066
5469
  .tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
5067
5470
  .tl .ev { margin-bottom: 8px; }
5068
5471
  .tl .ev .t { font-size: 12px; opacity: .65; }
@@ -5072,6 +5475,7 @@ var VIEW_HTML = `<!doctype html>
5072
5475
  <body>
5073
5476
  <header>
5074
5477
  <h1>basou view</h1>
5478
+ <button id="btn-back" style="display:none">&larr; portfolio</button>
5075
5479
  <input type="text" id="project" placeholder="source root (optional override)" />
5076
5480
  <button class="primary" id="btn-refresh">Refresh all</button>
5077
5481
  <button id="btn-import-claude">Import claude-code</button>
@@ -5091,7 +5495,12 @@ var VIEW_HTML = `<!doctype html>
5091
5495
  <script>
5092
5496
  (function () {
5093
5497
  var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
5094
- var state = { tab: 'overview', repoRoot: '' };
5498
+ // base is the API prefix for the active workspace: '/api' in single mode,
5499
+ // '/api/ws/<key>' once a portfolio card is opened.
5500
+ // canAct gates the mutating action bar: true only when a concrete workspace
5501
+ // is active (single mode, or a portfolio card opened). It is the real safety
5502
+ // guard \u2014 body.landing also hides the buttons, but that is cosmetic.
5503
+ var state = { tab: 'overview', repoRoot: '', base: '/api', mode: 'single', wsKey: null, canAct: false };
5095
5504
 
5096
5505
  function $(id) { return document.getElementById(id); }
5097
5506
  function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
@@ -5155,7 +5564,15 @@ var VIEW_HTML = `<!doctype html>
5155
5564
  for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
5156
5565
  }
5157
5566
 
5567
+ // Enable the action bar only when a workspace is active; disabled buttons
5568
+ // cannot post to a stale/wrong workspace even if a CSS regression un-hides them.
5569
+ function updateActionBar() {
5570
+ var ids = ['btn-refresh', 'btn-import-claude', 'btn-import-codex', 'btn-gen-handoff', 'btn-gen-decisions'];
5571
+ for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = !state.canAct;
5572
+ }
5573
+
5158
5574
  function post(path, label) {
5575
+ if (!state.canAct) { setStatus('Open a workspace first.', true); return; }
5159
5576
  setBusy(true);
5160
5577
  setStatus(label + '...', false);
5161
5578
  fetchJson(path, {
@@ -5188,6 +5605,155 @@ var VIEW_HTML = `<!doctype html>
5188
5605
  return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
5189
5606
  }
5190
5607
 
5608
+ // --- portfolio landing --------------------------------------------------
5609
+
5610
+ function boot() {
5611
+ fetchJson('/api/portfolio').then(function (d) {
5612
+ if (d && d.mode === 'portfolio') { state.mode = 'portfolio'; showLanding(d); }
5613
+ else { enterSingle(); }
5614
+ }).catch(function () {
5615
+ // First-load bootstrap failure: the single-workspace view is the safe default.
5616
+ enterSingle();
5617
+ });
5618
+ }
5619
+
5620
+ // Re-render the portfolio landing (the back button). Unlike boot(), a fetch
5621
+ // failure here keeps the inert landing and shows an error rather than silently
5622
+ // dropping into single mode pointed at the first workspace.
5623
+ function backToPortfolio() {
5624
+ enterLandingChrome();
5625
+ fetchJson('/api/portfolio').then(function (d) {
5626
+ if (d && d.workspaces) renderCards(d);
5627
+ else portfolioError('Portfolio unavailable.');
5628
+ }).catch(function (err) { portfolioError('Could not load portfolio: ' + err.message); });
5629
+ }
5630
+
5631
+ function enterSingle() {
5632
+ state.mode = 'single';
5633
+ state.base = '/api';
5634
+ state.wsKey = null;
5635
+ state.canAct = true;
5636
+ document.body.classList.remove('landing');
5637
+ $('btn-back').style.display = 'none';
5638
+ updateActionBar();
5639
+ buildTabs();
5640
+ loadTab('overview');
5641
+ }
5642
+
5643
+ // Landing chrome: no workspace is active, so actions are disabled (and hidden
5644
+ // by body.landing). The disable is the safety guard; the hide is cosmetic.
5645
+ function enterLandingChrome() {
5646
+ state.wsKey = null;
5647
+ state.canAct = false;
5648
+ document.body.classList.add('landing');
5649
+ $('btn-back').style.display = 'none';
5650
+ setStatus('', false);
5651
+ clear($('tabs'));
5652
+ updateActionBar();
5653
+ single(true);
5654
+ }
5655
+
5656
+ function showLanding(d) { enterLandingChrome(); renderCards(d); }
5657
+
5658
+ function renderCards(d) {
5659
+ var detail = $('detail');
5660
+ clear(detail);
5661
+ var ws = d.workspaces || [];
5662
+ detail.appendChild(el('p', { class: 'muted', text: 'Portfolio \u2014 ' + ws.length + ' workspace(s). Click a card to open it.' }));
5663
+ var cards = el('div', { class: 'cards' }, []);
5664
+ ws.forEach(function (w) { cards.appendChild(portfolioCard(w, d.generatedAt)); });
5665
+ detail.appendChild(cards);
5666
+ }
5667
+
5668
+ function portfolioError(msg) {
5669
+ var detail = $('detail');
5670
+ clear(detail);
5671
+ detail.appendChild(el('p', { class: 'err', text: msg }));
5672
+ detail.appendChild(el('button', { text: 'Retry', onclick: backToPortfolio }));
5673
+ }
5674
+
5675
+ function highestRisk(approvals) {
5676
+ var order = ['critical', 'high', 'medium', 'low'];
5677
+ for (var i = 0; i < order.length; i++) {
5678
+ for (var j = 0; j < approvals.length; j++) {
5679
+ if (approvals[j].risk === order[i]) return order[i];
5680
+ }
5681
+ }
5682
+ return approvals.length ? approvals[0].risk : '';
5683
+ }
5684
+
5685
+ // Human-readable age of an ISO timestamp relative to the portfolio's
5686
+ // generatedAt ("now"), so a stale capture reads as "3d ago" not a raw ISO.
5687
+ function relAge(iso, nowIso) {
5688
+ if (!iso) return '(none)';
5689
+ var ms = Date.parse(nowIso) - Date.parse(iso);
5690
+ if (!isFinite(ms)) return iso;
5691
+ if (ms < 60000) return 'just now';
5692
+ var m = Math.floor(ms / 60000); if (m < 60) return m + 'm ago';
5693
+ var h = Math.floor(m / 60); if (h < 48) return h + 'h ago';
5694
+ return Math.floor(h / 24) + 'd ago';
5695
+ }
5696
+
5697
+ // A "run refresh" badge when a dry-run found uncaptured/changed native sessions,
5698
+ // an "up to date" badge when the capture is current, and nothing loud when the
5699
+ // staleness probe could not run (degrades to a quiet note).
5700
+ function stalenessBadge(st) {
5701
+ if (!st) return null;
5702
+ if (!st.checked) return el('span', { class: 'badge', text: 'freshness unknown' });
5703
+ if (st.unverifiableSessions > 0)
5704
+ return el('span', { class: 'badge danger', text: '\u26A0 ' + st.unverifiableSessions + ' unverifiable \u2014 run verify' });
5705
+ if (st.newSessions > 0)
5706
+ return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured \u2014 run refresh' });
5707
+ if (st.updatedSessions > 0)
5708
+ return el('span', { class: 'badge warn', text: st.updatedSessions + ' updated \u2014 run refresh' });
5709
+ return el('span', { class: 'badge ok', text: 'up to date' });
5710
+ }
5711
+
5712
+ function portfolioCard(w, generatedAt) {
5713
+ if (!w.initialized) {
5714
+ return el('div', { class: 'card pcard muted' }, [
5715
+ el('div', { class: 'l', text: w.label }),
5716
+ el('div', { class: 'f', text: w.error ? ('unreadable: ' + w.error) : 'not initialized' })
5717
+ ]);
5718
+ }
5719
+ if (w.error) {
5720
+ return el('div', { class: 'card pcard' }, [
5721
+ el('div', { class: 'l', text: w.label }),
5722
+ el('div', { class: 'f' }, [el('span', { class: 'badge warn', text: 'unreadable: ' + w.error })])
5723
+ ]);
5724
+ }
5725
+ var pend = w.pendingApprovals || [];
5726
+ var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
5727
+ var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
5728
+ var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
5729
+ var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
5730
+ var badge = stalenessBadge(w.staleness);
5731
+ return el('div', { class: 'card pcard open', onclick: function () { openWorkspace(w.key, w.label); } }, [
5732
+ el('div', { class: 'l' }, [
5733
+ el('span', { text: w.label }),
5734
+ badge ? el('span', { text: ' ' }) : null,
5735
+ badge
5736
+ ]),
5737
+ el('div', { class: 'f', text: 'now: ' + now }),
5738
+ el('div', { class: 'f', text: 'latest: ' + dec }),
5739
+ el('div', { class: 'f', text: 'in-flight ' + w.inFlightCount + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
5740
+ el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
5741
+ ]);
5742
+ }
5743
+
5744
+ function openWorkspace(key, label) {
5745
+ state.mode = 'portfolio';
5746
+ state.wsKey = key;
5747
+ state.base = '/api/ws/' + encodeURIComponent(key);
5748
+ state.canAct = true;
5749
+ document.body.classList.remove('landing');
5750
+ $('btn-back').style.display = '';
5751
+ updateActionBar();
5752
+ setStatus('workspace: ' + label, false);
5753
+ buildTabs();
5754
+ loadTab('overview');
5755
+ }
5756
+
5191
5757
  // --- tabs ---------------------------------------------------------------
5192
5758
 
5193
5759
  function buildTabs() {
@@ -5211,16 +5777,16 @@ var VIEW_HTML = `<!doctype html>
5211
5777
  if (name === 'stats') return loadStats();
5212
5778
  if (name === 'sessions') return loadSessions();
5213
5779
  if (name === 'tasks') return loadTasks();
5214
- if (name === 'decisions') return loadMarkdown('/api/decisions', 'decisions');
5780
+ if (name === 'decisions') return loadMarkdown(state.base + '/decisions', 'decisions');
5215
5781
  if (name === 'approvals') return loadApprovals();
5216
- if (name === 'handoff') return loadMarkdown('/api/handoff', 'handoff');
5782
+ if (name === 'handoff') return loadMarkdown(state.base + '/handoff', 'handoff');
5217
5783
  }
5218
5784
 
5219
5785
  function fail(err) { setStatus(err.message, true); }
5220
5786
 
5221
5787
  function loadOverview() {
5222
5788
  single(true);
5223
- fetchJson('/api/overview').then(function (d) {
5789
+ fetchJson(state.base + '/overview').then(function (d) {
5224
5790
  var detail = $('detail');
5225
5791
  if (!d || d.initialized === false) {
5226
5792
  detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
@@ -5269,7 +5835,7 @@ var VIEW_HTML = `<!doctype html>
5269
5835
 
5270
5836
  function loadStats() {
5271
5837
  single(true);
5272
- fetchJson('/api/stats').then(function (d) {
5838
+ fetchJson(state.base + '/stats').then(function (d) {
5273
5839
  var detail = $('detail');
5274
5840
  var t = d.totals;
5275
5841
  detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
@@ -5330,7 +5896,7 @@ var VIEW_HTML = `<!doctype html>
5330
5896
 
5331
5897
  function loadSessions() {
5332
5898
  single(false);
5333
- fetchJson('/api/sessions').then(function (d) {
5899
+ fetchJson(state.base + '/sessions').then(function (d) {
5334
5900
  var list = $('list');
5335
5901
  var rows = (d && d.sessions) || [];
5336
5902
  if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
@@ -5349,7 +5915,7 @@ var VIEW_HTML = `<!doctype html>
5349
5915
  row.classList.add('active');
5350
5916
  var detail = $('detail');
5351
5917
  clear(detail);
5352
- fetchJson('/api/sessions/' + encodeURIComponent(id)).then(function (d) {
5918
+ fetchJson(state.base + '/sessions/' + encodeURIComponent(id)).then(function (d) {
5353
5919
  var s = d.session.session;
5354
5920
  detail.appendChild(el('h3', { text: s.label || id }));
5355
5921
  detail.appendChild(kv([
@@ -5382,7 +5948,7 @@ var VIEW_HTML = `<!doctype html>
5382
5948
 
5383
5949
  function loadTasks() {
5384
5950
  single(false);
5385
- fetchJson('/api/tasks').then(function (d) {
5951
+ fetchJson(state.base + '/tasks').then(function (d) {
5386
5952
  var list = $('list');
5387
5953
  var rows = (d && d.tasks) || [];
5388
5954
  if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
@@ -5401,7 +5967,7 @@ var VIEW_HTML = `<!doctype html>
5401
5967
  row.classList.add('active');
5402
5968
  var detail = $('detail');
5403
5969
  clear(detail);
5404
- fetchJson('/api/tasks/' + encodeURIComponent(id)).then(function (d) {
5970
+ fetchJson(state.base + '/tasks/' + encodeURIComponent(id)).then(function (d) {
5405
5971
  detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
5406
5972
  detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
5407
5973
  if (d.body) detail.appendChild(el('pre', { text: d.body }));
@@ -5420,7 +5986,7 @@ var VIEW_HTML = `<!doctype html>
5420
5986
 
5421
5987
  function loadApprovals() {
5422
5988
  single(true);
5423
- fetchJson('/api/approvals').then(function (d) {
5989
+ fetchJson(state.base + '/approvals').then(function (d) {
5424
5990
  var detail = $('detail');
5425
5991
  var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
5426
5992
  groups.forEach(function (g) {
@@ -5445,14 +6011,14 @@ var VIEW_HTML = `<!doctype html>
5445
6011
 
5446
6012
  // --- wire up ------------------------------------------------------------
5447
6013
 
5448
- $('btn-refresh').addEventListener('click', function () { post('/api/refresh', 'Refresh all'); });
5449
- $('btn-import-claude').addEventListener('click', function () { post('/api/import/claude-code', 'Import claude-code'); });
5450
- $('btn-import-codex').addEventListener('click', function () { post('/api/import/codex', 'Import codex'); });
5451
- $('btn-gen-handoff').addEventListener('click', function () { post('/api/handoff/generate', 'Regenerate handoff'); });
5452
- $('btn-gen-decisions').addEventListener('click', function () { post('/api/decisions/generate', 'Regenerate decisions'); });
6014
+ $('btn-back').addEventListener('click', function () { backToPortfolio(); });
6015
+ $('btn-refresh').addEventListener('click', function () { post(state.base + '/refresh', 'Refresh all'); });
6016
+ $('btn-import-claude').addEventListener('click', function () { post(state.base + '/import/claude-code', 'Import claude-code'); });
6017
+ $('btn-import-codex').addEventListener('click', function () { post(state.base + '/import/codex', 'Import codex'); });
6018
+ $('btn-gen-handoff').addEventListener('click', function () { post(state.base + '/handoff/generate', 'Regenerate handoff'); });
6019
+ $('btn-gen-decisions').addEventListener('click', function () { post(state.base + '/decisions/generate', 'Regenerate decisions'); });
5453
6020
 
5454
- buildTabs();
5455
- loadTab('overview');
6021
+ boot();
5456
6022
  })();
5457
6023
  </script>
5458
6024
  </body>
@@ -5467,6 +6033,8 @@ var HttpError = class extends Error {
5467
6033
  status;
5468
6034
  };
5469
6035
  var MAX_BODY_BYTES = 64 * 1024;
6036
+ var API_PREFIX = "/api/";
6037
+ var WS_PREFIX = "/api/ws/";
5470
6038
  function startViewServer(opts) {
5471
6039
  const { port, host = "127.0.0.1", deps } = opts;
5472
6040
  let actionQueue = Promise.resolve();
@@ -5480,7 +6048,7 @@ function startViewServer(opts) {
5480
6048
  };
5481
6049
  let boundPort = port;
5482
6050
  const getPort = () => boundPort;
5483
- return new Promise((resolve4, reject) => {
6051
+ return new Promise((resolve7, reject) => {
5484
6052
  const server = createServer((req, res) => {
5485
6053
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
5486
6054
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -5491,7 +6059,7 @@ function startViewServer(opts) {
5491
6059
  const address = server.address();
5492
6060
  boundPort = isAddressInfo(address) ? address.port : port;
5493
6061
  server.off("error", reject);
5494
- resolve4({
6062
+ resolve7({
5495
6063
  url: `http://${host}:${boundPort}`,
5496
6064
  port: boundPort,
5497
6065
  close: () => closeServer(server)
@@ -5503,8 +6071,8 @@ function isAddressInfo(value) {
5503
6071
  return value !== null && typeof value === "object";
5504
6072
  }
5505
6073
  function closeServer(server) {
5506
- return new Promise((resolve4) => {
5507
- server.close(() => resolve4());
6074
+ return new Promise((resolve7) => {
6075
+ server.close(() => resolve7());
5508
6076
  server.closeAllConnections();
5509
6077
  });
5510
6078
  }
@@ -5536,90 +6104,204 @@ async function handleGet(res, pathname, deps) {
5536
6104
  sendHtml(res, VIEW_HTML);
5537
6105
  return;
5538
6106
  }
5539
- if (pathname === "/api/overview") {
5540
- sendJson(res, 200, await overview(deps));
6107
+ if (pathname === "/api/portfolio") {
6108
+ sendJson(res, 200, await portfolio(deps));
5541
6109
  return;
5542
6110
  }
5543
- if (pathname === "/api/sessions") {
5544
- sendJson(res, 200, await sessionsList(deps));
6111
+ const scoped = matchWsRoute(pathname);
6112
+ if (scoped !== null) {
6113
+ const ws = findWorkspace(deps, scoped.key);
6114
+ if (ws === null) {
6115
+ sendError(res, 404, "Unknown workspace");
6116
+ return;
6117
+ }
6118
+ if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
6119
+ sendError(res, 404, "Not found");
6120
+ }
5545
6121
  return;
5546
6122
  }
5547
- const sessionId = matchId(pathname, "/api/sessions/");
5548
- if (sessionId !== null) {
5549
- sendJson(res, 200, await sessionDetail(deps, sessionId));
6123
+ if (pathname.startsWith(API_PREFIX)) {
6124
+ const sub = pathname.slice(API_PREFIX.length);
6125
+ if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
6126
+ sendError(res, 404, "Not found");
6127
+ }
5550
6128
  return;
5551
6129
  }
5552
- if (pathname === "/api/tasks") {
5553
- sendJson(res, 200, await tasksList(deps));
6130
+ sendError(res, 404, "Not found");
6131
+ }
6132
+ async function handlePost(res, pathname, body, deps, runExclusive) {
6133
+ const scoped = matchWsRoute(pathname);
6134
+ if (scoped !== null) {
6135
+ const ws = findWorkspace(deps, scoped.key);
6136
+ if (ws === null) {
6137
+ sendError(res, 404, "Unknown workspace");
6138
+ return;
6139
+ }
6140
+ if (!await handleWorkspacePost(res, scoped.sub, ws, body, deps, runExclusive)) {
6141
+ sendError(res, 404, "Not found");
6142
+ }
5554
6143
  return;
5555
6144
  }
5556
- const taskId = matchId(pathname, "/api/tasks/");
5557
- if (taskId !== null) {
5558
- sendJson(res, 200, await taskDetail(deps, taskId));
6145
+ if (pathname.startsWith(API_PREFIX)) {
6146
+ const sub = pathname.slice(API_PREFIX.length);
6147
+ if (!await handleWorkspacePost(res, sub, primaryWorkspace(deps), body, deps, runExclusive)) {
6148
+ sendError(res, 404, "Not found");
6149
+ }
5559
6150
  return;
5560
6151
  }
5561
- if (pathname === "/api/decisions") {
5562
- sendJson(res, 200, await decisionsView(deps));
5563
- return;
6152
+ sendError(res, 404, "Not found");
6153
+ }
6154
+ async function handleWorkspaceGet(res, sub, ws, nowProvider) {
6155
+ if (sub === "overview") {
6156
+ sendJson(res, 200, await overview(ws, nowProvider));
6157
+ return true;
5564
6158
  }
5565
- if (pathname === "/api/approvals") {
5566
- sendJson(res, 200, await approvalsView(deps));
5567
- return;
6159
+ if (sub === "sessions") {
6160
+ sendJson(res, 200, await sessionsList(ws, nowProvider));
6161
+ return true;
5568
6162
  }
5569
- if (pathname === "/api/handoff") {
5570
- sendJson(res, 200, await handoffView(deps));
5571
- return;
6163
+ const sessionId = matchId(sub, "sessions/");
6164
+ if (sessionId !== null) {
6165
+ sendJson(res, 200, await sessionDetail(ws, sessionId));
6166
+ return true;
5572
6167
  }
5573
- if (pathname === "/api/stats") {
5574
- sendJson(res, 200, await computeWorkStats2({ paths: deps.paths, now: deps.nowProvider() }));
5575
- return;
6168
+ if (sub === "tasks") {
6169
+ sendJson(res, 200, await tasksList(ws));
6170
+ return true;
5576
6171
  }
5577
- sendError(res, 404, "Not found");
6172
+ const taskId = matchId(sub, "tasks/");
6173
+ if (taskId !== null) {
6174
+ sendJson(res, 200, await taskDetail(ws, taskId));
6175
+ return true;
6176
+ }
6177
+ if (sub === "decisions") {
6178
+ sendJson(res, 200, await decisionsView(ws, nowProvider));
6179
+ return true;
6180
+ }
6181
+ if (sub === "approvals") {
6182
+ sendJson(res, 200, await approvalsView(ws, nowProvider));
6183
+ return true;
6184
+ }
6185
+ if (sub === "handoff") {
6186
+ sendJson(res, 200, await handoffView(ws, nowProvider));
6187
+ return true;
6188
+ }
6189
+ if (sub === "stats") {
6190
+ sendJson(res, 200, await computeWorkStats2({ paths: ws.paths, now: nowProvider() }));
6191
+ return true;
6192
+ }
6193
+ return false;
5578
6194
  }
5579
- async function handlePost(res, pathname, body, deps, runExclusive) {
6195
+ async function handleWorkspacePost(res, sub, ws, body, deps, runExclusive) {
5580
6196
  const nowIso = deps.nowProvider().toISOString();
5581
6197
  const actionOptions = readActionOptions(body);
5582
- if (pathname === "/api/refresh") {
6198
+ if (sub === "refresh") {
5583
6199
  const result = await runExclusive(
5584
- () => refreshAll({ options: actionOptions, ctx: deps.importCtx, paths: deps.paths, nowIso })
6200
+ () => refreshAll({ options: actionOptions, ctx: ws.importCtx, paths: ws.paths, nowIso })
5585
6201
  );
5586
6202
  sendJson(res, 200, result);
5587
- return;
6203
+ return true;
5588
6204
  }
5589
- if (pathname === "/api/import/claude-code") {
5590
- sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, deps.importCtx)));
5591
- return;
6205
+ if (sub === "import/claude-code") {
6206
+ sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, ws.importCtx)));
6207
+ return true;
5592
6208
  }
5593
- if (pathname === "/api/import/codex") {
5594
- sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, deps.importCtx)));
5595
- return;
6209
+ if (sub === "import/codex") {
6210
+ sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, ws.importCtx)));
6211
+ return true;
5596
6212
  }
5597
- if (pathname === "/api/handoff/generate") {
5598
- sendJson(res, 200, await runExclusive(() => regenerateHandoff(deps.paths, nowIso)));
5599
- return;
6213
+ if (sub === "handoff/generate") {
6214
+ sendJson(res, 200, await runExclusive(() => regenerateHandoff(ws.paths, nowIso)));
6215
+ return true;
5600
6216
  }
5601
- if (pathname === "/api/decisions/generate") {
5602
- sendJson(res, 200, await runExclusive(() => regenerateDecisions(deps.paths, nowIso)));
5603
- return;
6217
+ if (sub === "decisions/generate") {
6218
+ sendJson(res, 200, await runExclusive(() => regenerateDecisions(ws.paths, nowIso)));
6219
+ return true;
5604
6220
  }
5605
- sendError(res, 404, "Not found");
6221
+ return false;
6222
+ }
6223
+ function primaryWorkspace(deps) {
6224
+ const first = deps.workspaces[0];
6225
+ if (first === void 0) throw new HttpError(500, "No workspace configured");
6226
+ return first;
6227
+ }
6228
+ function findWorkspace(deps, key) {
6229
+ return deps.workspaces.find((w) => w.key === key) ?? null;
5606
6230
  }
5607
- async function overview(deps) {
6231
+ function matchWsRoute(pathname) {
6232
+ if (!pathname.startsWith(WS_PREFIX)) return null;
6233
+ const rest = pathname.slice(WS_PREFIX.length);
6234
+ const slash = rest.indexOf("/");
6235
+ if (slash <= 0) return null;
6236
+ const sub = rest.slice(slash + 1);
6237
+ if (sub.length === 0) return null;
6238
+ let key;
6239
+ try {
6240
+ key = decodeURIComponent(rest.slice(0, slash));
6241
+ } catch {
6242
+ return null;
6243
+ }
6244
+ if (key.length === 0 || key.includes("/") || key.includes("\0")) return null;
6245
+ return { key, sub };
6246
+ }
6247
+ async function portfolio(deps) {
6248
+ const nowIso = deps.nowProvider().toISOString();
6249
+ const workspaces = await Promise.all(deps.workspaces.map((ws) => portfolioCard(ws, nowIso)));
6250
+ for (let i = 0; i < deps.workspaces.length; i++) {
6251
+ const card = workspaces[i];
6252
+ const ws = deps.workspaces[i];
6253
+ if (ws !== void 0 && card !== void 0 && card.initialized === true && card.error === void 0) {
6254
+ card.staleness = await captureStaleness(ws, nowIso);
6255
+ }
6256
+ }
6257
+ return { mode: deps.mode, generatedAt: nowIso, workspaces };
6258
+ }
6259
+ async function portfolioCard(ws, nowIso) {
6260
+ const base = { key: ws.key, label: ws.label, repoRoot: ws.repoRoot };
6261
+ if (!ws.initialized) {
6262
+ return ws.manifestError !== void 0 ? { ...base, initialized: false, error: ws.manifestError } : { ...base, initialized: false };
6263
+ }
6264
+ try {
6265
+ const s = await summarizeOrientation({ paths: ws.paths, nowIso });
6266
+ return {
6267
+ ...base,
6268
+ initialized: true,
6269
+ sessionCount: s.sessionCount,
6270
+ suspectCount: s.suspects.length,
6271
+ inFlightCount: s.inFlightTasks.length,
6272
+ pendingApprovals: s.pendingApprovals.map((a) => ({
6273
+ risk: a.risk,
6274
+ kind: a.kind,
6275
+ expired: a.expired
6276
+ })),
6277
+ latestDecision: s.latestDecision !== null ? { title: s.latestDecision.title } : null,
6278
+ latestSession: s.latestSession !== null ? { label: s.latestSession.label, status: s.latestSession.status } : null,
6279
+ freshness: { newestStartedAt: s.freshness.newestStartedAt, bySource: s.freshness.bySource }
6280
+ };
6281
+ } catch (error) {
6282
+ return { ...base, initialized: true, error: pathlessMessage(error) };
6283
+ }
6284
+ }
6285
+ async function captureStaleness(ws, nowIso) {
6286
+ const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
6287
+ return probe === null ? { checked: false } : { checked: true, ...probe };
6288
+ }
6289
+ async function overview(ws, nowProvider) {
5608
6290
  let manifest;
5609
6291
  try {
5610
- manifest = await readManifest8(deps.paths);
6292
+ manifest = await readManifest9(ws.paths);
5611
6293
  } catch (error) {
5612
- if (findErrorCode14(error, "ENOENT")) {
5613
- return { initialized: false, repoRoot: deps.repoRoot };
6294
+ if (findErrorCode15(error, "ENOENT")) {
6295
+ return { initialized: false, repoRoot: ws.repoRoot };
5614
6296
  }
5615
6297
  throw error;
5616
6298
  }
5617
- const nowIso = deps.nowProvider().toISOString();
5618
- const handoff = await renderHandoff3({ paths: deps.paths, nowIso });
5619
- const approvals = await enumerateApprovals2(deps.paths);
6299
+ const nowIso = nowProvider().toISOString();
6300
+ const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
6301
+ const approvals = await enumerateApprovals2(ws.paths);
5620
6302
  return {
5621
6303
  initialized: true,
5622
- repoRoot: deps.repoRoot,
6304
+ repoRoot: ws.repoRoot,
5623
6305
  workspace: {
5624
6306
  id: manifest.workspace.id,
5625
6307
  name: manifest.workspace.name,
@@ -5637,8 +6319,8 @@ async function overview(deps) {
5637
6319
  generatedAt: nowIso
5638
6320
  };
5639
6321
  }
5640
- async function sessionsList(deps) {
5641
- const entries = await loadSessionEntries3(deps.paths, { now: deps.nowProvider() });
6322
+ async function sessionsList(ws, nowProvider) {
6323
+ const entries = await loadSessionEntries3(ws.paths, { now: nowProvider() });
5642
6324
  const sessions = entries.map((entry) => ({
5643
6325
  sessionId: entry.sessionId,
5644
6326
  label: entry.session.session.label ?? null,
@@ -5653,10 +6335,10 @@ async function sessionsList(deps) {
5653
6335
  })).reverse();
5654
6336
  return { sessions };
5655
6337
  }
5656
- async function sessionDetail(deps, sessionId) {
6338
+ async function sessionDetail(ws, sessionId) {
5657
6339
  let session;
5658
6340
  try {
5659
- session = await readSessionYaml3(deps.paths, sessionId);
6341
+ session = await readSessionYaml3(ws.paths, sessionId);
5660
6342
  } catch (error) {
5661
6343
  if (error instanceof Error && error.message === "YAML file not found") {
5662
6344
  throw new HttpError(404, "Session not found");
@@ -5664,19 +6346,19 @@ async function sessionDetail(deps, sessionId) {
5664
6346
  throw error;
5665
6347
  }
5666
6348
  try {
5667
- const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
6349
+ const events = await readAllEvents2(join10(ws.paths.sessions, sessionId));
5668
6350
  return { session, events };
5669
6351
  } catch {
5670
6352
  return { session, events: [], degraded: true };
5671
6353
  }
5672
6354
  }
5673
- async function tasksList(deps) {
5674
- const entries = await loadTaskEntries2(deps.paths);
6355
+ async function tasksList(ws) {
6356
+ const entries = await loadTaskEntries2(ws.paths);
5675
6357
  return { tasks: entries.map((entry) => entry.task).reverse() };
5676
6358
  }
5677
- async function taskDetail(deps, taskId) {
6359
+ async function taskDetail(ws, taskId) {
5678
6360
  try {
5679
- const doc = await readTaskFile2(deps.paths, taskId);
6361
+ const doc = await readTaskFile2(ws.paths, taskId);
5680
6362
  return { task: doc.task, body: doc.body };
5681
6363
  } catch (error) {
5682
6364
  if (error instanceof Error && error.message === "Task file not found") {
@@ -5685,22 +6367,22 @@ async function taskDetail(deps, taskId) {
5685
6367
  throw error;
5686
6368
  }
5687
6369
  }
5688
- async function decisionsView(deps) {
5689
- const fromDisk = await readMarkdownFile4(deps.paths.files.decisions);
6370
+ async function decisionsView(ws, nowProvider) {
6371
+ const fromDisk = await readMarkdownFile4(ws.paths.files.decisions);
5690
6372
  if (fromDisk !== null) {
5691
6373
  return { body: fromDisk, fromDisk: true };
5692
6374
  }
5693
- const nowIso = deps.nowProvider().toISOString();
5694
- const result = await renderDecisions3({ paths: deps.paths, nowIso });
6375
+ const nowIso = nowProvider().toISOString();
6376
+ const result = await renderDecisions3({ paths: ws.paths, nowIso });
5695
6377
  return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
5696
6378
  }
5697
- async function approvalsView(deps) {
5698
- const now = deps.nowProvider();
5699
- const ids = await enumerateApprovals2(deps.paths);
6379
+ async function approvalsView(ws, nowProvider) {
6380
+ const now = nowProvider();
6381
+ const ids = await enumerateApprovals2(ws.paths);
5700
6382
  const toViews = async (list) => {
5701
6383
  const views = [];
5702
6384
  for (const id of list) {
5703
- const loaded = await loadApproval2(deps.paths, id);
6385
+ const loaded = await loadApproval2(ws.paths, id);
5704
6386
  if (loaded === null) continue;
5705
6387
  views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
5706
6388
  }
@@ -5708,13 +6390,13 @@ async function approvalsView(deps) {
5708
6390
  };
5709
6391
  return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
5710
6392
  }
5711
- async function handoffView(deps) {
5712
- const fromDisk = await readMarkdownFile4(deps.paths.files.handoff);
6393
+ async function handoffView(ws, nowProvider) {
6394
+ const fromDisk = await readMarkdownFile4(ws.paths.files.handoff);
5713
6395
  if (fromDisk !== null) {
5714
6396
  return { body: fromDisk, fromDisk: true };
5715
6397
  }
5716
- const nowIso = deps.nowProvider().toISOString();
5717
- const result = await renderHandoff3({ paths: deps.paths, nowIso });
6398
+ const nowIso = nowProvider().toISOString();
6399
+ const result = await renderHandoff3({ paths: ws.paths, nowIso });
5718
6400
  return { body: result.body, fromDisk: false };
5719
6401
  }
5720
6402
  function readActionOptions(body) {
@@ -5808,8 +6490,18 @@ function parsePort(value) {
5808
6490
  }
5809
6491
  return port;
5810
6492
  }
6493
+ function collectPath3(value, previous = []) {
6494
+ return [...previous, value];
6495
+ }
5811
6496
  function registerViewCommand(program2) {
5812
- program2.command("view").description("Open a local web UI to browse provenance and run imports (localhost only)").option("--port <number>", "Port to listen on (default 4319)", parsePort).option("--no-open", "Do not open the browser automatically").option("-v, --verbose", "Show error causes").action(async (options) => {
6497
+ program2.command("view").description("Open a local web UI to browse provenance and run imports (localhost only)").option("--port <number>", "Port to listen on (default 4319)", parsePort).option("--no-open", "Do not open the browser automatically").option(
6498
+ "--portfolio",
6499
+ "Serve every workspace listed in ~/.basou/portfolio.yaml (cross-repo orientation)"
6500
+ ).option(
6501
+ "--workspace <path>",
6502
+ "Workspace repo path to include (repeatable; implies portfolio mode; resolved against the cwd)",
6503
+ collectPath3
6504
+ ).option("--check", "Run the portfolio safety preflight and exit (no server)").option("--skip-safety-check", "Skip the portfolio safety preflight on start (not recommended)").option("-v, --verbose", "Show error causes").action(async (options) => {
5813
6505
  await runView(options);
5814
6506
  });
5815
6507
  }
@@ -5823,23 +6515,37 @@ async function runView(options, ctx = {}) {
5823
6515
  }
5824
6516
  async function doRunView(options, ctx) {
5825
6517
  const cwd = ctx.cwd ?? process.cwd();
5826
- const repositoryRoot = await resolveRepositoryRootForView(cwd);
5827
- const paths = basouPaths15(repositoryRoot);
5828
- await assertWorkspaceInitialized12(paths.root);
5829
- const deps = {
5830
- paths,
5831
- repoRoot: repositoryRoot,
5832
- importCtx: {
5833
- cwd: repositoryRoot,
5834
- ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
5835
- ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
5836
- },
5837
- nowProvider: ctx.nowProvider ?? (() => /* @__PURE__ */ new Date())
5838
- };
6518
+ const workspaceFlags = options.workspace ?? [];
6519
+ const isPortfolio = workspaceFlags.length > 0 || options.portfolio === true;
6520
+ const deps = isPortfolio ? await buildPortfolioDeps(workspaceFlags, ctx, cwd) : await buildSingleDeps(ctx, cwd);
6521
+ if (options.check === true) {
6522
+ const result = await checkPortfolioSafety(deps.workspaces);
6523
+ for (const line of formatSafetyReport(result)) console.log(line);
6524
+ if (result.findings.length > 0) process.exitCode = 1;
6525
+ return;
6526
+ }
6527
+ if (deps.mode === "portfolio" && options.skipSafetyCheck !== true) {
6528
+ const result = await checkPortfolioSafety(deps.workspaces);
6529
+ const blocking = result.findings.filter((f) => f.kind === "footprint" || f.kind === "overlap");
6530
+ if (blocking.length > 0) {
6531
+ for (const line of formatSafetyReport(result)) console.error(line);
6532
+ throw new Error(
6533
+ "Portfolio safety preflight failed (see findings above). Fix the monitored repos, or re-run with --skip-safety-check to override."
6534
+ );
6535
+ }
6536
+ if (result.findings.length > 0) {
6537
+ console.error(
6538
+ `Portfolio safety: ${result.findings.length} unverifiable item(s) \u2014 the read-only view will still open; run 'basou view --check' for detail.`
6539
+ );
6540
+ }
6541
+ }
5839
6542
  const port = options.port ?? DEFAULT_PORT;
5840
6543
  const handle = await startListening(port, deps);
5841
6544
  try {
5842
6545
  console.log(`basou view running at ${handle.url}`);
6546
+ if (deps.mode === "portfolio") {
6547
+ console.log(`Portfolio mode: ${deps.workspaces.length} workspace(s).`);
6548
+ }
5843
6549
  console.log(
5844
6550
  "Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
5845
6551
  );
@@ -5852,11 +6558,69 @@ async function doRunView(options, ctx) {
5852
6558
  await handle.close();
5853
6559
  }
5854
6560
  }
6561
+ async function buildSingleDeps(ctx, cwd) {
6562
+ const repositoryRoot = await resolveRepositoryRootForView(cwd);
6563
+ const paths = basouPaths16(repositoryRoot);
6564
+ await assertWorkspaceInitialized13(paths.root);
6565
+ const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
6566
+ return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
6567
+ }
6568
+ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
6569
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve6(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
6570
+ const entries = [];
6571
+ const seenPath = /* @__PURE__ */ new Set();
6572
+ const seenKey = /* @__PURE__ */ new Set();
6573
+ for (const spec of specs) {
6574
+ const repoRoot = resolve6(spec.path);
6575
+ if (seenPath.has(repoRoot)) continue;
6576
+ seenPath.add(repoRoot);
6577
+ const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
6578
+ let key = entry.key;
6579
+ for (let n = 1; seenKey.has(key); n++) key = `${entry.key}-${n}`;
6580
+ seenKey.add(key);
6581
+ entries.push({ ...entry, key });
6582
+ }
6583
+ if (entries.length === 0) throw new Error("No workspaces to show.");
6584
+ return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
6585
+ }
6586
+ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
6587
+ const paths = basouPaths16(repoRoot);
6588
+ const importCtx = {
6589
+ cwd: repoRoot,
6590
+ ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
6591
+ ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
6592
+ };
6593
+ try {
6594
+ const manifest = await readManifest10(paths);
6595
+ return {
6596
+ key: manifest.workspace.id,
6597
+ label: labelOverride ?? manifest.workspace.name,
6598
+ paths,
6599
+ repoRoot,
6600
+ importCtx,
6601
+ initialized: true
6602
+ };
6603
+ } catch (error) {
6604
+ const notFound = error instanceof Error && error.message === "YAML file not found";
6605
+ return {
6606
+ key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
6607
+ label: labelOverride ?? basename4(repoRoot),
6608
+ paths,
6609
+ repoRoot,
6610
+ importCtx,
6611
+ initialized: false,
6612
+ ...notFound ? {} : { manifestError: "manifest unreadable or invalid" }
6613
+ };
6614
+ }
6615
+ }
6616
+ function nowProviderOf(ctx) {
6617
+ return ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
6618
+ }
5855
6619
  async function startListening(port, deps) {
5856
6620
  try {
5857
6621
  return await startViewServer({ port, deps });
5858
6622
  } catch (error) {
5859
- if (findErrorCode15(error, "EADDRINUSE")) {
6623
+ if (findErrorCode16(error, "EADDRINUSE")) {
5860
6624
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
5861
6625
  cause: error
5862
6626
  });
@@ -5879,7 +6643,7 @@ function openInBrowser(url, override) {
5879
6643
  }
5880
6644
  }
5881
6645
  function waitForShutdown(signal) {
5882
- return new Promise((resolve4) => {
6646
+ return new Promise((resolve7) => {
5883
6647
  const cleanup = () => {
5884
6648
  process.off("SIGINT", onSignal);
5885
6649
  process.off("SIGTERM", onSignal);
@@ -5887,18 +6651,18 @@ function waitForShutdown(signal) {
5887
6651
  };
5888
6652
  const onSignal = () => {
5889
6653
  cleanup();
5890
- resolve4();
6654
+ resolve7();
5891
6655
  };
5892
6656
  const onAbort = () => {
5893
6657
  cleanup();
5894
- resolve4();
6658
+ resolve7();
5895
6659
  };
5896
6660
  process.on("SIGINT", onSignal);
5897
6661
  process.on("SIGTERM", onSignal);
5898
6662
  if (signal !== void 0) {
5899
6663
  if (signal.aborted) {
5900
6664
  cleanup();
5901
- resolve4();
6665
+ resolve7();
5902
6666
  return;
5903
6667
  }
5904
6668
  signal.addEventListener("abort", onAbort);
@@ -5907,7 +6671,7 @@ function waitForShutdown(signal) {
5907
6671
  }
5908
6672
  async function resolveRepositoryRootForView(cwd) {
5909
6673
  try {
5910
- return await resolveRepositoryRoot16(cwd);
6674
+ return await resolveRepositoryRoot15(cwd);
5911
6675
  } catch (error) {
5912
6676
  if (error instanceof Error && error.message === "Not a git repository") {
5913
6677
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
@@ -5917,11 +6681,11 @@ async function resolveRepositoryRootForView(cwd) {
5917
6681
  throw error;
5918
6682
  }
5919
6683
  }
5920
- async function assertWorkspaceInitialized12(basouRoot) {
6684
+ async function assertWorkspaceInitialized13(basouRoot) {
5921
6685
  try {
5922
- await assertBasouRootSafe15(basouRoot);
6686
+ await assertBasouRootSafe16(basouRoot);
5923
6687
  } catch (error) {
5924
- if (findErrorCode15(error, "ENOENT")) {
6688
+ if (findErrorCode16(error, "ENOENT")) {
5925
6689
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5926
6690
  }
5927
6691
  throw error;
@@ -5951,6 +6715,7 @@ function buildProgram() {
5951
6715
  registerHandoffCommand(program2);
5952
6716
  registerDecisionsCommand(program2);
5953
6717
  registerReportCommand(program2);
6718
+ registerOrientCommand(program2);
5954
6719
  return program2;
5955
6720
  }
5956
6721