@basou/cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/program.js CHANGED
@@ -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(program) {
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,187 @@ 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";
2011
+ // src/commands/note.ts
2012
+ import {
2013
+ acquireLock as acquireLock4,
2014
+ appendEventToExistingSession as appendEventToExistingSession2,
2015
+ assertBasouRootSafe as assertBasouRootSafe7,
2016
+ basouPaths as basouPaths7,
2017
+ createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
2018
+ findErrorCode as findErrorCode6,
2019
+ readManifest as readManifest4,
2020
+ resolveSessionId as resolveSessionId2
2021
+ } from "@basou/core";
2005
2022
  import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
2006
2023
 
2024
+ // src/lib/repo-root.ts
2025
+ import { resolveBasouRepositoryRoot } from "@basou/core";
2026
+ async function resolveBasouRootForCommand(cwd, commandName) {
2027
+ try {
2028
+ return await resolveBasouRepositoryRoot(cwd, {
2029
+ onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
2030
+ });
2031
+ } catch (error) {
2032
+ if (error instanceof Error && error.message === "Not a git repository") {
2033
+ throw new Error(
2034
+ `Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
2035
+ { cause: error }
2036
+ );
2037
+ }
2038
+ throw error;
2039
+ }
2040
+ }
2041
+
2042
+ // src/commands/note.ts
2043
+ var LABEL_BODY_MAX = 80;
2044
+ var LABEL_TRUNCATE_HEAD2 = LABEL_BODY_MAX - 3;
2045
+ function registerNoteCommand(program) {
2046
+ program.command("note").description("Record a free-text note (orientation surfaces the latest as the next step)").argument("<body>", "Note text", parseBody).option(
2047
+ "--session <session_id>",
2048
+ "Attach to an existing session; otherwise an ad-hoc session is created"
2049
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (body, options) => {
2050
+ await runNote(body, options);
2051
+ });
2052
+ }
2053
+ async function runNote(body, options, ctx = {}) {
2054
+ try {
2055
+ await doRunNote(body, options, ctx);
2056
+ } catch (error) {
2057
+ renderCliError(error, {
2058
+ verbose: isVerbose(options),
2059
+ classifiers: [failedToFinalizeClassifier]
2060
+ });
2061
+ process.exitCode = 1;
2062
+ }
2063
+ }
2064
+ async function doRunNote(body, options, ctx) {
2065
+ if (body.trim().length === 0) {
2066
+ throw new Error("Note body must not be empty");
2067
+ }
2068
+ const cwd = ctx.cwd ?? process.cwd();
2069
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "note");
2070
+ const paths = basouPaths7(repositoryRoot);
2071
+ await assertWorkspaceInitialized6(paths.root);
2072
+ const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
2073
+ const occurredAt = now.toISOString();
2074
+ if (options.session !== void 0) {
2075
+ const sessionId = await resolveSessionId2(paths, options.session);
2076
+ const sesId = sessionId;
2077
+ const sessionLock = await acquireLock4(paths, "session", sesId);
2078
+ let result;
2079
+ try {
2080
+ result = await appendEventToExistingSession2({
2081
+ paths,
2082
+ sessionId: sesId,
2083
+ eventBuilder: (eventId) => buildNoteEvent({ eventId, sessionId: sesId, occurredAt, body })
2084
+ });
2085
+ } finally {
2086
+ await sessionLock.release();
2087
+ }
2088
+ printNoteResult(options, {
2089
+ mode: "attached",
2090
+ sessionId,
2091
+ eventId: result.eventId,
2092
+ sessionStatus: result.sessionStatus,
2093
+ body
2094
+ });
2095
+ return;
2096
+ }
2097
+ const manifest = await readManifest4(paths);
2098
+ const adHoc = await createAdHocSessionWithEvent2({
2099
+ paths,
2100
+ manifest,
2101
+ label: buildAdHocLabel2(body),
2102
+ occurredAt,
2103
+ sessionSource: "human",
2104
+ workingDirectory: repositoryRoot,
2105
+ invocation: {
2106
+ command: "basou note",
2107
+ args: [body]
2108
+ },
2109
+ targetEventBuilders: [
2110
+ (sessionId, eventId) => buildNoteEvent({ eventId, sessionId, occurredAt, body })
2111
+ ]
2112
+ });
2113
+ printNoteResult(options, {
2114
+ mode: "ad-hoc",
2115
+ sessionId: adHoc.sessionId,
2116
+ eventId: adHoc.targetEventIds[0],
2117
+ sessionStatus: "completed",
2118
+ body
2119
+ });
2120
+ }
2121
+ function buildNoteEvent(input) {
2122
+ return {
2123
+ schema_version: "0.1.0",
2124
+ id: input.eventId,
2125
+ session_id: input.sessionId,
2126
+ occurred_at: input.occurredAt,
2127
+ source: "local-cli",
2128
+ type: "note_added",
2129
+ body: input.body,
2130
+ // `basou note` is the resume-hint command; mark it so orientation surfaces
2131
+ // it as the next step and a plain `basou session note` annotation does not.
2132
+ kind: "next_step"
2133
+ };
2134
+ }
2135
+ function buildAdHocLabel2(body) {
2136
+ const oneLine = body.replace(/\s+/g, " ").trim();
2137
+ const truncated = oneLine.length > LABEL_BODY_MAX ? `${oneLine.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine;
2138
+ return `Ad-hoc note: ${truncated}`;
2139
+ }
2140
+ function parseBody(raw) {
2141
+ if (raw.trim().length === 0) {
2142
+ throw new InvalidArgumentError2("Note body must not be empty");
2143
+ }
2144
+ return raw;
2145
+ }
2146
+ function printNoteResult(options, result) {
2147
+ const sid = shortSessionId(result.sessionId);
2148
+ if (options.json === true) {
2149
+ console.log(
2150
+ JSON.stringify({
2151
+ event_id: result.eventId,
2152
+ session_id: result.sessionId,
2153
+ session_status: result.sessionStatus,
2154
+ mode: result.mode,
2155
+ body: result.body
2156
+ })
2157
+ );
2158
+ return;
2159
+ }
2160
+ if (result.mode === "ad-hoc") {
2161
+ console.log(`Recorded note ${result.eventId} in ad-hoc session ${sid}`);
2162
+ } else {
2163
+ console.log(`Recorded note ${result.eventId} in session ${sid} (${result.sessionStatus})`);
2164
+ }
2165
+ }
2166
+ async function assertWorkspaceInitialized6(basouRoot) {
2167
+ try {
2168
+ await assertBasouRootSafe7(basouRoot);
2169
+ } catch (error) {
2170
+ if (findErrorCode6(error, "ENOENT")) {
2171
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
2172
+ }
2173
+ throw error;
2174
+ }
2175
+ }
2176
+
2177
+ // src/commands/orient.ts
2178
+ import {
2179
+ assertBasouRootSafe as assertBasouRootSafe8,
2180
+ basouPaths as basouPaths8,
2181
+ findErrorCode as findErrorCode7,
2182
+ renderOrientation as renderOrientation2,
2183
+ writeMarkdownFile as writeMarkdownFile4
2184
+ } from "@basou/core";
2185
+
2007
2186
  // src/lib/provenance-actions.ts
2008
2187
  import {
2009
2188
  readMarkdownFile as readMarkdownFile3,
2010
2189
  renderDecisions as renderDecisions2,
2011
2190
  renderHandoff as renderHandoff2,
2191
+ renderOrientation,
2012
2192
  renderWithMarkers as renderWithMarkers3,
2013
2193
  writeMarkdownFile as writeMarkdownFile3
2014
2194
  } from "@basou/core";
@@ -2059,6 +2239,7 @@ async function runImport(adapter, fn) {
2059
2239
  skippedNoAction: readCount(json.skipped_no_action),
2060
2240
  skippedAlreadyImported: readCount(json.skipped_already_imported),
2061
2241
  skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
2242
+ skippedUnverifiable: readCount(json.skipped_unverifiable),
2062
2243
  eventTotal: readCount(json.event_total),
2063
2244
  dryRun: json.dry_run === true
2064
2245
  };
@@ -2105,40 +2286,1889 @@ async function regenerateDecisions(paths, nowIso, callbacks) {
2105
2286
  paths.files.decisions,
2106
2287
  renderWithMarkers3(existing, result.body, "decisions.md")
2107
2288
  );
2108
- return { decisionCount: result.decisionCount };
2289
+ return { decisionCount: result.decisionCount };
2290
+ }
2291
+ async function regenerateOrientation(paths, nowIso, callbacks) {
2292
+ const result = await renderOrientation({ paths, nowIso, ...callbacks });
2293
+ await writeMarkdownFile3(paths.files.orientation, `${result.body}
2294
+ `);
2295
+ return {
2296
+ sessionCount: result.sessionCount,
2297
+ inFlightTaskCount: result.inFlightTaskCount,
2298
+ pendingApprovalsCount: result.pendingApprovalsCount,
2299
+ suspectCount: result.suspectCount
2300
+ };
2301
+ }
2302
+ async function refreshAll(args) {
2303
+ const { options, ctx, paths, nowIso } = args;
2304
+ const dryRun = options.dryRun === true;
2305
+ const claudeCode = await importClaudeCode(options, ctx);
2306
+ const codex = await importCodex(options, ctx);
2307
+ if (dryRun) {
2308
+ const skipped = { status: "skipped", reason: "dry-run" };
2309
+ return {
2310
+ claudeCode,
2311
+ codex,
2312
+ handoff: skipped,
2313
+ decisions: skipped,
2314
+ orientation: skipped,
2315
+ dryRun
2316
+ };
2317
+ }
2318
+ const handoffCounts = await regenerateHandoff(paths, nowIso);
2319
+ const decisionCounts = await regenerateDecisions(paths, nowIso);
2320
+ const scoped = options.project !== void 0 && options.project.length > 0;
2321
+ const orientationCounts = await regenerateOrientation(
2322
+ paths,
2323
+ nowIso,
2324
+ scoped ? {} : {
2325
+ staleness: {
2326
+ newSessions: 0,
2327
+ updatedSessions: 0,
2328
+ unverifiableSessions: wouldBlock(claudeCode) + wouldBlock(codex)
2329
+ }
2330
+ }
2331
+ );
2332
+ return {
2333
+ claudeCode,
2334
+ codex,
2335
+ handoff: { status: "generated", ...handoffCounts },
2336
+ decisions: { status: "generated", ...decisionCounts },
2337
+ orientation: { status: "generated", ...orientationCounts },
2338
+ dryRun
2339
+ };
2340
+ }
2341
+ function wouldImport(outcome) {
2342
+ return outcome.status === "ran" ? outcome.importedCount : 0;
2343
+ }
2344
+ function wouldUpdate(outcome) {
2345
+ return outcome.status === "ran" ? outcome.reimportedCount + outcome.replacedCount : 0;
2346
+ }
2347
+ function wouldBlock(outcome) {
2348
+ return outcome.status === "ran" ? outcome.skippedUnverifiable : 0;
2349
+ }
2350
+ async function probeStaleness(args) {
2351
+ try {
2352
+ const dry = await refreshAll({
2353
+ options: { dryRun: true },
2354
+ ctx: args.ctx,
2355
+ paths: args.paths,
2356
+ nowIso: args.nowIso
2357
+ });
2358
+ return {
2359
+ newSessions: wouldImport(dry.claudeCode) + wouldImport(dry.codex),
2360
+ updatedSessions: wouldUpdate(dry.claudeCode) + wouldUpdate(dry.codex),
2361
+ unverifiableSessions: wouldBlock(dry.claudeCode) + wouldBlock(dry.codex)
2362
+ };
2363
+ } catch {
2364
+ return null;
2365
+ }
2366
+ }
2367
+
2368
+ // src/commands/orient.ts
2369
+ function registerOrientCommand(program) {
2370
+ program.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) => {
2371
+ await runOrient(opts);
2372
+ });
2373
+ }
2374
+ async function runOrient(options, ctx = {}) {
2375
+ try {
2376
+ await doRunOrient(options, ctx);
2377
+ } catch (error) {
2378
+ renderCliError(error, { verbose: isVerbose(options) });
2379
+ process.exitCode = 1;
2380
+ }
2381
+ }
2382
+ async function doRunOrient(options, ctx) {
2383
+ const cwd = ctx.cwd ?? process.cwd();
2384
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "orient");
2385
+ const paths = basouPaths8(repositoryRoot);
2386
+ await assertWorkspaceInitialized7(paths.root);
2387
+ const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2388
+ const probeCtx = { cwd: repositoryRoot };
2389
+ if (ctx.claudeProjectsDir !== void 0) probeCtx.claudeProjectsDir = ctx.claudeProjectsDir;
2390
+ if (ctx.codexSessionsDir !== void 0) probeCtx.codexSessionsDir = ctx.codexSessionsDir;
2391
+ const staleness = await probeStaleness({ ctx: probeCtx, paths, nowIso });
2392
+ const result = await renderOrientation2({
2393
+ paths,
2394
+ nowIso,
2395
+ staleness,
2396
+ verbose: options.verbose === true,
2397
+ onWarning: (w, sid) => printReplayWarning(w, sid),
2398
+ onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
2399
+ onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2400
+ });
2401
+ await writeMarkdownFile4(paths.files.orientation, `${result.body}
2402
+ `);
2403
+ if (options.quiet === true) {
2404
+ console.log(
2405
+ `Generated .basou/orientation.md (sessions: ${result.sessionCount}, in-flight tasks: ${result.inFlightTaskCount}, pending approvals: ${result.pendingApprovalsCount}, suspect: ${result.suspectCount})`
2406
+ );
2407
+ } else {
2408
+ console.log(result.body);
2409
+ }
2410
+ }
2411
+ async function assertWorkspaceInitialized7(basouRoot) {
2412
+ try {
2413
+ await assertBasouRootSafe8(basouRoot);
2414
+ } catch (error) {
2415
+ if (findErrorCode7(error, "ENOENT")) {
2416
+ throw new Error("Workspace not initialized. Run 'basou init' first.");
2417
+ }
2418
+ throw error;
2419
+ }
2420
+ }
2421
+
2422
+ // src/commands/project.ts
2423
+ import {
2424
+ existsSync,
2425
+ lstatSync,
2426
+ mkdirSync,
2427
+ readdirSync,
2428
+ readFileSync,
2429
+ readlinkSync,
2430
+ realpathSync,
2431
+ statSync,
2432
+ symlinkSync,
2433
+ unlinkSync,
2434
+ writeFileSync
2435
+ } from "fs";
2436
+ import { basename as basename3, dirname, isAbsolute, join as join4, relative as relative2, resolve as resolve3 } from "path";
2437
+ import {
2438
+ basouPaths as basouPaths9,
2439
+ GENERATED_END,
2440
+ GENERATED_START,
2441
+ isGitNotFound,
2442
+ parseMarkers,
2443
+ pathBasename,
2444
+ planArchive,
2445
+ planGitignore,
2446
+ planRename,
2447
+ planRosterAdoption,
2448
+ planWorkspaceView,
2449
+ readManifest as readManifest5,
2450
+ readMarkdownFile as readMarkdownFile4,
2451
+ reconcileSourceRoots,
2452
+ renderWithMarkers as renderWithMarkers4,
2453
+ safeSimpleGit,
2454
+ summarizePresetPlan,
2455
+ summarizeRosterDrift,
2456
+ summarizeSymlinkPlan,
2457
+ summarizeWiring,
2458
+ unknownManifestKeys,
2459
+ writeManifest as writeManifest2,
2460
+ writeMarkdownFile as writeMarkdownFile5
2461
+ } from "@basou/core";
2462
+ var INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
2463
+ var CANONICAL_FILE = "AGENTS.md";
2464
+ function registerProjectCommand(program) {
2465
+ const project = program.command("project").description("Inspect a project's declared repo roster (read-only)");
2466
+ project.command("check").description(
2467
+ "Compare the declared repo roster (manifest `repos`) against the capture config (`source_roots`) and surface drift (read-only, advisory)"
2468
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2469
+ await runProjectCheck(opts);
2470
+ });
2471
+ project.command("sync").description(
2472
+ "Reconcile the capture config (`source_roots`) to cover every declared repo (manifest `repos`). Dry-run by default; pass --apply to write. Additive only \u2014 it never removes an existing source root (e.g. the workspace view)"
2473
+ ).option(
2474
+ "--apply",
2475
+ "Write the reconciled source_roots to the manifest (default: dry-run preview)"
2476
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2477
+ await runProjectSync(opts);
2478
+ });
2479
+ project.command("adopt").description(
2480
+ "Bootstrap a repo roster (manifest `repos`) from the existing capture config (`source_roots`): classify each by realpath + `.git`, keep the git repos, exclude non-repos (the workspace view, /tmp). Dry-run by default; pass --apply to write (refuses if a roster already exists)"
2481
+ ).option("--apply", "Write the bootstrapped roster to the manifest (default: dry-run preview)").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2482
+ await runProjectAdopt(opts);
2483
+ });
2484
+ project.command("wiring").description(
2485
+ "Inspect each declared repo's agent instruction-file wiring (AGENTS.md, CLAUDE.md, copilot-instructions.md): present? tracked by git? Surfaces privacy risks (a public repo tracking an instruction file) and gaps (read-only, advisory)"
2486
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2487
+ await runProjectWiring(opts);
2488
+ });
2489
+ project.command("gitignore").description(
2490
+ "Reconcile each public-facing repo's .gitignore to exclude the agent instruction files (so the gitignored symlinks never enter public history). Dry-run by default; pass --apply to write. Additive only \u2014 it never removes a line; private repos and unset-visibility repos are left untouched"
2491
+ ).option(
2492
+ "--apply",
2493
+ "Append the missing patterns to each repo's .gitignore (default: dry-run preview)"
2494
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2495
+ await runProjectGitignore(opts);
2496
+ });
2497
+ project.command("symlinks").description(
2498
+ "Generate each declared repo's agent instruction-file symlinks (AGENTS.md, CLAUDE.md, copilot-instructions.md) pointing at the project anchor's canonical (agents/<repo>/AGENTS.md). Dry-run by default; pass --apply to create. Non-destructive \u2014 it only creates missing links and never overwrites an existing file or repoints a link"
2499
+ ).option("--apply", "Create the missing instruction-file symlinks (default: dry-run preview)").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2500
+ await runProjectSymlinks(opts);
2501
+ });
2502
+ project.command("workspace").description(
2503
+ "Generate the project's workspace view: a directory (manifest `workspace.view`) that aggregates every declared repo via a `<repo-basename>` symlink (the anchor included). Dry-run by default; pass --apply to create missing links. Creation is non-destructive \u2014 it never overwrites an existing entry or repoints a link. Stray repo links (a view symlink whose repo is no longer in the roster) are reported always and removed only with --prune; pruning removes ONLY a symlink whose relative target resolves to a git repository (never a real file/dir, the view's own instruction files, a broken link, or a non-repo target), and never the linked repo itself"
2504
+ ).option("--apply", "Create the missing view symlinks (default: dry-run preview)").option(
2505
+ "--prune",
2506
+ "Remove stray repo symlinks (links the roster no longer backs); default: dry-run preview. Independent of --apply"
2507
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2508
+ await runProjectWorkspace(opts);
2509
+ });
2510
+ project.command("preset").description(
2511
+ "Generate the stable-preset block (source visibility, source language, published surfaces) of each declared repo's canonical instruction file (agents/<repo>/AGENTS.md) from the manifest. Dry-run by default; pass --apply to write. Non-destructive \u2014 it only writes the marker-delimited region (creating an absent canonical, updating an out-of-date one) and never touches hand-authored content or a canonical whose markers are missing/malformed"
2512
+ ).option(
2513
+ "--apply",
2514
+ "Write the generated preset block to each canonical (default: dry-run preview)"
2515
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
2516
+ await runProjectPreset(opts);
2517
+ });
2518
+ project.command("archive").argument("<repo>", "The roster repo path to archive (as declared, e.g. ../takuhon)").description(
2519
+ "Fold a repo out of the project: remove it from the declared roster (manifest `repos`) and prune its capture entry (`source_roots`). Dry-run by default; pass --apply to write. Manifest-only and reversible (the manifest is git-tracked); it never deletes the repo, its captured history, or its on-disk wiring (view symlink / instruction symlinks / .gitignore / canonical) \u2014 those are reported as a manual teardown checklist. Archiving the anchor (`.`) is refused"
2520
+ ).option(
2521
+ "--apply",
2522
+ "Write the pruned roster / source_roots to the manifest (default: dry-run preview)"
2523
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
2524
+ await runProjectArchive(repo, opts);
2525
+ });
2526
+ project.command("rename").argument("<old>", "The current roster repo path (as declared, e.g. ../takuhon)").argument("<new>", "The new roster repo path (e.g. ../takuhon-cli)").description(
2527
+ "Re-path a repo in the project: update its declared roster path (manifest `repos`) and its capture entry (`source_roots`). Dry-run by default; pass --apply to write. Manifest-only and reversible (the manifest is git-tracked); it does not move the repo on disk or rewire it \u2014 when the basename changes, the anchor canonical dir and view symlink that still use the old name are reported as a manual checklist (re-run `basou project symlinks` / `workspace` after). Renaming the anchor (`.`) or onto an existing entry is refused"
2528
+ ).option(
2529
+ "--apply",
2530
+ "Write the re-pathed roster / source_roots to the manifest (default: dry-run preview)"
2531
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (oldPath, newPath, opts) => {
2532
+ await runProjectRename(oldPath, newPath, opts);
2533
+ });
2534
+ }
2535
+ async function runProjectCheck(options, ctx = {}) {
2536
+ try {
2537
+ await doRunProjectCheck(options, ctx);
2538
+ } catch (error) {
2539
+ renderCliError(error, { verbose: isVerbose(options) });
2540
+ process.exitCode = 1;
2541
+ }
2542
+ }
2543
+ function effectiveSourceRoots(manifest) {
2544
+ return manifest.import?.source_roots ?? ["."];
2545
+ }
2546
+ function preservedUnknownLines(fields) {
2547
+ if (fields.length === 0) return [];
2548
+ return [
2549
+ `\u2139\uFE0F basou \u304C\u8A8D\u8B58\u3057\u306A\u3044 manifest \u306E\u30C8\u30C3\u30D7\u30EC\u30D9\u30EB\u30D5\u30A3\u30FC\u30EB\u30C9\u3092 ${fields.length} \u4EF6\u4FDD\u6301\u3057\u3066\u3044\u307E\u3059(write \u6642\u3082\u524A\u9664\u3057\u307E\u305B\u3093): ${fields.join(", ")}`,
2550
+ ""
2551
+ ];
2552
+ }
2553
+ async function doRunProjectCheck(options, ctx) {
2554
+ const cwd = ctx.cwd ?? process.cwd();
2555
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
2556
+ const paths = basouPaths9(repositoryRoot);
2557
+ const manifest = await readManifest5(paths);
2558
+ const summary = summarizeRosterDrift({
2559
+ ...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
2560
+ sourceRoots: effectiveSourceRoots(manifest)
2561
+ });
2562
+ if (options.json === true) {
2563
+ console.log(JSON.stringify(summary));
2564
+ } else {
2565
+ console.log(renderProjectCheck(summary));
2566
+ }
2567
+ return summary;
2568
+ }
2569
+ function renderProjectCheck(summary) {
2570
+ const lines = [];
2571
+ lines.push("# \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210\u30C1\u30A7\u30C3\u30AF(\u5BA3\u8A00 vs \u6355\u6349)");
2572
+ lines.push("");
2573
+ if (summary.declaredCount === 0) {
2574
+ lines.push(
2575
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`source_roots` \u306E\u307F\u3067\u904B\u7528\u4E2D\u306E\u305F\u3081\u3001\u5BA3\u8A00\u3068\u306E\u7167\u5408\u306F\u3067\u304D\u307E\u305B\u3093\u3002"
2576
+ );
2577
+ if (summary.extra.length > 0) {
2578
+ lines.push("");
2579
+ lines.push(`\u6355\u6349\u4E2D\u306E source_roots (${summary.extra.length}):`);
2580
+ for (const p of summary.extra) lines.push(`- ${p}`);
2581
+ }
2582
+ return lines.join("\n");
2583
+ }
2584
+ if (summary.gaps.length === 0) {
2585
+ lines.push(
2586
+ `\u2705 \u5BA3\u8A00\u3055\u308C\u305F ${summary.declaredCount} repo \u306F\u3059\u3079\u3066\u6355\u6349\u5BFE\u8C61(source_roots)\u306B\u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002`
2587
+ );
2588
+ } else {
2589
+ lines.push(`\u26A0\uFE0F \u5BA3\u8A00\u3055\u308C\u3066\u3044\u308B\u306E\u306B\u6355\u6349\u5BFE\u8C61\u306B\u7121\u3044 repo: ${summary.gaps.length}(\u53D6\u308A\u3053\u307C\u3057)`);
2590
+ for (const g of summary.gaps) {
2591
+ lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 source_roots \u306B\u672A\u767B\u9332`);
2592
+ }
2593
+ }
2594
+ lines.push("");
2595
+ if (summary.extra.length > 0) {
2596
+ lines.push(
2597
+ `## \u5BA3\u8A00\u5916\u306E\u6355\u6349\u5BFE\u8C61 (${summary.extra.length}) \u2014 workspace view \u304B\u3001\u5BA3\u8A00\u6F0F\u308C\u306E\u53EF\u80FD\u6027`
2598
+ );
2599
+ for (const p of summary.extra) lines.push(`- ${p}`);
2600
+ lines.push("");
2601
+ }
2602
+ lines.push(
2603
+ "\u6CE8: read-only \u306E advisory \u3067\u3059\u3002\u5BA3\u8A00(repos)\u3068\u6355\u6349\u8A2D\u5B9A(source_roots)\u306E\u5DEE\u5206\u306E\u307F\u3092\u8868\u793A\u3057\u3001enforce \u306F\u3057\u307E\u305B\u3093\u3002"
2604
+ );
2605
+ return lines.join("\n");
2606
+ }
2607
+ async function runProjectSync(options, ctx = {}) {
2608
+ try {
2609
+ await doRunProjectSync(options, ctx);
2610
+ } catch (error) {
2611
+ renderCliError(error, { verbose: isVerbose(options) });
2612
+ process.exitCode = 1;
2613
+ }
2614
+ }
2615
+ async function doRunProjectSync(options, ctx) {
2616
+ const cwd = ctx.cwd ?? process.cwd();
2617
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project sync");
2618
+ const paths = basouPaths9(repositoryRoot);
2619
+ const manifest = await readManifest5(paths);
2620
+ const hasRoster = manifest.repos !== void 0 && manifest.repos.length > 0;
2621
+ const reconcile = reconcileSourceRoots({
2622
+ ...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
2623
+ ...manifest.import?.source_roots !== void 0 ? { sourceRoots: manifest.import.source_roots } : {}
2624
+ });
2625
+ const applied = options.apply === true && hasRoster && !reconcile.unchanged;
2626
+ if (applied) {
2627
+ const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
2628
+ await writeManifest2(
2629
+ paths,
2630
+ {
2631
+ ...manifest,
2632
+ import: { ...manifest.import, source_roots: reconcile.next },
2633
+ workspace: { ...manifest.workspace, updated_at: now().toISOString() }
2634
+ },
2635
+ { force: true }
2636
+ );
2637
+ }
2638
+ const result = {
2639
+ ...reconcile,
2640
+ hasRoster,
2641
+ applied,
2642
+ preservedUnknownFields: unknownManifestKeys(manifest)
2643
+ };
2644
+ if (options.json === true) {
2645
+ console.log(JSON.stringify(result));
2646
+ } else {
2647
+ console.log(renderProjectSync(result));
2648
+ }
2649
+ return result;
2650
+ }
2651
+ function renderProjectSync(result) {
2652
+ const lines = [];
2653
+ lines.push("# source_roots \u540C\u671F(\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC \u2192 \u6355\u6349\u8A2D\u5B9A)");
2654
+ lines.push("");
2655
+ lines.push(...preservedUnknownLines(result.preservedUnknownFields));
2656
+ if (!result.hasRoster) {
2657
+ lines.push(
2658
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002\u540C\u671F\u306E\u5143\u306B\u306A\u308B\u5BA3\u8A00\u304C\u7121\u3044\u305F\u3081\u3001\u5909\u66F4\u306F\u3042\u308A\u307E\u305B\u3093\u3002"
2659
+ );
2660
+ return lines.join("\n");
2661
+ }
2662
+ if (result.unchanged) {
2663
+ lines.push("\u2705 source_roots \u306F\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC\u3092\u3059\u3079\u3066\u8986\u3063\u3066\u3044\u307E\u3059(\u540C\u671F\u4E0D\u8981)\u3002");
2664
+ return lines.join("\n");
2665
+ }
2666
+ if (result.applied) {
2667
+ lines.push(`\u2705 source_roots \u306B ${result.added.length} \u4EF6\u8FFD\u52A0\u3057\u307E\u3057\u305F:`);
2668
+ for (const p of result.added) lines.push(`- ${p}`);
2669
+ } else {
2670
+ lines.push(
2671
+ `${result.added.length} \u4EF6\u306E repo \u304C source_roots \u306B\u672A\u767B\u9332\u3067\u3059\u3002\u8FFD\u52A0\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
2672
+ );
2673
+ for (const p of result.added) lines.push(`- ${p}`);
2674
+ lines.push("");
2675
+ lines.push("\u6CE8: \u65E2\u5B58\u306E source_roots \u306F\u4FDD\u6301\u3057\u3001\u4E0D\u8DB3\u5206\u306E\u8FFD\u8A18\u306E\u307F\u884C\u3044\u307E\u3059(\u524A\u9664\u306F\u3057\u307E\u305B\u3093)\u3002");
2676
+ }
2677
+ return lines.join("\n");
2678
+ }
2679
+ async function runProjectAdopt(options, ctx = {}) {
2680
+ try {
2681
+ await doRunProjectAdopt(options, ctx);
2682
+ } catch (error) {
2683
+ renderCliError(error, { verbose: isVerbose(options) });
2684
+ process.exitCode = 1;
2685
+ }
2686
+ }
2687
+ function classifySourceRoot(repositoryRoot, declaredPath) {
2688
+ const absolute = resolve3(repositoryRoot, declaredPath);
2689
+ let real;
2690
+ try {
2691
+ real = realpathSync(absolute);
2692
+ } catch {
2693
+ return { path: declaredPath, kind: "unresolved" };
2694
+ }
2695
+ return { path: declaredPath, kind: existsSync(join4(real, ".git")) ? "repo" : "non-repo" };
2696
+ }
2697
+ async function doRunProjectAdopt(options, ctx) {
2698
+ const cwd = ctx.cwd ?? process.cwd();
2699
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project adopt");
2700
+ const paths = basouPaths9(repositoryRoot);
2701
+ const manifest = await readManifest5(paths);
2702
+ const alreadyDeclared = manifest.repos !== void 0 && manifest.repos.length > 0;
2703
+ const candidates = effectiveSourceRoots(manifest).map(
2704
+ (r) => classifySourceRoot(repositoryRoot, r)
2705
+ );
2706
+ const plan = planRosterAdoption(candidates);
2707
+ const applied = options.apply === true && !alreadyDeclared && plan.repos.length > 0;
2708
+ if (applied) {
2709
+ const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
2710
+ await writeManifest2(
2711
+ paths,
2712
+ {
2713
+ ...manifest,
2714
+ repos: plan.repos,
2715
+ workspace: { ...manifest.workspace, updated_at: now().toISOString() }
2716
+ },
2717
+ { force: true }
2718
+ );
2719
+ }
2720
+ const result = {
2721
+ ...plan,
2722
+ alreadyDeclared,
2723
+ applied,
2724
+ preservedUnknownFields: unknownManifestKeys(manifest)
2725
+ };
2726
+ if (options.json === true) {
2727
+ console.log(JSON.stringify(result));
2728
+ } else {
2729
+ console.log(renderProjectAdopt(result));
2730
+ }
2731
+ return result;
2732
+ }
2733
+ function renderProjectAdopt(result) {
2734
+ const lines = [];
2735
+ lines.push("# repo \u30ED\u30FC\u30B9\u30BF\u30FC\u306E bootstrap(source_roots \u2192 repos)");
2736
+ lines.push("");
2737
+ lines.push(...preservedUnknownLines(result.preservedUnknownFields));
2738
+ if (result.alreadyDeclared) {
2739
+ lines.push(
2740
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC(manifest \u306E `repos`)\u306F\u65E2\u306B\u5BA3\u8A00\u6E08\u307F\u3067\u3059\u3002adopt \u306F\u4E00\u5EA6\u304D\u308A\u306E bootstrap \u306E\u305F\u3081\u4F55\u3082\u66F8\u304D\u8FBC\u307F\u307E\u305B\u3093\u3002\u4EE5\u5F8C\u306E\u4FDD\u5B88\u306F `project check` / `project sync` \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002"
2741
+ );
2742
+ return lines.join("\n");
2743
+ }
2744
+ if (result.repos.length === 0) {
2745
+ lines.push("\u2139\uFE0F source_roots \u306B git repo \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F(bootstrap \u5BFE\u8C61\u306A\u3057)\u3002");
2746
+ } else if (result.applied) {
2747
+ lines.push(`\u2705 ${result.repos.length} repo \u3092 repos \u30ED\u30FC\u30B9\u30BF\u30FC\u306B\u66F8\u304D\u8FBC\u307F\u307E\u3057\u305F:`);
2748
+ for (const r of result.repos) lines.push(`- ${r.path}`);
2749
+ lines.push("");
2750
+ lines.push(
2751
+ "\u6CE8: visibility \u306F\u672A\u8A2D\u5B9A\u3067\u3059\u3002\u5404 repo \u306B public / private / future-public \u3092\u624B\u52D5\u3067\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
2752
+ );
2753
+ } else {
2754
+ lines.push(
2755
+ `${result.repos.length} repo \u3092 repos \u30ED\u30FC\u30B9\u30BF\u30FC\u306B\u5BA3\u8A00\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
2756
+ );
2757
+ for (const r of result.repos) lines.push(`- ${r.path}`);
2758
+ lines.push("");
2759
+ lines.push("\u6CE8: visibility \u306F\u672A\u8A2D\u5B9A\u3067\u63D0\u6848\u3057\u307E\u3059\u3002\u53CD\u6620\u5F8C\u306B\u624B\u52D5\u3067\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
2760
+ }
2761
+ if (result.excluded.length > 0) {
2762
+ lines.push("");
2763
+ lines.push(`## \u9664\u5916 (${result.excluded.length}) \u2014 git repo \u3067\u306F\u306A\u3044\u305F\u3081 repos \u306B\u542B\u3081\u307E\u305B\u3093`);
2764
+ for (const e of result.excluded) {
2765
+ const reason = e.kind === "non-repo" ? "\u975E repo(workspace view / tmp \u7B49)" : "\u89E3\u6C7A\u4E0D\u80FD(\u30D1\u30B9\u304C\u5B58\u5728\u3057\u306A\u3044)";
2766
+ lines.push(`- ${e.path} \u2014 ${reason}`);
2767
+ }
2768
+ }
2769
+ return lines.join("\n");
2770
+ }
2771
+ async function runProjectWiring(options, ctx = {}) {
2772
+ try {
2773
+ await doRunProjectWiring(options, ctx);
2774
+ } catch (error) {
2775
+ renderCliError(error, { verbose: isVerbose(options) });
2776
+ process.exitCode = 1;
2777
+ }
2778
+ }
2779
+ async function isTrackedByGit(repoRoot, relPath) {
2780
+ const out = await safeSimpleGit(repoRoot).raw(["ls-files", "--", relPath]);
2781
+ return out.trim().length > 0;
2782
+ }
2783
+ async function gatherRepoWiring(repositoryRoot, entry) {
2784
+ const base = {
2785
+ path: entry.path,
2786
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
2787
+ };
2788
+ let real;
2789
+ try {
2790
+ real = realpathSync(resolve3(repositoryRoot, entry.path));
2791
+ } catch {
2792
+ return { ...base, reachable: false, instructionFiles: [] };
2793
+ }
2794
+ if (!existsSync(join4(real, ".git"))) {
2795
+ return { ...base, reachable: false, instructionFiles: [] };
2796
+ }
2797
+ try {
2798
+ const instructionFiles = [];
2799
+ for (const name of INSTRUCTION_FILES) {
2800
+ let present = true;
2801
+ try {
2802
+ lstatSync(join4(real, name));
2803
+ } catch {
2804
+ present = false;
2805
+ }
2806
+ instructionFiles.push({ name, present, tracked: await isTrackedByGit(real, name) });
2807
+ }
2808
+ return { ...base, reachable: true, instructionFiles };
2809
+ } catch (error) {
2810
+ if (isGitNotFound(error)) throw error;
2811
+ return { ...base, reachable: false, instructionFiles: [] };
2812
+ }
2813
+ }
2814
+ async function doRunProjectWiring(options, ctx) {
2815
+ const cwd = ctx.cwd ?? process.cwd();
2816
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project wiring");
2817
+ const paths = basouPaths9(repositoryRoot);
2818
+ const manifest = await readManifest5(paths);
2819
+ const roster = manifest.repos ?? [];
2820
+ const facts = [];
2821
+ for (const entry of roster) facts.push(await gatherRepoWiring(repositoryRoot, entry));
2822
+ const summary = summarizeWiring(facts);
2823
+ const result = { ...summary, hasRoster: roster.length > 0 };
2824
+ if (options.json === true) {
2825
+ console.log(JSON.stringify(result));
2826
+ } else {
2827
+ console.log(renderProjectWiring(result));
2828
+ }
2829
+ return result;
2830
+ }
2831
+ function renderProjectWiring(result) {
2832
+ const lines = [];
2833
+ lines.push("# \u6307\u793A\u66F8 wiring \u30C1\u30A7\u30C3\u30AF(\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC \xD7 \u6307\u793A\u66F8\u306E\u5B58\u5728/git \u8FFD\u8DE1)");
2834
+ lines.push("");
2835
+ if (!result.hasRoster) {
2836
+ lines.push(
2837
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
2838
+ );
2839
+ return lines.join("\n");
2840
+ }
2841
+ if (result.risks.length > 0) {
2842
+ lines.push(
2843
+ `\u26A0\uFE0F \u516C\u958B\u7CFB repo \u3067\u6307\u793A\u66F8\u304C git \u8FFD\u8DE1\u3055\u308C\u3066\u3044\u307E\u3059: ${result.risks.length}(canonical \u306E\u6F0F\u6D29\u30EA\u30B9\u30AF)`
2844
+ );
2845
+ for (const r of result.risks) {
2846
+ lines.push(
2847
+ `- ${r.repo} [${r.visibility}] \u2014 ${r.file} \u304C tracked(gitignore \u3055\u308C\u305F symlink \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059)`
2848
+ );
2849
+ }
2850
+ } else if (result.ok) {
2851
+ lines.push("\u2705 \u516C\u958B\u7CFB repo \u3067 git \u8FFD\u8DE1\u3055\u308C\u3066\u3044\u308B\u6307\u793A\u66F8\u306F\u3042\u308A\u307E\u305B\u3093(privacy \u30EA\u30B9\u30AF\u306A\u3057)\u3002");
2852
+ } else {
2853
+ lines.push(
2854
+ "\u2139\uFE0F \u78BA\u5B9A\u3057\u305F privacy \u30EA\u30B9\u30AF\u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u5224\u5B9A\u3067\u304D\u306A\u3044/\u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
2855
+ );
2856
+ }
2857
+ lines.push("");
2858
+ if (result.unknown.length > 0) {
2859
+ lines.push(
2860
+ `## visibility \u672A\u8A2D\u5B9A (${result.unknown.length}) \u2014 privacy \u5224\u5B9A\u4E0D\u53EF\u3002manifest \u306E repos \u306B visibility \u3092\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044`
2861
+ );
2862
+ for (const p of result.unknown) lines.push(`- ${p}`);
2863
+ lines.push("");
2864
+ }
2865
+ if (result.incomplete.length > 0) {
2866
+ lines.push(`## \u6307\u793A\u66F8\u306E\u6B20\u843D (${result.incomplete.length}) \u2014 \u5F8C\u7D9A\u306E\u751F\u6210\u30B9\u30E9\u30A4\u30B9\u3067\u88DC\u5B8C\u4E88\u5B9A`);
2867
+ for (const i of result.incomplete) lines.push(`- ${i.repo} \u2014 ${i.missing.join(", ")}`);
2868
+ lines.push("");
2869
+ }
2870
+ if (result.unreachable.length > 0) {
2871
+ lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
2872
+ for (const p of result.unreachable) lines.push(`- ${p}`);
2873
+ lines.push("");
2874
+ }
2875
+ lines.push(
2876
+ "\u6CE8: read-only \u306E advisory \u3067\u3059\u3002\u6307\u793A\u66F8\u306E\u5B58\u5728\u3068 git \u8FFD\u8DE1\u72B6\u6CC1\u306E\u307F\u3092\u8868\u793A\u3057\u3001\u751F\u6210\u30FBenforce \u306F\u3057\u307E\u305B\u3093(.basou \u306E\u30D5\u30C3\u30C8\u30D7\u30EA\u30F3\u30C8\u306F `basou view --check`)\u3002"
2877
+ );
2878
+ return lines.join("\n");
2879
+ }
2880
+ async function runProjectGitignore(options, ctx = {}) {
2881
+ try {
2882
+ await doRunProjectGitignore(options, ctx);
2883
+ } catch (error) {
2884
+ renderCliError(error, { verbose: isVerbose(options) });
2885
+ process.exitCode = 1;
2886
+ }
2887
+ }
2888
+ function gatherRepoGitignore(repositoryRoot, entry) {
2889
+ const base = {
2890
+ path: entry.path,
2891
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
2892
+ };
2893
+ let real;
2894
+ try {
2895
+ real = realpathSync(resolve3(repositoryRoot, entry.path));
2896
+ } catch {
2897
+ return { ...base, reachable: false, currentLines: [] };
2898
+ }
2899
+ if (!existsSync(join4(real, ".git"))) {
2900
+ return { ...base, reachable: false, currentLines: [] };
2901
+ }
2902
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join4(real, ".gitignore")) };
2903
+ }
2904
+ function hasErrorCode(error) {
2905
+ return error instanceof Error && typeof error.code === "string";
2906
+ }
2907
+ function readGitignoreLines(file) {
2908
+ try {
2909
+ return readFileSync(file, "utf8").split(/\r?\n/);
2910
+ } catch (error) {
2911
+ if (hasErrorCode(error) && error.code === "ENOENT") return [];
2912
+ throw new Error("Failed to read .gitignore", { cause: error });
2913
+ }
2914
+ }
2915
+ function applyGitignorePlan(repositoryRoot, plan) {
2916
+ const file = join4(realpathSync(resolve3(repositoryRoot, plan.path)), ".gitignore");
2917
+ let existing = "";
2918
+ try {
2919
+ existing = readFileSync(file, "utf8");
2920
+ } catch (error) {
2921
+ if (!(hasErrorCode(error) && error.code === "ENOENT")) {
2922
+ throw new Error("Failed to read .gitignore", { cause: error });
2923
+ }
2924
+ }
2925
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2926
+ try {
2927
+ writeFileSync(file, `${existing}${sep}${plan.toAdd.join("\n")}
2928
+ `);
2929
+ } catch (error) {
2930
+ throw new Error("Failed to write .gitignore", { cause: error });
2931
+ }
2932
+ }
2933
+ async function doRunProjectGitignore(options, ctx) {
2934
+ const cwd = ctx.cwd ?? process.cwd();
2935
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project gitignore");
2936
+ const paths = basouPaths9(repositoryRoot);
2937
+ const manifest = await readManifest5(paths);
2938
+ const roster = manifest.repos ?? [];
2939
+ const facts = roster.map((entry) => gatherRepoGitignore(repositoryRoot, entry));
2940
+ const summary = planGitignore({ repos: facts, required: [...INSTRUCTION_FILES] });
2941
+ const applied = options.apply === true && summary.plans.length > 0;
2942
+ if (applied) {
2943
+ for (const plan of summary.plans) applyGitignorePlan(repositoryRoot, plan);
2944
+ }
2945
+ const result = { ...summary, hasRoster: roster.length > 0, applied };
2946
+ if (options.json === true) {
2947
+ console.log(JSON.stringify(result));
2948
+ } else {
2949
+ console.log(renderProjectGitignore(result));
2950
+ }
2951
+ return result;
2952
+ }
2953
+ function renderProjectGitignore(result) {
2954
+ const lines = [];
2955
+ lines.push("# .gitignore \u751F\u6210(\u516C\u958B\u7CFB repo \u306E\u6307\u793A\u66F8\u3092\u9664\u5916)");
2956
+ lines.push("");
2957
+ if (!result.hasRoster) {
2958
+ lines.push(
2959
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
2960
+ );
2961
+ return lines.join("\n");
2962
+ }
2963
+ if (result.plans.length > 0) {
2964
+ const verb = result.applied ? "\u8FFD\u52A0\u3057\u307E\u3057\u305F" : "\u8FFD\u52A0\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply)";
2965
+ lines.push(
2966
+ `${result.applied ? "\u2705 " : ""}${result.plans.length} repo \u306E .gitignore \u306B${verb}:`
2967
+ );
2968
+ for (const p of result.plans) lines.push(`- ${p.path} \u2014 ${p.toAdd.join(", ")}`);
2969
+ } else if (result.ok) {
2970
+ lines.push("\u2705 \u516C\u958B\u7CFB repo \u306E .gitignore \u306F\u6307\u793A\u66F8\u3092\u3059\u3079\u3066\u9664\u5916\u6E08\u307F\u3067\u3059(\u8FFD\u52A0\u4E0D\u8981)\u3002");
2971
+ } else {
2972
+ lines.push(
2973
+ "\u2139\uFE0F \u8FFD\u52A0\u304C\u5FC5\u8981\u306A\u516C\u958B\u7CFB repo \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u5224\u5B9A\u3067\u304D\u306A\u3044/\u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
2974
+ );
2975
+ }
2976
+ lines.push("");
2977
+ if (result.unknown.length > 0) {
2978
+ lines.push(
2979
+ `## visibility \u672A\u8A2D\u5B9A (${result.unknown.length}) \u2014 \u5BFE\u8C61\u5916\u3002manifest \u306E repos \u306B visibility \u3092\u4ED8\u4E0E\u3057\u3066\u304F\u3060\u3055\u3044`
2980
+ );
2981
+ for (const p of result.unknown) lines.push(`- ${p}`);
2982
+ lines.push("");
2983
+ }
2984
+ if (result.unreachable.length > 0) {
2985
+ lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
2986
+ for (const p of result.unreachable) lines.push(`- ${p}`);
2987
+ lines.push("");
2988
+ }
2989
+ lines.push(
2990
+ "\u6CE8: \u65E2\u5B58\u306E .gitignore \u884C\u306F\u4FDD\u6301\u3057\u3001\u4E0D\u8DB3\u30D1\u30BF\u30FC\u30F3\u306E\u8FFD\u8A18\u306E\u307F\u884C\u3044\u307E\u3059(\u524A\u9664\u306F\u3057\u307E\u305B\u3093)\u3002private / visibility \u672A\u8A2D\u5B9A\u306E repo \u306F\u5BFE\u8C61\u5916\u3067\u3059\u3002"
2991
+ );
2992
+ lines.push(
2993
+ "\u6CE8: .gitignore \u3078\u306E\u8FFD\u8A18\u306F\u3001\u65E2\u306B git \u8FFD\u8DE1\u6E08\u307F\u306E\u30D5\u30A1\u30A4\u30EB\u3092 untrack \u3057\u307E\u305B\u3093\u3002\u8FFD\u8DE1\u6E08\u307F\u306E\u6307\u793A\u66F8\u306F `basou project wiring` \u3067\u691C\u51FA\u3057\u3001`git rm --cached <file>` \u3067\u5916\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
2994
+ );
2995
+ return lines.join("\n");
2996
+ }
2997
+ async function runProjectSymlinks(options, ctx = {}) {
2998
+ try {
2999
+ await doRunProjectSymlinks(options, ctx);
3000
+ } catch (error) {
3001
+ renderCliError(error, { verbose: isVerbose(options) });
3002
+ process.exitCode = 1;
3003
+ }
3004
+ }
3005
+ function expectedSymlinkTargets(repoDirReal, canonicalFile) {
3006
+ return [
3007
+ { name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) },
3008
+ { name: "CLAUDE.md", target: CANONICAL_FILE },
3009
+ { name: ".github/copilot-instructions.md", target: `../${CANONICAL_FILE}` }
3010
+ ];
3011
+ }
3012
+ function inspectSymlink(filePath, expectedTarget) {
3013
+ let isLink;
3014
+ try {
3015
+ isLink = lstatSync(filePath).isSymbolicLink();
3016
+ } catch (error) {
3017
+ if (hasErrorCode(error) && error.code === "ENOENT") return { state: "missing" };
3018
+ return { state: "blocked" };
3019
+ }
3020
+ if (!isLink) return { state: "occupied" };
3021
+ const actual = readlinkSync(filePath);
3022
+ return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
3023
+ }
3024
+ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
3025
+ const base = { path: entry.path };
3026
+ let real;
3027
+ try {
3028
+ real = realpathSync(resolve3(repositoryRoot, entry.path));
3029
+ } catch {
3030
+ return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
3031
+ }
3032
+ if (real === anchorReal) {
3033
+ return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
3034
+ }
3035
+ if (!existsSync(join4(real, ".git"))) {
3036
+ return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
3037
+ }
3038
+ const canonicalFile = join4(anchorReal, "agents", basename3(real), CANONICAL_FILE);
3039
+ if (!existsSync(canonicalFile)) {
3040
+ return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
3041
+ }
3042
+ const files = expectedSymlinkTargets(real, canonicalFile).map(
3043
+ (spec) => {
3044
+ const { state, actualTarget } = inspectSymlink(join4(real, spec.name), spec.target);
3045
+ return {
3046
+ name: spec.name,
3047
+ expectedTarget: spec.target,
3048
+ state,
3049
+ ...actualTarget !== void 0 ? { actualTarget } : {}
3050
+ };
3051
+ }
3052
+ );
3053
+ return {
3054
+ ...base,
3055
+ isAnchor: false,
3056
+ reachable: true,
3057
+ canonicalPresent: true,
3058
+ canonicalName: basename3(real),
3059
+ files
3060
+ };
3061
+ }
3062
+ function applySymlinkPlan(repositoryRoot, plan) {
3063
+ let real;
3064
+ try {
3065
+ real = realpathSync(resolve3(repositoryRoot, plan.path));
3066
+ } catch (error) {
3067
+ const message = failureReason(error);
3068
+ return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
3069
+ }
3070
+ const created = [];
3071
+ const failed = [];
3072
+ for (const { name, target } of plan.toCreate) {
3073
+ const filePath = join4(real, name);
3074
+ try {
3075
+ mkdirSync(dirname(filePath), { recursive: true });
3076
+ symlinkSync(target, filePath);
3077
+ created.push(name);
3078
+ } catch (error) {
3079
+ failed.push({ file: name, message: failureReason(error) });
3080
+ }
3081
+ }
3082
+ return { created, failed };
3083
+ }
3084
+ function failureReason(error) {
3085
+ return hasErrorCode(error) ? error.code : "unknown error";
3086
+ }
3087
+ async function doRunProjectSymlinks(options, ctx) {
3088
+ const cwd = ctx.cwd ?? process.cwd();
3089
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
3090
+ const paths = basouPaths9(repositoryRoot);
3091
+ const manifest = await readManifest5(paths);
3092
+ const roster = manifest.repos ?? [];
3093
+ const anchorReal = realpathSync(repositoryRoot);
3094
+ const facts = roster.map((entry) => gatherRepoSymlinks(repositoryRoot, anchorReal, entry));
3095
+ const summary = summarizeSymlinkPlan(facts);
3096
+ const wantApply = options.apply === true && summary.plans.length > 0;
3097
+ const failures = [];
3098
+ let createdCount = 0;
3099
+ if (wantApply) {
3100
+ for (const plan of summary.plans) {
3101
+ const { created, failed } = applySymlinkPlan(repositoryRoot, plan);
3102
+ createdCount += created.length;
3103
+ for (const f of failed) failures.push({ repo: plan.path, file: f.file, message: f.message });
3104
+ }
3105
+ }
3106
+ const result = {
3107
+ ...summary,
3108
+ hasRoster: roster.length > 0,
3109
+ applied: createdCount > 0,
3110
+ failures
3111
+ };
3112
+ if (options.json === true) {
3113
+ console.log(JSON.stringify(result));
3114
+ } else {
3115
+ console.log(renderProjectSymlinks(result));
3116
+ }
3117
+ return result;
3118
+ }
3119
+ function renderProjectSymlinks(result) {
3120
+ const lines = [];
3121
+ lines.push("# \u6307\u793A\u66F8 symlink \u751F\u6210(\u5404 repo \u2192 anchor \u306E canonical)");
3122
+ lines.push("");
3123
+ if (!result.hasRoster) {
3124
+ lines.push(
3125
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3126
+ );
3127
+ return lines.join("\n");
3128
+ }
3129
+ if (result.plans.length > 0) {
3130
+ const attempted = result.applied || result.failures.length > 0;
3131
+ if (!attempted) {
3132
+ lines.push(
3133
+ `${result.plans.length} repo \u306B\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
3134
+ );
3135
+ for (const p of result.plans) {
3136
+ lines.push(`- ${p.path}`);
3137
+ for (const c of p.toCreate) lines.push(` ${c.name} -> ${c.target}`);
3138
+ }
3139
+ } else {
3140
+ const header = result.failures.length === 0 ? "\u2705 \u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F:" : result.applied ? "\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
3141
+ lines.push(header);
3142
+ for (const p of result.plans) {
3143
+ const failedFiles = new Set(
3144
+ result.failures.filter((f) => f.repo === p.path).map((f) => f.file)
3145
+ );
3146
+ const created = p.toCreate.filter((c) => !failedFiles.has(c.name));
3147
+ if (created.length === 0) continue;
3148
+ lines.push(`- ${p.path}`);
3149
+ for (const c of created) lines.push(` ${c.name} -> ${c.target}`);
3150
+ }
3151
+ }
3152
+ } else if (result.ok) {
3153
+ lines.push("\u2705 \u5BA3\u8A00\u3055\u308C\u305F\u5168 repo \u306E\u6307\u793A\u66F8 symlink \u306F\u6B63\u3057\u304F\u5F35\u3089\u308C\u3066\u3044\u307E\u3059(\u751F\u6210\u4E0D\u8981)\u3002");
3154
+ } else {
3155
+ lines.push(
3156
+ "\u2139\uFE0F \u751F\u6210\u304C\u5FC5\u8981\u306A symlink \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u7AF6\u5408 / \u885D\u7A81 / canonical \u4E0D\u5728 / \u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
3157
+ );
3158
+ }
3159
+ lines.push("");
3160
+ if (result.failures.length > 0) {
3161
+ lines.push(`## \u4F5C\u6210\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`);
3162
+ for (const f of result.failures) lines.push(`- ${f.repo} \u2014 ${f.file}: ${f.message}`);
3163
+ lines.push("");
3164
+ }
3165
+ if (result.conflicts.length > 0) {
3166
+ lines.push(
3167
+ `## \u7AF6\u5408 (${result.conflicts.length}) \u2014 \u65E2\u5B58\u3092\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\u3002\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
3168
+ );
3169
+ for (const c of result.conflicts) {
3170
+ const detail = c.reason === "mismatch" ? `\u5225\u306E\u5834\u6240\u3092\u6307\u3059 symlink(\u73FE\u5728: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "symlink \u3067\u306A\u3044\u5B9F\u30D5\u30A1\u30A4\u30EB/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA" : "\u691C\u67FB\u3067\u304D\u306A\u3044\u30D1\u30B9(\u89AA\u304C\u975E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u7B49)";
3171
+ lines.push(`- ${c.repo} \u2014 ${c.file}: ${detail}`);
3172
+ }
3173
+ lines.push("");
3174
+ }
3175
+ if (result.collisions.length > 0) {
3176
+ lines.push(
3177
+ `## canonical \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u540D canonical \u3092\u5171\u6709(\u81EA\u52D5\u914D\u7DDA\u3057\u307E\u305B\u3093)`
3178
+ );
3179
+ for (const c of result.collisions) {
3180
+ lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
3181
+ }
3182
+ lines.push("");
3183
+ }
3184
+ if (result.missingCanonical.length > 0) {
3185
+ lines.push(
3186
+ `## canonical \u4E0D\u5728 (${result.missingCanonical.length}) \u2014 anchor \u306B agents/<repo>/AGENTS.md \u304C\u7121\u3044\u305F\u3081\u751F\u6210\u3067\u304D\u307E\u305B\u3093`
3187
+ );
3188
+ for (const p of result.missingCanonical) lines.push(`- ${p}`);
3189
+ lines.push("");
3190
+ }
3191
+ if (result.unreachable.length > 0) {
3192
+ lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
3193
+ for (const p of result.unreachable) lines.push(`- ${p}`);
3194
+ lines.push("");
3195
+ }
3196
+ lines.push(
3197
+ "\u6CE8: \u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u30FB\u5225\u306E\u5834\u6240\u3092\u6307\u3059 symlink \u306F\u4E0A\u66F8\u304D\u305B\u305A\u3001\u4E0D\u8DB3\u5206\u306E\u4F5C\u6210\u306E\u307F\u884C\u3044\u307E\u3059(GEMINI.md \u306F\u5EC3\u6B62\u306E\u305F\u3081\u751F\u6210\u3057\u307E\u305B\u3093)\u3002"
3198
+ );
3199
+ return lines.join("\n");
3200
+ }
3201
+ async function runProjectWorkspace(options, ctx = {}) {
3202
+ try {
3203
+ await doRunProjectWorkspace(options, ctx);
3204
+ } catch (error) {
3205
+ renderCliError(error, { verbose: isVerbose(options) });
3206
+ process.exitCode = 1;
3207
+ }
3208
+ }
3209
+ function resolveViewDir(repositoryRoot, viewPath) {
3210
+ const abs = resolve3(repositoryRoot, viewPath);
3211
+ try {
3212
+ return realpathSync(abs);
3213
+ } catch {
3214
+ try {
3215
+ return join4(realpathSync(dirname(abs)), basename3(abs));
3216
+ } catch {
3217
+ return abs;
3218
+ }
3219
+ }
3220
+ }
3221
+ function gatherViewRepo(repositoryRoot, viewDir, entry) {
3222
+ let repoReal;
3223
+ try {
3224
+ repoReal = realpathSync(resolve3(repositoryRoot, entry.path));
3225
+ } catch {
3226
+ return { path: entry.path, reachable: false };
3227
+ }
3228
+ const expectedTarget = relative2(viewDir, repoReal);
3229
+ if (expectedTarget === "" || expectedTarget === ".") {
3230
+ return { path: entry.path, reachable: false };
3231
+ }
3232
+ const linkName = basename3(repoReal);
3233
+ const { state, actualTarget } = inspectSymlink(join4(viewDir, linkName), expectedTarget);
3234
+ return {
3235
+ path: entry.path,
3236
+ reachable: true,
3237
+ linkName,
3238
+ expectedTarget,
3239
+ state,
3240
+ ...actualTarget !== void 0 ? { actualTarget } : {}
3241
+ };
3242
+ }
3243
+ function applyViewPlan(viewDir, toCreate) {
3244
+ const created = [];
3245
+ const failed = [];
3246
+ for (const { name, target } of toCreate) {
3247
+ const filePath = join4(viewDir, name);
3248
+ try {
3249
+ mkdirSync(dirname(filePath), { recursive: true });
3250
+ symlinkSync(target, filePath);
3251
+ created.push(name);
3252
+ } catch (error) {
3253
+ failed.push({ name, message: failureReason(error) });
3254
+ }
3255
+ }
3256
+ return { created, failed };
3257
+ }
3258
+ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
3259
+ INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
3260
+ );
3261
+ function classifyViewLink(viewDir, name, rosterRealpaths) {
3262
+ const filePath = join4(viewDir, name);
3263
+ let isLink;
3264
+ try {
3265
+ isLink = lstatSync(filePath).isSymbolicLink();
3266
+ } catch {
3267
+ return null;
3268
+ }
3269
+ if (!isLink) return null;
3270
+ let target;
3271
+ try {
3272
+ target = readlinkSync(filePath);
3273
+ } catch {
3274
+ return null;
3275
+ }
3276
+ const resolved = isAbsolute(target) ? target : resolve3(viewDir, target);
3277
+ try {
3278
+ if (rosterRealpaths.has(realpathSync(resolved))) return null;
3279
+ } catch {
3280
+ }
3281
+ if (isAbsolute(target)) return { target, kind: "absolute" };
3282
+ let isDir = false;
3283
+ try {
3284
+ isDir = statSync(resolved).isDirectory();
3285
+ } catch {
3286
+ isDir = false;
3287
+ }
3288
+ if (!isDir) {
3289
+ return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
3290
+ }
3291
+ return { target, kind: existsSync(join4(resolved, ".git")) ? "repo" : "non-repo" };
3292
+ }
3293
+ function gatherExistingViewLinks(viewDir, rosterRealpaths) {
3294
+ let names;
3295
+ try {
3296
+ names = readdirSync(viewDir);
3297
+ } catch (error) {
3298
+ if (hasErrorCode(error) && error.code === "ENOENT") return [];
3299
+ throw new Error("workspace view \u3092\u8D70\u67FB\u3067\u304D\u307E\u305B\u3093(\u30D1\u30B9/\u7A2E\u5225\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044)", {
3300
+ cause: error
3301
+ });
3302
+ }
3303
+ const links = [];
3304
+ for (const name of names) {
3305
+ if (TOP_LEVEL_INSTRUCTION_FILES_LOWER.has(name.toLowerCase())) continue;
3306
+ const c = classifyViewLink(viewDir, name, rosterRealpaths);
3307
+ if (c === null) continue;
3308
+ links.push({ name, target: c.target, kind: c.kind });
3309
+ }
3310
+ return links;
3311
+ }
3312
+ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
3313
+ const pruned = [];
3314
+ const failed = [];
3315
+ for (const { name } of toPrune) {
3316
+ const filePath = join4(viewDir, name);
3317
+ const c = classifyViewLink(viewDir, name, rosterRealpaths);
3318
+ if (c === null || c.kind !== "repo") {
3319
+ failed.push({
3320
+ name,
3321
+ message: "\u64A4\u53BB\u5BFE\u8C61\u304C scan \u6642\u3068\u5909\u308F\u308A\u307E\u3057\u305F(basou \u751F\u6210\u306E stray repo link \u3067\u306F\u306A\u304F\u306A\u3063\u305F/\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044)"
3322
+ });
3323
+ continue;
3324
+ }
3325
+ try {
3326
+ unlinkSync(filePath);
3327
+ pruned.push(name);
3328
+ } catch (error) {
3329
+ failed.push({ name, message: failureReason(error) });
3330
+ }
3331
+ }
3332
+ return { pruned, failed };
3333
+ }
3334
+ async function doRunProjectWorkspace(options, ctx) {
3335
+ const cwd = ctx.cwd ?? process.cwd();
3336
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project workspace");
3337
+ const paths = basouPaths9(repositoryRoot);
3338
+ const manifest = await readManifest5(paths);
3339
+ const viewPath = manifest.workspace.view;
3340
+ const roster = manifest.repos ?? [];
3341
+ let result;
3342
+ if (viewPath === void 0) {
3343
+ result = {
3344
+ toCreate: [],
3345
+ conflicts: [],
3346
+ collisions: [],
3347
+ unreachable: [],
3348
+ toPrune: [],
3349
+ strayUnknown: [],
3350
+ correctCount: 0,
3351
+ ok: true,
3352
+ hasView: false,
3353
+ applied: false,
3354
+ pruned: false,
3355
+ pruneWithheld: false,
3356
+ failures: [],
3357
+ pruneFailures: []
3358
+ };
3359
+ } else {
3360
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
3361
+ const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
3362
+ const rosterNames = roster.map((entry) => basename3(resolve3(repositoryRoot, entry.path)));
3363
+ const rosterRealpaths = /* @__PURE__ */ new Set();
3364
+ for (const entry of roster) {
3365
+ try {
3366
+ rosterRealpaths.add(realpathSync(resolve3(repositoryRoot, entry.path)));
3367
+ } catch {
3368
+ }
3369
+ }
3370
+ const existing = gatherExistingViewLinks(viewDir, rosterRealpaths);
3371
+ const plan = planWorkspaceView(facts, existing, rosterNames);
3372
+ const failures = [];
3373
+ let createdCount = 0;
3374
+ if (options.apply === true && plan.toCreate.length > 0) {
3375
+ const applied = applyViewPlan(viewDir, plan.toCreate);
3376
+ createdCount = applied.created.length;
3377
+ for (const f of applied.failed) failures.push(f);
3378
+ }
3379
+ const pruneWithheld = options.prune === true && plan.toPrune.length > 0 && plan.unreachable.length > 0;
3380
+ const pruneFailures = [];
3381
+ let prunedCount = 0;
3382
+ if (options.prune === true && plan.toPrune.length > 0 && plan.unreachable.length === 0) {
3383
+ const removed = pruneViewLinks(viewDir, plan.toPrune, rosterRealpaths);
3384
+ prunedCount = removed.pruned.length;
3385
+ for (const f of removed.failed) pruneFailures.push(f);
3386
+ }
3387
+ const createsOutstanding = plan.toCreate.length > 0 && !(options.apply === true && failures.length === 0);
3388
+ const prunesOutstanding = plan.toPrune.length > 0 && !(options.prune === true && !pruneWithheld && pruneFailures.length === 0);
3389
+ const ok = plan.conflicts.length === 0 && plan.collisions.length === 0 && plan.unreachable.length === 0 && plan.strayUnknown.length === 0 && !createsOutstanding && !prunesOutstanding;
3390
+ result = {
3391
+ ...plan,
3392
+ ok,
3393
+ hasView: true,
3394
+ applied: createdCount > 0,
3395
+ pruned: prunedCount > 0,
3396
+ pruneWithheld,
3397
+ failures,
3398
+ pruneFailures
3399
+ };
3400
+ }
3401
+ if (options.json === true) {
3402
+ console.log(JSON.stringify(result));
3403
+ } else {
3404
+ console.log(renderProjectWorkspace(result));
3405
+ }
3406
+ return result;
3407
+ }
3408
+ function renderProjectWorkspace(result) {
3409
+ const lines = [];
3410
+ lines.push("# workspace view \u751F\u6210(roster repo \u3092\u96C6\u7D04)");
3411
+ lines.push("");
3412
+ if (!result.hasView) {
3413
+ lines.push(
3414
+ "\u2139\uFE0F view \u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `workspace.view`)\u3002\u96C6\u7D04\u5148\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3415
+ );
3416
+ return lines.join("\n");
3417
+ }
3418
+ if (result.toCreate.length > 0) {
3419
+ const attempted = result.applied || result.failures.length > 0;
3420
+ if (!attempted) {
3421
+ lines.push(
3422
+ `${result.toCreate.length} \u4EF6\u306E repo symlink \u3092 view \u306B\u4F5C\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
3423
+ );
3424
+ for (const c of result.toCreate) lines.push(` ${c.name} -> ${c.target}`);
3425
+ } else {
3426
+ const failed = new Set(result.failures.map((f) => f.name));
3427
+ const header = result.failures.length === 0 ? "\u2705 view \u306B repo symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F:" : result.applied ? "view \u306B repo symlink \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "view \u306B repo symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
3428
+ lines.push(header);
3429
+ for (const c of result.toCreate) {
3430
+ if (failed.has(c.name)) continue;
3431
+ lines.push(` ${c.name} -> ${c.target}`);
3432
+ }
3433
+ }
3434
+ } else if (result.ok) {
3435
+ lines.push(
3436
+ `\u2705 view \u306F\u5BA3\u8A00\u3055\u308C\u305F roster \u3092\u3059\u3079\u3066\u96C6\u7D04\u3057\u3066\u3044\u307E\u3059(${result.correctCount} links\u3001\u751F\u6210\u4E0D\u8981)\u3002`
3437
+ );
3438
+ } else {
3439
+ lines.push(
3440
+ "\u2139\uFE0F \u4F5C\u6210\u304C\u5FC5\u8981\u306A symlink \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u5BFE\u5FDC\u306E\u5FC5\u8981\u306A\u9805\u76EE\u304C\u3042\u308A\u307E\u3059(stray / \u7AF6\u5408 / \u885D\u7A81 / \u5230\u9054\u3067\u304D\u306A\u3044 repo\u3001\u4E0B\u8A18\u53C2\u7167)\u3002"
3441
+ );
3442
+ }
3443
+ lines.push("");
3444
+ if (result.failures.length > 0) {
3445
+ lines.push(`## \u4F5C\u6210\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E symlink \u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`);
3446
+ for (const f of result.failures) lines.push(`- ${f.name}: ${f.message}`);
3447
+ lines.push("");
3448
+ }
3449
+ if (result.toPrune.length > 0) {
3450
+ const attempted = result.pruned || result.pruneFailures.length > 0;
3451
+ if (result.pruneWithheld) {
3452
+ lines.push(
3453
+ `${result.toPrune.length} \u4EF6\u306E stray repo symlink \u3092\u64A4\u53BB\u4E88\u5B9A\u3067\u3057\u305F\u304C\u3001\u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308B\u305F\u3081\u64A4\u53BB\u3092\u4FDD\u7559\u3057\u307E\u3057\u305F(\u5230\u9054\u3067\u304D\u306A\u3044 repo \u306E link \u3068 stray \u3092\u533A\u5225\u3067\u304D\u306A\u3044\u305F\u3081\u3002\u4E0B\u8A18\u306E repo \u3092\u89E3\u6C7A\u3059\u308B\u304B archive \u3057\u3066\u304B\u3089\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044):`
3454
+ );
3455
+ for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
3456
+ } else if (!attempted) {
3457
+ lines.push(
3458
+ `${result.toPrune.length} \u4EF6\u306E stray repo symlink \u3092\u64A4\u53BB\u4E88\u5B9A(dry-run\u3001\u64A4\u53BB\u3059\u308B\u306B\u306F --prune):`
3459
+ );
3460
+ for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
3461
+ } else {
3462
+ const failed = new Set(result.pruneFailures.map((f) => f.name));
3463
+ const header = result.pruneFailures.length === 0 ? "\u{1F9F9} stray repo symlink \u3092\u64A4\u53BB\u3057\u307E\u3057\u305F:" : result.pruned ? "stray repo symlink \u3092\u64A4\u53BB\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "stray repo symlink \u3092\u64A4\u53BB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
3464
+ lines.push(header);
3465
+ for (const p of result.toPrune) {
3466
+ if (failed.has(p.name)) continue;
3467
+ lines.push(` ${p.name} -> ${p.target}`);
3468
+ }
3469
+ }
3470
+ lines.push("");
3471
+ }
3472
+ if (result.pruneFailures.length > 0) {
3473
+ lines.push(
3474
+ `## \u64A4\u53BB\u306B\u5931\u6557 (${result.pruneFailures.length}) \u2014 \u4E00\u90E8\u306E stray symlink \u3092\u64A4\u53BB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F`
3475
+ );
3476
+ for (const f of result.pruneFailures) lines.push(`- ${f.name}: ${f.message}`);
3477
+ lines.push("");
3478
+ }
3479
+ if (result.conflicts.length > 0) {
3480
+ lines.push(
3481
+ `## \u7AF6\u5408 (${result.conflicts.length}) \u2014 \u65E2\u5B58\u3092\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\u3002\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
3482
+ );
3483
+ for (const c of result.conflicts) {
3484
+ const detail = c.reason === "mismatch" ? `\u5225\u306E\u5834\u6240\u3092\u6307\u3059 symlink(\u73FE\u5728: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "symlink \u3067\u306A\u3044\u5B9F\u30D5\u30A1\u30A4\u30EB/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA" : "\u691C\u67FB\u3067\u304D\u306A\u3044\u30D1\u30B9(\u89AA\u304C\u975E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u7B49)";
3485
+ lines.push(`- ${c.name}: ${detail}`);
3486
+ }
3487
+ lines.push("");
3488
+ }
3489
+ if (result.collisions.length > 0) {
3490
+ lines.push(
3491
+ `## basename \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u3058 view \u540D\u3092\u53D6\u308A\u5408\u3044(\u81EA\u52D5\u914D\u7DDA\u3057\u307E\u305B\u3093)`
3492
+ );
3493
+ for (const c of result.collisions) lines.push(`- ${c.linkName} \u2190 ${c.repos.join(", ")}`);
3494
+ lines.push("");
3495
+ }
3496
+ if (result.unreachable.length > 0) {
3497
+ lines.push(
3498
+ `## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A\u3001\u307E\u305F\u306F view \u81EA\u8EAB\u306B\u89E3\u6C7A\u3059\u308B\u305F\u3081\u96C6\u7D04\u3067\u304D\u307E\u305B\u3093`
3499
+ );
3500
+ for (const p of result.unreachable) lines.push(`- ${p}`);
3501
+ lines.push("");
3502
+ }
3503
+ if (result.strayUnknown.length > 0) {
3504
+ lines.push(
3505
+ `## \u672A\u64A4\u53BB\u306E stray (${result.strayUnknown.length}) \u2014 basou \u751F\u6210\u306E repo link \u3068\u78BA\u8A8D\u3067\u304D\u306A\u3044\u305F\u3081\u64A4\u53BB\u3057\u307E\u305B\u3093\u3002\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
3506
+ );
3507
+ for (const s of result.strayUnknown) {
3508
+ const detail = s.reason === "broken" ? "\u30EA\u30F3\u30AF\u5207\u308C(\u30BF\u30FC\u30B2\u30C3\u30C8\u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093)" : s.reason === "non-repo" ? "git repo \u3067\u306A\u3044\u30BF\u30FC\u30B2\u30C3\u30C8(\u30D5\u30A1\u30A4\u30EB\u3001\u307E\u305F\u306F .git \u306E\u7121\u3044\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA)" : "\u7D76\u5BFE\u30D1\u30B9\u306E\u30BF\u30FC\u30B2\u30C3\u30C8(basou \u306F\u76F8\u5BFE\u30EA\u30F3\u30AF\u306E\u307F\u751F\u6210\u3057\u307E\u3059)";
3509
+ lines.push(`- ${s.name} -> ${s.target}: ${detail}`);
3510
+ }
3511
+ lines.push("");
3512
+ }
3513
+ lines.push(
3514
+ "\u6CE8: \u4F5C\u6210(--apply)\u306F\u65E2\u5B58\u30A8\u30F3\u30C8\u30EA\u3092\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\u3002stray repo link \u306E\u64A4\u53BB\u306F --prune \u3067\u884C\u3044\u307E\u3059(symlink \u306E\u307F\u524A\u9664\u3057\u3001\u53C2\u7167\u5148 repo \u306F\u524A\u9664\u3057\u307E\u305B\u3093)\u3002basou \u751F\u6210\u3068\u78BA\u8A8D\u3067\u304D\u306A\u3044 stray(\u30EA\u30F3\u30AF\u5207\u308C / \u975E repo / \u7D76\u5BFE\u30D1\u30B9)\u306F\u64A4\u53BB\u3057\u307E\u305B\u3093\u3002"
3515
+ );
3516
+ return lines.join("\n");
3517
+ }
3518
+ async function runProjectPreset(options, ctx = {}) {
3519
+ try {
3520
+ await doRunProjectPreset(options, ctx);
3521
+ } catch (error) {
3522
+ renderCliError(error, { verbose: isVerbose(options) });
3523
+ process.exitCode = 1;
3524
+ }
3525
+ }
3526
+ function canonicalFileFor(anchorReal, canonicalName) {
3527
+ return join4(anchorReal, "agents", canonicalName, CANONICAL_FILE);
3528
+ }
3529
+ function canonicalLabelFor(canonicalName) {
3530
+ return join4("agents", canonicalName, CANONICAL_FILE);
3531
+ }
3532
+ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
3533
+ const declared = {
3534
+ path: entry.path,
3535
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
3536
+ ...entry.language !== void 0 ? { language: entry.language } : {},
3537
+ ...entry.publishes !== void 0 ? { publishes: entry.publishes } : {}
3538
+ };
3539
+ let real;
3540
+ try {
3541
+ real = realpathSync(resolve3(repositoryRoot, entry.path));
3542
+ } catch {
3543
+ return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
3544
+ }
3545
+ if (real === anchorReal) {
3546
+ return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
3547
+ }
3548
+ if (!existsSync(join4(real, ".git"))) {
3549
+ return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
3550
+ }
3551
+ const canonicalName = basename3(real);
3552
+ let content;
3553
+ try {
3554
+ content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
3555
+ } catch {
3556
+ return {
3557
+ ...declared,
3558
+ isAnchor: false,
3559
+ reachable: true,
3560
+ canonicalName,
3561
+ canonicalPresent: true,
3562
+ canonicalReadable: false
3563
+ };
3564
+ }
3565
+ if (content === null) {
3566
+ return {
3567
+ ...declared,
3568
+ isAnchor: false,
3569
+ reachable: true,
3570
+ canonicalName,
3571
+ canonicalPresent: false
3572
+ };
3573
+ }
3574
+ const section = parseMarkers(content);
3575
+ return {
3576
+ ...declared,
3577
+ isAnchor: false,
3578
+ reachable: true,
3579
+ canonicalName,
3580
+ canonicalPresent: true,
3581
+ canonicalReadable: true,
3582
+ markerKind: section.kind,
3583
+ ...section.kind === "ok" ? { currentBlock: section.generated } : {}
3584
+ };
3585
+ }
3586
+ async function applyPresetPlan(anchorReal, plan) {
3587
+ const file = canonicalFileFor(anchorReal, plan.canonicalName);
3588
+ const label = canonicalLabelFor(plan.canonicalName);
3589
+ let isLink = false;
3590
+ try {
3591
+ isLink = lstatSync(file).isSymbolicLink();
3592
+ } catch {
3593
+ isLink = false;
3594
+ }
3595
+ if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
3596
+ if (plan.action === "create") mkdirSync(dirname(file), { recursive: true });
3597
+ const existing = await readMarkdownFile4(file);
3598
+ await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
3599
+ }
3600
+ function presetFailureReason(error) {
3601
+ if (error instanceof Error && (error.message.startsWith("Markers") || error.message.startsWith("Canonical"))) {
3602
+ return error.message;
3603
+ }
3604
+ const cause = error instanceof Error ? error.cause : void 0;
3605
+ if (hasErrorCode(cause)) return cause.code;
3606
+ if (hasErrorCode(error)) return error.code;
3607
+ return "unknown error";
3608
+ }
3609
+ async function doRunProjectPreset(options, ctx) {
3610
+ const cwd = ctx.cwd ?? process.cwd();
3611
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project preset");
3612
+ const paths = basouPaths9(repositoryRoot);
3613
+ const manifest = await readManifest5(paths);
3614
+ const roster = manifest.repos ?? [];
3615
+ const anchorReal = realpathSync(repositoryRoot);
3616
+ const facts = [];
3617
+ for (const entry of roster) facts.push(await gatherRepoPreset(repositoryRoot, anchorReal, entry));
3618
+ const summary = summarizePresetPlan(facts);
3619
+ const failures = [];
3620
+ let writtenCount = 0;
3621
+ if (options.apply === true && summary.plans.length > 0) {
3622
+ for (const plan of summary.plans) {
3623
+ try {
3624
+ await applyPresetPlan(anchorReal, plan);
3625
+ writtenCount += 1;
3626
+ } catch (error) {
3627
+ failures.push({ repo: plan.path, message: presetFailureReason(error) });
3628
+ }
3629
+ }
3630
+ }
3631
+ const result = {
3632
+ ...summary,
3633
+ hasRoster: roster.length > 0,
3634
+ applied: writtenCount > 0,
3635
+ failures
3636
+ };
3637
+ if (options.json === true) {
3638
+ console.log(JSON.stringify(result));
3639
+ } else {
3640
+ console.log(renderProjectPreset(result));
3641
+ }
3642
+ return result;
3643
+ }
3644
+ function presetActionLabel(action) {
3645
+ return action === "create" ? "\u65B0\u898F\u4F5C\u6210" : "\u66F4\u65B0";
3646
+ }
3647
+ function renderProjectPreset(result) {
3648
+ const lines = [];
3649
+ lines.push("# \u6307\u793A\u66F8 A \u30D7\u30EA\u30BB\u30C3\u30C8\u751F\u6210(\u5BA3\u8A00 \u2192 canonical \u306E\u751F\u6210\u9818\u57DF)");
3650
+ lines.push("");
3651
+ if (!result.hasRoster) {
3652
+ lines.push(
3653
+ "\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002`basou project adopt` \u3067\u5BA3\u8A00\u3057\u3066\u304B\u3089\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3654
+ );
3655
+ return lines.join("\n");
3656
+ }
3657
+ if (result.plans.length > 0) {
3658
+ const attempted = result.applied || result.failures.length > 0;
3659
+ if (!attempted) {
3660
+ lines.push(
3661
+ `${result.plans.length} repo \u306E canonical \u306B A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
3662
+ );
3663
+ for (const p of result.plans) {
3664
+ lines.push(
3665
+ `- ${p.path} [${presetActionLabel(p.action)}] \u2192 ${canonicalLabelFor(p.canonicalName)}`
3666
+ );
3667
+ for (const bl of p.desiredBlock.split("\n")) lines.push(` ${bl}`);
3668
+ }
3669
+ } else {
3670
+ const failed = new Set(result.failures.map((f) => f.repo));
3671
+ const header = result.failures.length === 0 ? "\u2705 canonical \u306B A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u3057\u307E\u3057\u305F:" : result.applied ? "A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u3057\u307E\u3057\u305F(\u4E00\u90E8\u5931\u6557\u3001\u4E0B\u8A18\u53C2\u7167):" : "A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F(\u4E0B\u8A18\u53C2\u7167):";
3672
+ lines.push(header);
3673
+ for (const p of result.plans) {
3674
+ if (failed.has(p.path)) continue;
3675
+ lines.push(
3676
+ `- ${p.path} [${presetActionLabel(p.action)}] \u2192 ${canonicalLabelFor(p.canonicalName)}`
3677
+ );
3678
+ }
3679
+ }
3680
+ } else if (result.ok) {
3681
+ lines.push("\u2705 \u5BA3\u8A00\u3055\u308C\u305F\u5168 repo \u306E A \u30D7\u30EA\u30BB\u30C3\u30C8\u306F canonical \u3068\u540C\u671F\u6E08\u307F\u3067\u3059(\u751F\u6210\u4E0D\u8981)\u3002");
3682
+ } else {
3683
+ lines.push(
3684
+ "\u2139\uFE0F \u751F\u6210\u304C\u5FC5\u8981\u306A repo \u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u30DE\u30FC\u30AB\u30FC\u7AF6\u5408 / \u885D\u7A81 / \u672A\u5BA3\u8A00 / \u5230\u9054\u3067\u304D\u306A\u3044 repo \u304C\u3042\u308A\u307E\u3059(\u4E0B\u8A18\u53C2\u7167)\u3002"
3685
+ );
3686
+ }
3687
+ lines.push("");
3688
+ if (result.inSync.length > 0) {
3689
+ lines.push(`\u540C\u671F\u6E08\u307F (${result.inSync.length}): ${result.inSync.join(", ")}`);
3690
+ lines.push("");
3691
+ }
3692
+ if (result.failures.length > 0) {
3693
+ lines.push(
3694
+ `## \u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E canonical \u3092\u66F8\u3051\u307E\u305B\u3093\u3067\u3057\u305F`
3695
+ );
3696
+ for (const f of result.failures) lines.push(`- ${f.repo}: ${f.message}`);
3697
+ lines.push("");
3698
+ }
3699
+ if (result.markerConflicts.length > 0) {
3700
+ lines.push(
3701
+ `## \u30DE\u30FC\u30AB\u30FC\u7AF6\u5408 (${result.markerConflicts.length}) \u2014 canonical \u306E\u30DE\u30FC\u30AB\u30FC\u304C\u7121\u3044/\u58CA\u308C\u3066\u3044\u308B\u305F\u3081\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093`
3702
+ );
3703
+ for (const c of result.markerConflicts) {
3704
+ const detail = c.reason === "no_markers" ? "\u30DE\u30FC\u30AB\u30FC\u9818\u57DF\u304C\u7121\u3044" : `\u30DE\u30FC\u30AB\u30FC\u4E0D\u6574\u5408(${c.reason})`;
3705
+ lines.push(`- ${c.repo}: ${detail}`);
3706
+ }
3707
+ lines.push(
3708
+ ` \u5BFE\u51E6: A \u30D7\u30EA\u30BB\u30C3\u30C8\u3092\u5165\u308C\u305F\u3044\u4F4D\u7F6E\u306B\u6B21\u306E2\u884C\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044 \u2014 \`${GENERATED_START}\` \u3068 \`${GENERATED_END}\`(\u7121\u3051\u308C\u3070 basou \u304C\u65B0\u898F canonical \u3092\u4F5C\u308A\u307E\u3059)\u3002`
3709
+ );
3710
+ lines.push("");
3711
+ }
3712
+ if (result.unreadable.length > 0) {
3713
+ lines.push(
3714
+ `## canonical \u8AAD\u307F\u53D6\u308A\u4E0D\u80FD (${result.unreadable.length}) \u2014 \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA/\u6A29\u9650\u7B49\u3067\u8AAD\u3081\u307E\u305B\u3093`
3715
+ );
3716
+ for (const p of result.unreadable) lines.push(`- ${p}`);
3717
+ lines.push("");
3718
+ }
3719
+ if (result.collisions.length > 0) {
3720
+ lines.push(
3721
+ `## canonical \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u540D canonical \u3092\u5171\u6709(\u81EA\u52D5\u751F\u6210\u3057\u307E\u305B\u3093)`
3722
+ );
3723
+ for (const c of result.collisions) {
3724
+ lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
3725
+ }
3726
+ lines.push("");
3727
+ }
3728
+ if (result.undeclared.length > 0) {
3729
+ lines.push(
3730
+ `## \u5BA3\u8A00\u306A\u3057 (${result.undeclared.length}) \u2014 visibility / language / publishes \u304C\u672A\u8A2D\u5B9A\u306E\u305F\u3081\u751F\u6210\u3057\u307E\u305B\u3093`
3731
+ );
3732
+ for (const p of result.undeclared) lines.push(`- ${p}`);
3733
+ lines.push("");
3734
+ }
3735
+ if (result.anchors.length > 0) {
3736
+ lines.push(
3737
+ `## anchor (${result.anchors.length}) \u2014 \u81EA\u8EAB\u306E AGENTS.md \u306F\u624B\u3067\u7DAD\u6301\u3059\u308B\u305F\u3081\u30B9\u30AD\u30C3\u30D7`
3738
+ );
3739
+ for (const p of result.anchors) lines.push(`- ${p}`);
3740
+ lines.push("");
3741
+ }
3742
+ if (result.unreachable.length > 0) {
3743
+ lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
3744
+ for (const p of result.unreachable) lines.push(`- ${p}`);
3745
+ lines.push("");
3746
+ }
3747
+ lines.push(
3748
+ "\u6CE8: \u30DE\u30FC\u30AB\u30FC\u9818\u57DF\u306E\u307F\u3092\u751F\u6210\u3057\u3001canonical \u306E\u624B\u66F8\u304D\u90E8\u5206(\u30DE\u30FC\u30AB\u30FC\u5916)\u306F\u4FDD\u6301\u3057\u307E\u3059\u3002\u751F\u6210\u5185\u5BB9\u306F manifest \u306E\u5BA3\u8A00\u304B\u3089\u5C0E\u51FA\u3055\u308C\u307E\u3059\u3002"
3749
+ );
3750
+ return lines.join("\n");
3751
+ }
3752
+ async function runProjectArchive(target, options, ctx = {}) {
3753
+ try {
3754
+ await doRunProjectArchive(target, options, ctx);
3755
+ } catch (error) {
3756
+ renderCliError(error, { verbose: isVerbose(options) });
3757
+ process.exitCode = 1;
3758
+ }
3759
+ }
3760
+ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
3761
+ const empty = {
3762
+ inspected: false,
3763
+ viewLink: false,
3764
+ instructionFiles: [],
3765
+ gitignorePatterns: [],
3766
+ canonical: false
3767
+ };
3768
+ let real;
3769
+ try {
3770
+ real = realpathSync(resolve3(repositoryRoot, target));
3771
+ } catch {
3772
+ return empty;
3773
+ }
3774
+ const anchorReal = realpathSync(repositoryRoot);
3775
+ const canonicalName = basename3(real);
3776
+ const instructionFiles = [];
3777
+ for (const name of INSTRUCTION_FILES) {
3778
+ try {
3779
+ lstatSync(join4(real, name));
3780
+ instructionFiles.push(name);
3781
+ } catch {
3782
+ }
3783
+ }
3784
+ let ignored;
3785
+ try {
3786
+ ignored = new Set(readGitignoreLines(join4(real, ".gitignore")).map((l) => l.trim()));
3787
+ } catch {
3788
+ ignored = /* @__PURE__ */ new Set();
3789
+ }
3790
+ const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
3791
+ const canonical2 = existsSync(join4(anchorReal, "agents", canonicalName, CANONICAL_FILE));
3792
+ let viewLink = false;
3793
+ const viewPath = manifest.workspace.view;
3794
+ if (viewPath !== void 0) {
3795
+ try {
3796
+ lstatSync(join4(resolveViewDir(repositoryRoot, viewPath), canonicalName));
3797
+ viewLink = true;
3798
+ } catch {
3799
+ }
3800
+ }
3801
+ return {
3802
+ inspected: true,
3803
+ viewLink,
3804
+ instructionFiles,
3805
+ gitignorePatterns: [...gitignorePatterns],
3806
+ canonical: canonical2
3807
+ };
3808
+ }
3809
+ function omitKey(obj, key) {
3810
+ const clone = { ...obj };
3811
+ delete clone[key];
3812
+ return clone;
3813
+ }
3814
+ function buildArchivedManifest(manifest, plan, updatedAt) {
3815
+ let next = { ...manifest, workspace: { ...manifest.workspace, updated_at: updatedAt } };
3816
+ next = plan.reposEmptied ? omitKey(next, "repos") : { ...next, repos: plan.nextRepos };
3817
+ if (plan.nextSourceRoots !== void 0) {
3818
+ if (plan.nextSourceRoots.length === 0) {
3819
+ const prunedImport = manifest.import !== void 0 ? omitKey(manifest.import, "source_roots") : {};
3820
+ next = Object.keys(prunedImport).length === 0 ? omitKey(next, "import") : { ...next, import: prunedImport };
3821
+ } else {
3822
+ next = {
3823
+ ...next,
3824
+ import: { ...manifest.import ?? {}, source_roots: plan.nextSourceRoots }
3825
+ };
3826
+ }
3827
+ }
3828
+ return next;
3829
+ }
3830
+ async function doRunProjectArchive(target, options, ctx) {
3831
+ const cwd = ctx.cwd ?? process.cwd();
3832
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project archive");
3833
+ const paths = basouPaths9(repositoryRoot);
3834
+ const manifest = await readManifest5(paths);
3835
+ const roster = manifest.repos ?? [];
3836
+ let targetIsAnchor = false;
3837
+ try {
3838
+ targetIsAnchor = realpathSync(resolve3(repositoryRoot, target)) === realpathSync(repositoryRoot);
3839
+ } catch {
3840
+ targetIsAnchor = false;
3841
+ }
3842
+ const plan = planArchive({
3843
+ ...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
3844
+ ...manifest.import?.source_roots !== void 0 ? { sourceRoots: manifest.import.source_roots } : {},
3845
+ target,
3846
+ targetIsAnchor
3847
+ });
3848
+ const teardown = plan.found && !plan.isAnchor ? gatherArchiveTeardown(repositoryRoot, manifest, target) : {
3849
+ inspected: false,
3850
+ viewLink: false,
3851
+ instructionFiles: [],
3852
+ gitignorePatterns: [],
3853
+ canonical: false
3854
+ };
3855
+ const applied = options.apply === true && plan.found && !plan.isAnchor;
3856
+ if (applied) {
3857
+ const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
3858
+ await writeManifest2(paths, buildArchivedManifest(manifest, plan, now().toISOString()), {
3859
+ force: true
3860
+ });
3861
+ }
3862
+ const result = {
3863
+ ...plan,
3864
+ hasRoster: roster.length > 0,
3865
+ applied,
3866
+ teardown,
3867
+ preservedUnknownFields: unknownManifestKeys(manifest)
3868
+ };
3869
+ if (options.json === true) {
3870
+ console.log(JSON.stringify(result));
3871
+ } else {
3872
+ console.log(renderProjectArchive(result));
3873
+ }
3874
+ return result;
3875
+ }
3876
+ function renderProjectArchive(result) {
3877
+ const lines = [];
3878
+ lines.push("# repo \u306E archive(roster \u304B\u3089\u7573\u3080)");
3879
+ lines.push("");
3880
+ lines.push(...preservedUnknownLines(result.preservedUnknownFields));
3881
+ if (!result.hasRoster) {
3882
+ lines.push("\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002archive \u5BFE\u8C61\u304C\u3042\u308A\u307E\u305B\u3093\u3002");
3883
+ return lines.join("\n");
3884
+ }
3885
+ if (result.isAnchor) {
3886
+ lines.push(
3887
+ `\u26A0\uFE0F \`${result.target}\` \u306F anchor(\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E root)\u3067\u3059\u3002anchor \u306F archive \u3067\u304D\u307E\u305B\u3093(manifest \u306E\u5BB6\u306E\u305F\u3081)\u3002`
3888
+ );
3889
+ return lines.join("\n");
3890
+ }
3891
+ if (!result.found) {
3892
+ lines.push(`\u2139\uFE0F \`${result.target}\` \u306F roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093(archive \u5BFE\u8C61\u306A\u3057)\u3002`);
3893
+ return lines.join("\n");
3894
+ }
3895
+ if (result.applied) {
3896
+ lines.push(`\u2705 \`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u3057\u307E\u3057\u305F\u3002`);
3897
+ } else {
3898
+ lines.push(`\`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`);
3899
+ }
3900
+ if (result.sourceRootRemoval !== void 0) {
3901
+ lines.push(
3902
+ `- source_roots \u304B\u3089 ${result.sourceRootRemoval} \u3092 prune${result.applied ? "\u3057\u307E\u3057\u305F" : "\u3057\u307E\u3059"}(\u4EE5\u5F8C refresh \u306E\u5BFE\u8C61\u5916)\u3002`
3903
+ );
3904
+ } else {
3905
+ lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(prune \u4E0D\u8981)\u3002");
3906
+ }
3907
+ if (result.reposEmptied) {
3908
+ lines.push(
3909
+ "- \u3053\u308C\u304C\u6700\u5F8C\u306E\u30E1\u30F3\u30D0\u30FC\u3067\u3059 \u2192 roster \u306F\u7A7A\u306B\u306A\u308A `repos` \u5BA3\u8A00\u306F\u9664\u53BB\u3055\u308C\u307E\u3059(\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u7573\u3080)\u3002"
3910
+ );
3911
+ } else if (result.becomesSolo) {
3912
+ lines.push(
3913
+ "- \u6B8B\u308A 1 repo(solo)\u306B\u306A\u308A\u307E\u3059 \u2192 workspace view \u306F\u4E0D\u8981\u3067\u3059(view \u5BA3\u8A00/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u64A4\u53BB\u3092\u691C\u8A0E)\u3002"
3914
+ );
3915
+ }
3916
+ lines.push("");
3917
+ const t = result.teardown;
3918
+ const items = [];
3919
+ if (t.viewLink) items.push("workspace view \u306E symlink \u30A8\u30F3\u30C8\u30EA");
3920
+ if (t.instructionFiles.length > 0) items.push(`\u6307\u793A\u66F8(${t.instructionFiles.join(", ")})`);
3921
+ if (t.gitignorePatterns.length > 0)
3922
+ items.push(`.gitignore \u306E\u6307\u793A\u66F8\u30D1\u30BF\u30FC\u30F3(${t.gitignorePatterns.join(", ")})`);
3923
+ if (t.canonical) items.push(`anchor \u306E canonical(agents/${basename3(result.target)}/AGENTS.md)`);
3924
+ if (!t.inspected) {
3925
+ lines.push("## \u624B\u52D5 teardown(repo \u304C\u30C7\u30A3\u30B9\u30AF\u4E0A\u306B\u89E3\u6C7A\u3067\u304D\u306A\u3044\u305F\u3081\u672A\u691C\u67FB)");
3926
+ lines.push(
3927
+ "- repo \u306F\u65E2\u306B\u524A\u9664\u6E08\u307F\u306E\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002view symlink / \u6307\u793A\u66F8 symlink / .gitignore / canonical \u304C\u6B8B\u3063\u3066\u3044\u306A\u3044\u304B\u624B\u52D5\u3067\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3928
+ );
3929
+ lines.push("");
3930
+ } else if (items.length > 0) {
3931
+ lines.push("## \u624B\u52D5 teardown(--apply \u306F\u89E6\u308C\u307E\u305B\u3093\u3002\u6B8B\u3063\u3066\u3044\u308B wiring \u3092\u624B\u3067\u64A4\u53BB\u3057\u3066\u304F\u3060\u3055\u3044)");
3932
+ for (const i of items) lines.push(`- ${i}`);
3933
+ lines.push("");
3934
+ } else {
3935
+ lines.push("repo \u5074\u306E wiring(view/\u6307\u793A\u66F8/.gitignore/canonical)\u306F\u6B8B\u3063\u3066\u3044\u307E\u305B\u3093\u3002");
3936
+ lines.push("");
3937
+ }
3938
+ lines.push(
3939
+ "\u6CE8: archive \u306F manifest(.basou\u3001git \u8FFD\u8DE1=\u53EF\u9006)\u306E\u307F\u3092\u5909\u66F4\u3057\u307E\u3059\u3002repo\u30FB\u6355\u6349\u5C65\u6B74\u30FBon-disk \u306E wiring \u306F\u524A\u9664\u3057\u307E\u305B\u3093\u3002"
3940
+ );
3941
+ return lines.join("\n");
3942
+ }
3943
+ async function runProjectRename(oldPath, newPath, options, ctx = {}) {
3944
+ try {
3945
+ await doRunProjectRename(oldPath, newPath, options, ctx);
3946
+ } catch (error) {
3947
+ renderCliError(error, { verbose: isVerbose(options) });
3948
+ process.exitCode = 1;
3949
+ }
3950
+ }
3951
+ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
3952
+ let anchorReal;
3953
+ try {
3954
+ anchorReal = realpathSync(repositoryRoot);
3955
+ } catch {
3956
+ return { canonicalDirOld: false, viewLinkOld: false };
3957
+ }
3958
+ const canonicalDirOld = existsSync(join4(anchorReal, "agents", oldBasename));
3959
+ let viewLinkOld = false;
3960
+ const viewPath = manifest.workspace.view;
3961
+ if (viewPath !== void 0) {
3962
+ try {
3963
+ lstatSync(join4(resolveViewDir(repositoryRoot, viewPath), oldBasename));
3964
+ viewLinkOld = true;
3965
+ } catch {
3966
+ }
3967
+ }
3968
+ return { canonicalDirOld, viewLinkOld };
3969
+ }
3970
+ function buildRenamedManifest(manifest, plan, updatedAt) {
3971
+ const next = {
3972
+ ...manifest,
3973
+ workspace: { ...manifest.workspace, updated_at: updatedAt },
3974
+ repos: plan.nextRepos
3975
+ };
3976
+ if (plan.nextSourceRoots !== void 0) {
3977
+ return { ...next, import: { ...manifest.import ?? {}, source_roots: plan.nextSourceRoots } };
3978
+ }
3979
+ return next;
3980
+ }
3981
+ async function doRunProjectRename(oldPath, newPath, options, ctx) {
3982
+ const cwd = ctx.cwd ?? process.cwd();
3983
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project rename");
3984
+ const paths = basouPaths9(repositoryRoot);
3985
+ const manifest = await readManifest5(paths);
3986
+ const roster = manifest.repos ?? [];
3987
+ let oldIsAnchor = false;
3988
+ try {
3989
+ oldIsAnchor = realpathSync(resolve3(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
3990
+ } catch {
3991
+ oldIsAnchor = false;
3992
+ }
3993
+ const plan = planRename({
3994
+ ...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
3995
+ ...manifest.import?.source_roots !== void 0 ? { sourceRoots: manifest.import.source_roots } : {},
3996
+ oldPath,
3997
+ newPath,
3998
+ oldIsAnchor
3999
+ });
4000
+ const actionable = plan.found && !plan.isAnchor && !plan.collision && !plan.noop;
4001
+ const wiring = actionable && plan.basenameChanged ? gatherRenameWiring(repositoryRoot, manifest, pathBasename(plan.oldTarget)) : { canonicalDirOld: false, viewLinkOld: false };
4002
+ const applied = options.apply === true && actionable;
4003
+ if (applied) {
4004
+ const now = ctx.now ?? (() => /* @__PURE__ */ new Date());
4005
+ await writeManifest2(paths, buildRenamedManifest(manifest, plan, now().toISOString()), {
4006
+ force: true
4007
+ });
4008
+ }
4009
+ const result = {
4010
+ ...plan,
4011
+ hasRoster: roster.length > 0,
4012
+ applied,
4013
+ wiring,
4014
+ preservedUnknownFields: unknownManifestKeys(manifest)
4015
+ };
4016
+ if (options.json === true) {
4017
+ console.log(JSON.stringify(result));
4018
+ } else {
4019
+ console.log(renderProjectRename(result));
4020
+ }
4021
+ return result;
4022
+ }
4023
+ function renderProjectRename(result) {
4024
+ const lines = [];
4025
+ lines.push("# repo \u306E rename(roster \u306E\u30D1\u30B9\u66F4\u65B0)");
4026
+ lines.push("");
4027
+ lines.push(...preservedUnknownLines(result.preservedUnknownFields));
4028
+ if (!result.hasRoster) {
4029
+ lines.push("\u2139\uFE0F repo \u30ED\u30FC\u30B9\u30BF\u30FC\u304C\u672A\u5BA3\u8A00\u3067\u3059(manifest \u306E `repos`)\u3002rename \u5BFE\u8C61\u304C\u3042\u308A\u307E\u305B\u3093\u3002");
4030
+ return lines.join("\n");
4031
+ }
4032
+ if (result.noop) {
4033
+ lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u3068 \`${result.newTarget}\` \u306F\u540C\u4E00\u3067\u3059(\u5909\u66F4\u306A\u3057)\u3002`);
4034
+ return lines.join("\n");
4035
+ }
4036
+ if (result.isAnchor) {
4037
+ lines.push(
4038
+ `\u26A0\uFE0F \`${result.oldTarget}\` \u306F anchor(\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E root)\u3067\u3059\u3002anchor \u306F rename \u3067\u304D\u307E\u305B\u3093\u3002`
4039
+ );
4040
+ return lines.join("\n");
4041
+ }
4042
+ if (!result.found) {
4043
+ lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u306F roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093(rename \u5BFE\u8C61\u306A\u3057)\u3002`);
4044
+ return lines.join("\n");
4045
+ }
4046
+ if (result.collision) {
4047
+ lines.push(
4048
+ `\u26A0\uFE0F \`${result.newTarget}\` \u306F\u65E2\u306B roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u3059\u3002\u91CD\u8907\u3092\u907F\u3051\u308B\u305F\u3081 rename \u3057\u307E\u305B\u3093\u3002`
4049
+ );
4050
+ return lines.join("\n");
4051
+ }
4052
+ if (result.applied) {
4053
+ lines.push(`\u2705 \`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u3057\u307E\u3057\u305F\u3002`);
4054
+ } else {
4055
+ lines.push(
4056
+ `\`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
4057
+ );
4058
+ }
4059
+ if (result.sourceRootRenamed !== void 0) {
4060
+ lines.push(
4061
+ `- source_roots \u306E ${result.sourceRootRenamed} \u3092 ${result.newTarget} \u306B\u66F4\u65B0${result.applied ? "\u3057\u307E\u3057\u305F" : "\u3057\u307E\u3059"}\u3002`
4062
+ );
4063
+ } else {
4064
+ lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(\u66F4\u65B0\u4E0D\u8981)\u3002");
4065
+ }
4066
+ lines.push("");
4067
+ if (result.basenameChanged) {
4068
+ const oldName = pathBasename(result.oldTarget);
4069
+ const newName = pathBasename(result.newTarget);
4070
+ const items = [];
4071
+ if (result.wiring.canonicalDirOld)
4072
+ items.push(`anchor canonical: agents/${oldName}/ \u2192 agents/${newName}/`);
4073
+ if (result.wiring.viewLinkOld) items.push(`workspace view \u306E symlink: ${oldName} \u2192 ${newName}`);
4074
+ if (items.length > 0) {
4075
+ lines.push(
4076
+ "## \u624B\u52D5\u30EA\u30CD\u30FC\u30E0(--apply \u306F\u89E6\u308C\u307E\u305B\u3093\u3002basename \u304C\u5909\u308F\u308B\u305F\u3081\u624B\u3067\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044)"
4077
+ );
4078
+ for (const i of items) lines.push(`- ${i}`);
4079
+ } else {
4080
+ lines.push(
4081
+ `basename \u304C ${oldName} \u2192 ${newName} \u306B\u5909\u308F\u308A\u307E\u3059\u304C\u3001anchor canonical / view symlink \u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002`
4082
+ );
4083
+ }
4084
+ lines.push(
4085
+ " \u53CD\u6620\u5F8C\u306F `basou project symlinks` / `basou project workspace` \u3067\u6307\u793A\u66F8 symlink \u3068 view \u3092\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4086
+ );
4087
+ } else {
4088
+ lines.push(
4089
+ "\u6CE8: basename \u306F\u4E0D\u5909\u3067\u3059\u3002repo \u3092\u5225\u306E\u5834\u6240\u3078\u79FB\u52D5\u3057\u305F\u5834\u5408\u306F `basou project symlinks` / `basou project workspace` \u3067\u76F8\u5BFE\u30BF\u30FC\u30B2\u30C3\u30C8\u3092\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4090
+ );
4091
+ }
4092
+ lines.push("");
4093
+ lines.push(
4094
+ "\u6CE8: rename \u306F manifest(.basou\u3001git \u8FFD\u8DE1=\u53EF\u9006)\u306E\u307F\u3092\u5909\u66F4\u3057\u307E\u3059\u3002repo \u306E\u79FB\u52D5\u30FBon-disk \u306E wiring \u66F4\u65B0\u306F\u884C\u3044\u307E\u305B\u3093\u3002"
4095
+ );
4096
+ return lines.join("\n");
2109
4097
  }
2110
- async function refreshAll(args) {
2111
- const { options, ctx, paths, nowIso } = args;
2112
- const dryRun = options.dryRun === true;
2113
- const claudeCode = await importClaudeCode(options, ctx);
2114
- const codex = await importCodex(options, ctx);
2115
- if (dryRun) {
2116
- const skipped = { status: "skipped", reason: "dry-run" };
2117
- return { claudeCode, codex, handoff: skipped, decisions: skipped, dryRun };
4098
+
4099
+ // src/commands/refresh.ts
4100
+ import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths10, findErrorCode as findErrorCode9 } from "@basou/core";
4101
+ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
4102
+
4103
+ // src/lib/portfolio-config.ts
4104
+ import { homedir as homedir3 } from "os";
4105
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
4106
+ import { readYamlFile as readYamlFile3 } from "@basou/core";
4107
+ var DEFAULT_PORTFOLIO_CONFIG_PATH = join5(homedir3(), ".basou", "portfolio.yaml");
4108
+ function expandTilde(p) {
4109
+ if (p === "~") return homedir3();
4110
+ if (p.startsWith("~/")) return join5(homedir3(), p.slice(2));
4111
+ return p;
4112
+ }
4113
+ function isRecord(value) {
4114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4115
+ }
4116
+ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
4117
+ let raw;
4118
+ try {
4119
+ raw = await readYamlFile3(configPath);
4120
+ } catch (error) {
4121
+ if (error instanceof Error && error.message === "YAML file not found") {
4122
+ throw new Error(
4123
+ "No portfolio config at ~/.basou/portfolio.yaml. Create one (a 'workspaces:' list of repo paths) or pass --workspace <path>."
4124
+ );
4125
+ }
4126
+ if (error instanceof Error && error.message === "Failed to parse YAML content") {
4127
+ throw new Error("~/.basou/portfolio.yaml is not valid YAML.");
4128
+ }
4129
+ throw error;
2118
4130
  }
2119
- const handoffCounts = await regenerateHandoff(paths, nowIso);
2120
- const decisionCounts = await regenerateDecisions(paths, nowIso);
2121
- return {
2122
- claudeCode,
2123
- codex,
2124
- handoff: { status: "generated", ...handoffCounts },
2125
- decisions: { status: "generated", ...decisionCounts },
2126
- dryRun
2127
- };
4131
+ if (!isRecord(raw) || !Array.isArray(raw.workspaces)) {
4132
+ throw new Error("~/.basou/portfolio.yaml must contain a 'workspaces:' list.");
4133
+ }
4134
+ const seen = /* @__PURE__ */ new Set();
4135
+ const result = [];
4136
+ for (const entry of raw.workspaces) {
4137
+ if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) {
4138
+ throw new Error("Each portfolio workspace needs a non-empty string 'path'.");
4139
+ }
4140
+ if (entry.label !== void 0 && typeof entry.label !== "string") {
4141
+ throw new Error("A portfolio workspace 'label' must be a string when present.");
4142
+ }
4143
+ const expanded = expandTilde(entry.path.trim());
4144
+ if (!isAbsolute2(expanded)) {
4145
+ throw new Error(
4146
+ "Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
4147
+ );
4148
+ }
4149
+ const abs = resolve4(expanded);
4150
+ if (seen.has(abs)) continue;
4151
+ seen.add(abs);
4152
+ result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
4153
+ }
4154
+ if (result.length === 0) {
4155
+ throw new Error("~/.basou/portfolio.yaml has no workspaces.");
4156
+ }
4157
+ return result;
2128
4158
  }
2129
4159
 
2130
4160
  // src/commands/refresh-watch.ts
2131
4161
  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";
4162
+ import { homedir as homedir4 } from "os";
4163
+ import { join as join6 } from "path";
4164
+ import { findErrorCode as findErrorCode8 } from "@basou/core";
2135
4165
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
2136
4166
  var MIN_WATCH_INTERVAL_SEC = 5;
2137
4167
  var MAX_WATCH_INTERVAL_SEC = 86400;
2138
4168
  function watchedRoots(ctx) {
2139
4169
  return [
2140
- ctx.codexSessionsDir ?? join4(homedir3(), ".codex", "sessions"),
2141
- ctx.claudeProjectsDir ?? join4(homedir3(), ".claude", "projects")
4170
+ ctx.codexSessionsDir ?? join6(homedir4(), ".codex", "sessions"),
4171
+ ctx.claudeProjectsDir ?? join6(homedir4(), ".claude", "projects")
2142
4172
  ];
2143
4173
  }
2144
4174
  async function scanSourceLogs(roots) {
@@ -2148,11 +4178,11 @@ async function scanSourceLogs(roots) {
2148
4178
  try {
2149
4179
  entries = await readdir2(dir, { withFileTypes: true });
2150
4180
  } catch (error) {
2151
- if (findErrorCode6(error, "ENOENT") || findErrorCode6(error, "ENOTDIR")) return;
4181
+ if (findErrorCode8(error, "ENOENT") || findErrorCode8(error, "ENOTDIR")) return;
2152
4182
  throw new Error("Failed to read a source log directory", { cause: error });
2153
4183
  }
2154
4184
  for (const entry of entries) {
2155
- const full = join4(dir, entry.name);
4185
+ const full = join6(dir, entry.name);
2156
4186
  if (entry.isDirectory()) {
2157
4187
  await walk(full);
2158
4188
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -2160,7 +4190,7 @@ async function scanSourceLogs(roots) {
2160
4190
  const info = await stat2(full);
2161
4191
  out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
2162
4192
  } catch (error) {
2163
- if (findErrorCode6(error, "ENOENT")) continue;
4193
+ if (findErrorCode8(error, "ENOENT")) continue;
2164
4194
  throw new Error("Failed to stat a source log file", { cause: error });
2165
4195
  }
2166
4196
  }
@@ -2252,26 +4282,26 @@ function collectPath2(value, previous) {
2252
4282
  function parseInterval(value) {
2253
4283
  const seconds = Number(value);
2254
4284
  if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
2255
- throw new InvalidArgumentError2(
4285
+ throw new InvalidArgumentError3(
2256
4286
  `--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
2257
4287
  );
2258
4288
  }
2259
4289
  return seconds;
2260
4290
  }
2261
4291
  function abortableSleep(ms, signal) {
2262
- return new Promise((resolve4) => {
4292
+ return new Promise((resolve8) => {
2263
4293
  if (signal.aborted) {
2264
- resolve4();
4294
+ resolve8();
2265
4295
  return;
2266
4296
  }
2267
4297
  let timer;
2268
4298
  const onAbort = () => {
2269
4299
  clearTimeout(timer);
2270
- resolve4();
4300
+ resolve8();
2271
4301
  };
2272
4302
  timer = setTimeout(() => {
2273
4303
  signal.removeEventListener("abort", onAbort);
2274
- resolve4();
4304
+ resolve8();
2275
4305
  }, ms);
2276
4306
  signal.addEventListener("abort", onAbort, { once: true });
2277
4307
  });
@@ -2285,6 +4315,9 @@ function registerRefreshCommand(program) {
2285
4315
  collectPath2,
2286
4316
  []
2287
4317
  ).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(
4318
+ "--portfolio",
4319
+ "Refresh every workspace listed in ~/.basou/portfolio.yaml (each with its own source roots)"
4320
+ ).option(
2288
4321
  "--watch",
2289
4322
  "Keep running: re-import + regenerate when the native logs change (Ctrl-C to stop)"
2290
4323
  ).option(
@@ -2297,7 +4330,9 @@ function registerRefreshCommand(program) {
2297
4330
  }
2298
4331
  async function runRefresh(options, ctx = {}) {
2299
4332
  try {
2300
- if (options.watch === true) {
4333
+ if (options.portfolio === true) {
4334
+ await doRunRefreshPortfolio(options, ctx);
4335
+ } else if (options.watch === true) {
2301
4336
  await doRunRefreshWatch(options, ctx);
2302
4337
  } else {
2303
4338
  await doRunRefresh(options, ctx);
@@ -2307,14 +4342,58 @@ async function runRefresh(options, ctx = {}) {
2307
4342
  process.exitCode = 1;
2308
4343
  }
2309
4344
  }
4345
+ async function doRunRefreshPortfolio(options, ctx) {
4346
+ if (options.watch === true) throw new Error("--portfolio cannot be combined with --watch.");
4347
+ if (options.project !== void 0 && options.project.length > 0) {
4348
+ throw new Error(
4349
+ "--portfolio refreshes each workspace with its own source roots; remove --project."
4350
+ );
4351
+ }
4352
+ const workspaces = await loadPortfolioConfig(ctx.portfolioConfigPath);
4353
+ const rollup = [];
4354
+ for (const ws of workspaces) {
4355
+ const label = ws.label ?? ws.path;
4356
+ try {
4357
+ const result = await computeRefresh(
4358
+ { ...options, portfolio: false },
4359
+ { ...ctx, cwd: ws.path }
4360
+ );
4361
+ rollup.push({ label, path: ws.path, status: "ok", result });
4362
+ if (options.json !== true) {
4363
+ console.log(`
4364
+ ## ${label} (${ws.path})`);
4365
+ printRefreshSummary(result);
4366
+ }
4367
+ } catch (error) {
4368
+ const message = error instanceof Error ? error.message : String(error);
4369
+ rollup.push({ label, path: ws.path, status: "failed", error: message });
4370
+ if (options.json !== true) {
4371
+ console.log(`
4372
+ ## ${label} (${ws.path})`);
4373
+ console.log(` failed: ${message}`);
4374
+ }
4375
+ }
4376
+ }
4377
+ if (options.json === true) {
4378
+ console.log(JSON.stringify({ portfolio: true, workspaces: rollup }));
4379
+ } else {
4380
+ const failed = rollup.filter((r) => r.status === "failed").length;
4381
+ const ok = rollup.length - failed;
4382
+ console.log(
4383
+ `
4384
+ portfolio: ${ok}/${rollup.length} refreshed${failed > 0 ? `, ${failed} failed` : ""}.`
4385
+ );
4386
+ }
4387
+ if (rollup.some((r) => r.status === "failed")) process.exitCode = 1;
4388
+ }
2310
4389
  async function doRunRefreshWatch(options, ctx) {
2311
4390
  if (options.dryRun === true) throw new Error("--watch cannot be combined with --dry-run.");
2312
4391
  if (options.json === true) throw new Error("--watch cannot be combined with --json.");
2313
4392
  if (options.force === true) throw new Error("--watch cannot be combined with --force.");
2314
4393
  const cwd = ctx.cwd ?? process.cwd();
2315
- const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
2316
- const paths = basouPaths7(repositoryRoot);
2317
- await assertWorkspaceInitialized6(paths.root);
4394
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
4395
+ const paths = basouPaths10(repositoryRoot);
4396
+ await assertWorkspaceInitialized8(paths.root);
2318
4397
  const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
2319
4398
  const controller = new AbortController();
2320
4399
  const onSignal = () => controller.abort();
@@ -2322,7 +4401,9 @@ async function doRunRefreshWatch(options, ctx) {
2322
4401
  process.on("SIGTERM", onSignal);
2323
4402
  try {
2324
4403
  await runRefreshWatch({
2325
- ctx,
4404
+ // Watch from a workspace view: import from the resolved planning repo, not
4405
+ // the raw (non-git) view cwd — mirrors the redirect in computeRefresh.
4406
+ ctx: { ...ctx, cwd: repositoryRoot },
2326
4407
  paths,
2327
4408
  intervalMs,
2328
4409
  importOptions: options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
@@ -2336,22 +4417,27 @@ async function doRunRefreshWatch(options, ctx) {
2336
4417
  process.off("SIGTERM", onSignal);
2337
4418
  }
2338
4419
  }
2339
- async function doRunRefresh(options, ctx) {
4420
+ async function computeRefresh(options, ctx) {
2340
4421
  const cwd = ctx.cwd ?? process.cwd();
2341
- const repositoryRoot = await resolveRepositoryRootForRefresh(cwd);
2342
- const paths = basouPaths7(repositoryRoot);
2343
- await assertWorkspaceInitialized6(paths.root);
4422
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
4423
+ const paths = basouPaths10(repositoryRoot);
4424
+ await assertWorkspaceInitialized8(paths.root);
2344
4425
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2345
- const result = await refreshAll({
4426
+ return refreshAll({
2346
4427
  options: {
2347
4428
  ...options.project !== void 0 && options.project.length > 0 ? { project: options.project } : {},
2348
4429
  ...options.force === true ? { force: true } : {},
2349
4430
  ...options.dryRun === true ? { dryRun: true } : {}
2350
4431
  },
2351
- ctx,
4432
+ // Import from the resolved repo root, not the raw cwd: a workspace-view cwd
4433
+ // redirects to its planning repo, and the import must run there too.
4434
+ ctx: { ...ctx, cwd: repositoryRoot },
2352
4435
  paths,
2353
4436
  nowIso
2354
4437
  });
4438
+ }
4439
+ async function doRunRefresh(options, ctx) {
4440
+ const result = await computeRefresh(options, ctx);
2355
4441
  if (options.json === true) {
2356
4442
  console.log(JSON.stringify(result));
2357
4443
  } else {
@@ -2383,28 +4469,30 @@ function printRefreshSummary(result) {
2383
4469
  console.log(`handoff: skipped (${result.handoff.reason})`);
2384
4470
  }
2385
4471
  if (result.decisions.status === "generated") {
2386
- console.log(`decisions: regenerated (${result.decisions.decisionCount})`);
4472
+ if (result.decisions.decisionCount === 0) {
4473
+ const hasSessions = result.handoff.status === "generated" && result.handoff.sessionCount > 0;
4474
+ console.log(
4475
+ hasSessions ? "decisions: 0 (none auto-recorded from these sessions; record any made with 'basou decision record')" : "decisions: 0"
4476
+ );
4477
+ } else {
4478
+ console.log(`decisions: regenerated (${result.decisions.decisionCount})`);
4479
+ }
2387
4480
  } else {
2388
4481
  console.log(`decisions: skipped (${result.decisions.reason})`);
2389
4482
  }
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;
4483
+ if (result.orientation.status === "generated") {
4484
+ console.log(
4485
+ `orientation: regenerated (in-flight: ${result.orientation.inFlightTaskCount}, pending approvals: ${result.orientation.pendingApprovalsCount}, suspect: ${result.orientation.suspectCount})`
4486
+ );
4487
+ } else {
4488
+ console.log(`orientation: skipped (${result.orientation.reason})`);
2401
4489
  }
2402
4490
  }
2403
- async function assertWorkspaceInitialized6(basouRoot) {
4491
+ async function assertWorkspaceInitialized8(basouRoot) {
2404
4492
  try {
2405
- await assertBasouRootSafe7(basouRoot);
4493
+ await assertBasouRootSafe9(basouRoot);
2406
4494
  } catch (error) {
2407
- if (findErrorCode7(error, "ENOENT")) {
4495
+ if (findErrorCode9(error, "ENOENT")) {
2408
4496
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2409
4497
  }
2410
4498
  throw error;
@@ -2412,14 +4500,14 @@ async function assertWorkspaceInitialized6(basouRoot) {
2412
4500
  }
2413
4501
 
2414
4502
  // src/commands/report.ts
2415
- import { isAbsolute, resolve as resolve3 } from "path";
4503
+ import { isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
2416
4504
  import {
2417
- assertBasouRootSafe as assertBasouRootSafe8,
2418
- basouPaths as basouPaths8,
2419
- findErrorCode as findErrorCode8,
4505
+ assertBasouRootSafe as assertBasouRootSafe10,
4506
+ basouPaths as basouPaths11,
4507
+ findErrorCode as findErrorCode10,
2420
4508
  renderReport,
2421
- resolveRepositoryRoot as resolveRepositoryRoot9,
2422
- writeMarkdownFile as writeMarkdownFile4
4509
+ resolveRepositoryRoot as resolveRepositoryRoot8,
4510
+ writeMarkdownFile as writeMarkdownFile6
2423
4511
  } from "@basou/core";
2424
4512
  function registerReportCommand(program) {
2425
4513
  const report = program.command("report").description(
@@ -2440,8 +4528,8 @@ async function runReportGenerate(options, ctx = {}) {
2440
4528
  async function doRunReportGenerate(options, ctx) {
2441
4529
  const cwd = ctx.cwd ?? process.cwd();
2442
4530
  const repositoryRoot = await resolveRepositoryRootForReport(cwd);
2443
- const paths = basouPaths8(repositoryRoot);
2444
- await assertWorkspaceInitialized7(paths.root);
4531
+ const paths = basouPaths11(repositoryRoot);
4532
+ await assertWorkspaceInitialized9(paths.root);
2445
4533
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2446
4534
  const result = await renderReport({
2447
4535
  paths,
@@ -2452,8 +4540,8 @@ async function doRunReportGenerate(options, ctx) {
2452
4540
  onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2453
4541
  });
2454
4542
  if (options.out !== void 0) {
2455
- const outPath = isAbsolute(options.out) ? options.out : resolve3(cwd, options.out);
2456
- await writeMarkdownFile4(outPath, result.body);
4543
+ const outPath = isAbsolute3(options.out) ? options.out : resolve5(cwd, options.out);
4544
+ await writeMarkdownFile6(outPath, result.body);
2457
4545
  const { sessions, decisions, tasks } = result.data;
2458
4546
  console.error(
2459
4547
  `Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
@@ -2467,7 +4555,7 @@ async function doRunReportGenerate(options, ctx) {
2467
4555
  }
2468
4556
  async function resolveRepositoryRootForReport(cwd) {
2469
4557
  try {
2470
- return await resolveRepositoryRoot9(cwd);
4558
+ return await resolveRepositoryRoot8(cwd);
2471
4559
  } catch (error) {
2472
4560
  if (error instanceof Error && error.message === "Not a git repository") {
2473
4561
  throw new Error(
@@ -2478,25 +4566,149 @@ async function resolveRepositoryRootForReport(cwd) {
2478
4566
  throw error;
2479
4567
  }
2480
4568
  }
2481
- async function assertWorkspaceInitialized7(basouRoot) {
4569
+ async function assertWorkspaceInitialized9(basouRoot) {
2482
4570
  try {
2483
- await assertBasouRootSafe8(basouRoot);
4571
+ await assertBasouRootSafe10(basouRoot);
2484
4572
  } catch (error) {
2485
- if (findErrorCode8(error, "ENOENT")) {
4573
+ if (findErrorCode10(error, "ENOENT")) {
2486
4574
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2487
4575
  }
2488
4576
  throw error;
2489
4577
  }
2490
4578
  }
2491
4579
 
4580
+ // src/commands/review-gaps.ts
4581
+ import {
4582
+ basouPaths as basouPaths12,
4583
+ findReviewGaps
4584
+ } from "@basou/core";
4585
+ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
4586
+ function collectRepo(value, previous) {
4587
+ return [...previous, value];
4588
+ }
4589
+ function parseWindow(value) {
4590
+ const hours = Number(value);
4591
+ if (!Number.isInteger(hours) || hours <= 0) {
4592
+ throw new InvalidArgumentError4("--window must be a positive integer (hours).");
4593
+ }
4594
+ return hours;
4595
+ }
4596
+ function registerReviewGapsCommand(program) {
4597
+ program.command("review-gaps").description(
4598
+ "Surface units of work committed without a bound cross-model review trail (read-only, advisory)"
4599
+ ).option(
4600
+ "--repo <name>",
4601
+ "Restrict to a repo by name (repeatable; default: every repo with captured commits)",
4602
+ collectRepo,
4603
+ []
4604
+ ).option(
4605
+ "--window <hours>",
4606
+ "Hours before a commit to look for a review (default 24)",
4607
+ parseWindow
4608
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
4609
+ await runReviewGaps(opts);
4610
+ });
4611
+ }
4612
+ async function runReviewGaps(options, ctx = {}) {
4613
+ try {
4614
+ await doRunReviewGaps(options, ctx);
4615
+ } catch (error) {
4616
+ renderCliError(error, { verbose: isVerbose(options) });
4617
+ process.exitCode = 1;
4618
+ }
4619
+ }
4620
+ async function doRunReviewGaps(options, ctx) {
4621
+ const cwd = ctx.cwd ?? process.cwd();
4622
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
4623
+ const paths = basouPaths12(repositoryRoot);
4624
+ const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
4625
+ const summary = await findReviewGaps({
4626
+ paths,
4627
+ nowIso,
4628
+ ...options.repo !== void 0 && options.repo.length > 0 ? { scope: options.repo } : {},
4629
+ ...options.window !== void 0 ? { windowHours: options.window } : {},
4630
+ onWarning: (w, sid) => printReplayWarning(w, sid),
4631
+ onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
4632
+ });
4633
+ if (options.json === true) {
4634
+ console.log(JSON.stringify(summary));
4635
+ } else {
4636
+ console.log(renderReviewGaps(summary));
4637
+ }
4638
+ return summary;
4639
+ }
4640
+ function relAge(iso, now) {
4641
+ if (iso === null) return "(\u4E0D\u660E)";
4642
+ const ms = now.getTime() - Date.parse(iso);
4643
+ if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
4644
+ const days = Math.floor(ms / 864e5);
4645
+ if (days >= 1) return `${days}\u65E5\u524D`;
4646
+ const hours = Math.floor(ms / 36e5);
4647
+ if (hours >= 1) return `${hours}\u6642\u9593\u524D`;
4648
+ return `${Math.max(1, Math.floor(ms / 6e4))}\u5206\u524D`;
4649
+ }
4650
+ function unitLine(u, now) {
4651
+ const when = relAge(u.lastCommitAt, now);
4652
+ const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
4653
+ if (u.verdict === "near_unbound") {
4654
+ const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
4655
+ return `${head} \u2014 \u8FD1\u63A5\u30EC\u30D3\u30E5\u30FC\u306F\u3042\u308B\u304C diff/\u5909\u66F4\u30D5\u30A1\u30A4\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u3044\u306A\u3044 [${ids}]`;
4656
+ }
4657
+ return `${head} \u2014 \u7D10\u3065\u304F\u30AF\u30ED\u30B9\u30E2\u30C7\u30EB\u30EC\u30D3\u30E5\u30FC\u306A\u3057`;
4658
+ }
4659
+ function candidateLine(u, now) {
4660
+ const when = relAge(u.lastCommitAt, now);
4661
+ const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
4662
+ return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 \u30EC\u30D3\u30E5\u30FC\u5F62\u8DE1: ${cite}`;
4663
+ }
4664
+ function renderReviewGaps(summary) {
4665
+ const now = new Date(summary.generatedAt);
4666
+ const lines = [];
4667
+ const scope = summary.scope ? summary.scope.join(", ") : "\u5168\u30EA\u30DD\u30B8\u30C8\u30EA";
4668
+ lines.push(`# \u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306E\u30AE\u30E3\u30C3\u30D7 (${scope})`);
4669
+ lines.push("");
4670
+ if (summary.gaps.length === 0) {
4671
+ lines.push("\u2705 \u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E\u7BC4\u56F2\u3067\u306F\u3001\u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306A\u3057\u3067\u7740\u5730\u3057\u305F\u4F5C\u696D\u5358\u4F4D\u306F\u3042\u308A\u307E\u305B\u3093\u3002");
4672
+ } else {
4673
+ lines.push(`\u26A0\uFE0F \u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306A\u3057\u3067\u7740\u5730\u3057\u305F\u4F5C\u696D\u5358\u4F4D: ${summary.gaps.length}`);
4674
+ for (const u of summary.gaps) lines.push(unitLine(u, now));
4675
+ }
4676
+ lines.push("");
4677
+ if (summary.candidates.length > 0) {
4678
+ lines.push(
4679
+ `## \u78BA\u8A8D\u5F85\u3061 (${summary.candidates.length}) \u2014 \u30AF\u30ED\u30B9\u30E2\u30C7\u30EB\u304C\u30EC\u30D3\u30E5\u30FC\u3057\u305F\u5F62\u8DE1\u3042\u308A\u3002\u3053\u306E\u5909\u66F4\u3092\u672C\u5F53\u306B\u898B\u305F\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`
4680
+ );
4681
+ for (const u of summary.candidates) lines.push(candidateLine(u, now));
4682
+ lines.push("");
4683
+ }
4684
+ if (summary.unknowns.length > 0) {
4685
+ const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
4686
+ lines.push(
4687
+ `## \u5C0E\u51FA\u4E0D\u53EF (${summary.unknowns.length} \u5358\u4F4D / ${n} commit) \u2014 repo \u304B\u6642\u523B\u3092\u6355\u6349\u304B\u3089\u5C0E\u3051\u305A\u3001\u5224\u5B9A\u3092\u4FDD\u7559(clear \u3067\u306F\u3042\u308A\u307E\u305B\u3093)`
4688
+ );
4689
+ lines.push("");
4690
+ }
4691
+ lines.push("## \u30EA\u30DD\u30B8\u30C8\u30EA\u5225");
4692
+ for (const r of summary.repos) {
4693
+ lines.push(
4694
+ `- ${r.repo}: ${r.units} \u5358\u4F4D (\u8A3C\u8DE1\u306A\u3057 ${r.omissionUnits} / \u8FD1\u63A5\u306E\u307F ${r.nearUnboundUnits} / \u78BA\u8A8D\u5F85\u3061 ${r.candidateUnits}${r.unknownUnits > 0 ? ` / \u4E0D\u660E ${r.unknownUnits}` : ""})`
4695
+ );
4696
+ }
4697
+ lines.push("");
4698
+ lines.push(
4699
+ `\u6CE8: read-only \u306E advisory \u3067\u3059\u3002\u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E commit \u306E\u307F\u304C\u5BFE\u8C61\uFF08\u6700\u65B0\u53D6\u8FBC commit: ${summary.newestCommitAt === null ? "\u306A\u3057" : relAge(summary.newestCommitAt, now)}\uFF09\u3002\u30EC\u30D3\u30E5\u30FC\u306E\u300C\u5B9F\u65BD\u300D\u306F\u81EA\u52D5\u5224\u5B9A\u305B\u305A\u3001\u6642\u9593\u7684\u8FD1\u63A5\u3060\u3051\u3067\u306F\u5408\u683C\u306B\u3057\u307E\u305B\u3093\u3002enforce \u306F\u3057\u307E\u305B\u3093\u3002`
4700
+ );
4701
+ return lines.join("\n");
4702
+ }
4703
+
2492
4704
  // src/commands/run.ts
2493
4705
  import { mkdir as mkdir2 } from "fs/promises";
2494
- import { homedir as homedir4 } from "os";
2495
- import { join as join5 } from "path";
4706
+ import { homedir as homedir5 } from "os";
4707
+ import { join as join7 } from "path";
2496
4708
  import {
2497
- acquireLock as acquireLock4,
2498
- assertBasouRootSafe as assertBasouRootSafe9,
2499
- basouPaths as basouPaths9,
4709
+ acquireLock as acquireLock5,
4710
+ assertBasouRootSafe as assertBasouRootSafe11,
4711
+ basouPaths as basouPaths13,
2500
4712
  ChildProcessRunner as ChildProcessRunner2,
2501
4713
  claudeCodeAdapterMetadata,
2502
4714
  appendChainedEvent as coreAppendChainedEvent2,
@@ -2505,10 +4717,10 @@ import {
2505
4717
  getSnapshot as getSnapshot2,
2506
4718
  overwriteYamlFile as overwriteYamlFile2,
2507
4719
  prefixedUlid as prefixedUlid4,
2508
- readManifest as readManifest4,
2509
- readYamlFile as readYamlFile3,
4720
+ readManifest as readManifest6,
4721
+ readYamlFile as readYamlFile4,
2510
4722
  resolveClaudeCodeCommand,
2511
- resolveRepositoryRoot as resolveRepositoryRoot10,
4723
+ resolveRepositoryRoot as resolveRepositoryRoot9,
2512
4724
  SessionSchema as SessionSchema2,
2513
4725
  sanitizeRelatedFiles,
2514
4726
  sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
@@ -2541,17 +4753,17 @@ async function runClaudeCode(args, options, ctx = {}) {
2541
4753
  const { command } = await resolveCommand();
2542
4754
  const cwd = options.cwd ?? process.cwd();
2543
4755
  const repoRoot = await resolveRepositoryRootForRun(cwd);
2544
- const paths = basouPaths9(repoRoot);
2545
- await assertBasouRootSafe9(paths.root);
2546
- const manifest = await readManifest4(paths);
4756
+ const paths = basouPaths13(repoRoot);
4757
+ await assertBasouRootSafe11(paths.root);
4758
+ const manifest = await readManifest6(paths);
2547
4759
  const sessionId = prefixedUlid4("ses");
2548
- const sessionDir = join5(paths.sessions, sessionId);
4760
+ const sessionDir = join7(paths.sessions, sessionId);
2549
4761
  await mkdir2(sessionDir, { recursive: true });
2550
4762
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
2551
4763
  await coreAppendChainedEvent2(paths, sessionId, event);
2552
4764
  });
2553
4765
  const startedAt = now().toISOString();
2554
- const sessionYamlPath = join5(sessionDir, "session.yaml");
4766
+ const sessionYamlPath = join7(sessionDir, "session.yaml");
2555
4767
  const session = buildInitialSession2({
2556
4768
  id: sessionId,
2557
4769
  command,
@@ -2584,7 +4796,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2584
4796
  from: "initialized",
2585
4797
  to: "running"
2586
4798
  });
2587
- const runningLock = await acquireLock4(paths, "session", sessionId);
4799
+ const runningLock = await acquireLock5(paths, "session", sessionId);
2588
4800
  try {
2589
4801
  await mutateSessionYaml2(sessionYamlPath, (s) => {
2590
4802
  s.session.status = "running";
@@ -2677,7 +4889,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2677
4889
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
2678
4890
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
2679
4891
  workingDirectory: repoRoot,
2680
- homedir: homedir4()
4892
+ homedir: homedir5()
2681
4893
  }).sanitized;
2682
4894
  const finalStatus = decideFinalStatus2(result, signalReceived);
2683
4895
  await appendEvent(sessionDir, {
@@ -2821,7 +5033,7 @@ function buildInitialSession2(input) {
2821
5033
  source: { ...claudeCodeAdapterMetadata },
2822
5034
  started_at: input.startedAt,
2823
5035
  status: "initialized",
2824
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir4() }),
5036
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir5() }),
2825
5037
  invocation: {
2826
5038
  command: input.command,
2827
5039
  args: [...input.args],
@@ -2833,7 +5045,7 @@ function buildInitialSession2(input) {
2833
5045
  };
2834
5046
  }
2835
5047
  async function mutateSessionYaml2(filePath, mutator) {
2836
- const raw = await readYamlFile3(filePath);
5048
+ const raw = await readYamlFile4(filePath);
2837
5049
  const parsed = SessionSchema2.parse(raw);
2838
5050
  mutator(parsed);
2839
5051
  const validated = SessionSchema2.parse(parsed);
@@ -2881,7 +5093,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
2881
5093
  }
2882
5094
  async function resolveRepositoryRootForRun(cwd) {
2883
5095
  try {
2884
- return await resolveRepositoryRoot10(cwd);
5096
+ return await resolveRepositoryRoot9(cwd);
2885
5097
  } catch (error) {
2886
5098
  if (error instanceof Error && error.message === "Not a git repository") {
2887
5099
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -2894,29 +5106,28 @@ async function resolveRepositoryRootForRun(cwd) {
2894
5106
 
2895
5107
  // src/commands/session.ts
2896
5108
  import { readFile as readFile2 } from "fs/promises";
2897
- import { basename as basename3, isAbsolute as isAbsolute2, join as join6, relative as relative2 } from "path";
5109
+ import { basename as basename4, isAbsolute as isAbsolute4, join as join8, relative as relative3 } from "path";
2898
5110
  import {
2899
- acquireLock as acquireLock5,
2900
- appendEventToExistingSession as appendEventToExistingSession2,
2901
- assertBasouRootSafe as assertBasouRootSafe10,
2902
- basouPaths as basouPaths10,
5111
+ acquireLock as acquireLock6,
5112
+ appendEventToExistingSession as appendEventToExistingSession3,
5113
+ assertBasouRootSafe as assertBasouRootSafe12,
5114
+ basouPaths as basouPaths14,
2903
5115
  enumerateSessionDirs as enumerateSessionDirs2,
2904
- findErrorCode as findErrorCode9,
5116
+ findErrorCode as findErrorCode11,
2905
5117
  importSessionFromJson as importSessionFromJson2,
2906
5118
  loadSessionEntries,
2907
5119
  readAllEvents,
2908
- readManifest as readManifest5,
2909
- readYamlFile as readYamlFile4,
5120
+ readManifest as readManifest7,
5121
+ readYamlFile as readYamlFile5,
2910
5122
  rechainSessionInPlace,
2911
- resolveRepositoryRoot as resolveRepositoryRoot11,
2912
- resolveSessionId as resolveSessionId2,
5123
+ resolveSessionId as resolveSessionId3,
2913
5124
  resolveTaskId,
2914
5125
  SessionImportPayloadSchema as SessionImportPayloadSchema2,
2915
5126
  SessionSchema as SessionSchema3,
2916
5127
  SessionStatusSchema,
2917
5128
  sessionWorkStatsFromEvents
2918
5129
  } from "@basou/core";
2919
- import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
5130
+ import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
2920
5131
 
2921
5132
  // src/lib/format-duration.ts
2922
5133
  import { formatDurationMs } from "@basou/core";
@@ -2965,8 +5176,8 @@ async function runSessionList(options, ctx = {}) {
2965
5176
  async function doRunSessionList(options, ctx) {
2966
5177
  const cwd = ctx.cwd ?? process.cwd();
2967
5178
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
2968
- const paths = basouPaths10(repositoryRoot);
2969
- await assertWorkspaceInitialized8(paths.root);
5179
+ const paths = basouPaths14(repositoryRoot);
5180
+ await assertWorkspaceInitialized10(paths.root);
2970
5181
  const now = /* @__PURE__ */ new Date();
2971
5182
  const records = (await loadSessionEntries(paths, {
2972
5183
  now,
@@ -3017,17 +5228,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
3017
5228
  async function doRunSessionShow(idInput, options, ctx) {
3018
5229
  const cwd = ctx.cwd ?? process.cwd();
3019
5230
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
3020
- const paths = basouPaths10(repositoryRoot);
3021
- await assertWorkspaceInitialized8(paths.root);
3022
- const sessionId = await resolveSessionId2(paths, idInput);
3023
- const sessionDir = join6(paths.sessions, sessionId);
3024
- const sessionYamlPath = join6(sessionDir, "session.yaml");
5231
+ const paths = basouPaths14(repositoryRoot);
5232
+ await assertWorkspaceInitialized10(paths.root);
5233
+ const sessionId = await resolveSessionId3(paths, idInput);
5234
+ const sessionDir = join8(paths.sessions, sessionId);
5235
+ const sessionYamlPath = join8(sessionDir, "session.yaml");
3025
5236
  let session;
3026
5237
  try {
3027
- const raw = await readYamlFile4(sessionYamlPath);
5238
+ const raw = await readYamlFile5(sessionYamlPath);
3028
5239
  session = SessionSchema3.parse(raw);
3029
5240
  } catch (error) {
3030
- if (findErrorCode9(error, "ENOENT")) {
5241
+ if (findErrorCode11(error, "ENOENT")) {
3031
5242
  throw new Error(`Session not found: ${idInput}`);
3032
5243
  }
3033
5244
  throw new Error("Failed to read session", { cause: error });
@@ -3142,12 +5353,12 @@ function formatSessionWork(session, events, now) {
3142
5353
  }
3143
5354
  function formatWorkingDir(workingDir, repositoryRoot, options) {
3144
5355
  if (options.fullPath === true) return workingDir;
3145
- if (!isAbsolute2(workingDir)) {
5356
+ if (!isAbsolute4(workingDir)) {
3146
5357
  if (workingDir === ".") return "<repository_root>";
3147
5358
  return workingDir;
3148
5359
  }
3149
5360
  if (workingDir === repositoryRoot) return "<repository_root>";
3150
- const rel = relative2(repositoryRoot, workingDir);
5361
+ const rel = relative3(repositoryRoot, workingDir);
3151
5362
  if (rel.length === 0 || rel === ".") return "<repository_root>";
3152
5363
  if (rel.startsWith("..")) return rel;
3153
5364
  return `./${rel}`;
@@ -3261,23 +5472,13 @@ function maxLen2(values, floor) {
3261
5472
  return max;
3262
5473
  }
3263
5474
  async function resolveRepositoryRootForSession(cwd, subcmd) {
3264
- try {
3265
- return await resolveRepositoryRoot11(cwd);
3266
- } catch (error) {
3267
- if (error instanceof Error && error.message === "Not a git repository") {
3268
- throw new Error(
3269
- `Not a git repository. Run 'git init' first, then re-run 'basou session ${subcmd}'.`,
3270
- { cause: error }
3271
- );
3272
- }
3273
- throw error;
3274
- }
5475
+ return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
3275
5476
  }
3276
- async function assertWorkspaceInitialized8(basouRoot) {
5477
+ async function assertWorkspaceInitialized10(basouRoot) {
3277
5478
  try {
3278
- await assertBasouRootSafe10(basouRoot);
5479
+ await assertBasouRootSafe12(basouRoot);
3279
5480
  } catch (error) {
3280
- if (findErrorCode9(error, "ENOENT")) {
5481
+ if (findErrorCode11(error, "ENOENT")) {
3281
5482
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3282
5483
  }
3283
5484
  throw error;
@@ -3315,9 +5516,9 @@ async function runSessionImport(options, ctx = {}) {
3315
5516
  async function doRunSessionImport(options, ctx) {
3316
5517
  const cwd = ctx.cwd ?? process.cwd();
3317
5518
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
3318
- const paths = basouPaths10(repositoryRoot);
3319
- await assertWorkspaceInitialized8(paths.root);
3320
- const manifest = await readManifest5(paths);
5519
+ const paths = basouPaths14(repositoryRoot);
5520
+ await assertWorkspaceInitialized10(paths.root);
5521
+ const manifest = await readManifest7(paths);
3321
5522
  const rawBody = await readInputFile(options.from);
3322
5523
  const json = parseJsonStrict(rawBody);
3323
5524
  const parsed = SessionImportPayloadSchema2.safeParse(json);
@@ -3346,10 +5547,10 @@ async function readInputFile(path) {
3346
5547
  try {
3347
5548
  return await readFile2(path, "utf8");
3348
5549
  } catch (error) {
3349
- if (findErrorCode9(error, "ENOENT")) {
5550
+ if (findErrorCode11(error, "ENOENT")) {
3350
5551
  throw new Error("Import source not found", { cause: error });
3351
5552
  }
3352
- if (findErrorCode9(error, "EISDIR")) {
5553
+ if (findErrorCode11(error, "EISDIR")) {
3353
5554
  throw new Error("Import source is not a file", { cause: error });
3354
5555
  }
3355
5556
  throw new Error("Failed to read import source", { cause: error });
@@ -3364,19 +5565,19 @@ function parseJsonStrict(body) {
3364
5565
  }
3365
5566
  function parseImportFormat(raw) {
3366
5567
  if (raw !== "json") {
3367
- throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
5568
+ throw new InvalidArgumentError5(`Unsupported format: ${raw}. Valid values: json`);
3368
5569
  }
3369
5570
  return "json";
3370
5571
  }
3371
5572
  function parseLabelOverride(raw) {
3372
5573
  if (raw.length === 0) {
3373
- throw new InvalidArgumentError3("Label must not be empty");
5574
+ throw new InvalidArgumentError5("Label must not be empty");
3374
5575
  }
3375
5576
  return raw;
3376
5577
  }
3377
5578
  function parseTaskIdOverride(raw) {
3378
5579
  if (raw.length === 0) {
3379
- throw new InvalidArgumentError3("Task id is empty");
5580
+ throw new InvalidArgumentError5("Task id is empty");
3380
5581
  }
3381
5582
  return raw;
3382
5583
  }
@@ -3402,7 +5603,7 @@ function printSessionImportResult(options, result) {
3402
5603
  return;
3403
5604
  }
3404
5605
  console.log(
3405
- `Imported session ${sid} (${result.eventCount} events) from ${basename3(options.from)}`
5606
+ `Imported session ${sid} (${result.eventCount} events) from ${basename4(options.from)}`
3406
5607
  );
3407
5608
  }
3408
5609
  var NOTE_BODY_PREVIEW_LIMIT = 80;
@@ -3429,19 +5630,19 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
3429
5630
  }
3430
5631
  const cwd = ctx.cwd ?? process.cwd();
3431
5632
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
3432
- const paths = basouPaths10(repositoryRoot);
3433
- await assertWorkspaceInitialized8(paths.root);
3434
- const sessionId = await resolveSessionId2(paths, sessionIdInput);
5633
+ const paths = basouPaths14(repositoryRoot);
5634
+ await assertWorkspaceInitialized10(paths.root);
5635
+ const sessionId = await resolveSessionId3(paths, sessionIdInput);
3435
5636
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
3436
5637
  if (body.length === 0) {
3437
5638
  throw new Error("Note body is empty");
3438
5639
  }
3439
5640
  const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
3440
5641
  const sesId = sessionId;
3441
- const sessionLock = await acquireLock5(paths, "session", sesId);
5642
+ const sessionLock = await acquireLock6(paths, "session", sesId);
3442
5643
  let result;
3443
5644
  try {
3444
- result = await appendEventToExistingSession2({
5645
+ result = await appendEventToExistingSession3({
3445
5646
  paths,
3446
5647
  sessionId: sesId,
3447
5648
  eventBuilder: (eventId) => ({
@@ -3463,10 +5664,10 @@ async function readNoteFile(path) {
3463
5664
  try {
3464
5665
  return await readFile2(path, "utf8");
3465
5666
  } catch (error) {
3466
- if (findErrorCode9(error, "ENOENT")) {
5667
+ if (findErrorCode11(error, "ENOENT")) {
3467
5668
  throw new Error("Note source not found", { cause: error });
3468
5669
  }
3469
- if (findErrorCode9(error, "EISDIR")) {
5670
+ if (findErrorCode11(error, "EISDIR")) {
3470
5671
  throw new Error("Note source is not a file", { cause: error });
3471
5672
  }
3472
5673
  throw new Error("Failed to read note source", { cause: error });
@@ -3474,7 +5675,7 @@ async function readNoteFile(path) {
3474
5675
  }
3475
5676
  function parseNoteBodyOption(raw) {
3476
5677
  if (raw.length === 0) {
3477
- throw new InvalidArgumentError3("--body must not be empty");
5678
+ throw new InvalidArgumentError5("--body must not be empty");
3478
5679
  }
3479
5680
  return raw;
3480
5681
  }
@@ -3511,9 +5712,9 @@ async function doRunSessionRechain(options, ctx) {
3511
5712
  }
3512
5713
  const cwd = ctx.cwd ?? process.cwd();
3513
5714
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
3514
- const paths = basouPaths10(repositoryRoot);
3515
- await assertWorkspaceInitialized8(paths.root);
3516
- const sessionIds = options.session !== void 0 ? [await resolveSessionId2(paths, options.session)] : await enumerateSessionDirs2(paths);
5715
+ const paths = basouPaths14(repositoryRoot);
5716
+ await assertWorkspaceInitialized10(paths.root);
5717
+ const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
3517
5718
  const dryRun = options.dryRun === true;
3518
5719
  const rows = [];
3519
5720
  for (const sessionId of sessionIds) {
@@ -3565,11 +5766,11 @@ function renderRechainRow(row, dryRun) {
3565
5766
 
3566
5767
  // src/commands/stats.ts
3567
5768
  import {
3568
- assertBasouRootSafe as assertBasouRootSafe11,
3569
- basouPaths as basouPaths11,
5769
+ assertBasouRootSafe as assertBasouRootSafe13,
5770
+ basouPaths as basouPaths15,
3570
5771
  computeWorkStats,
3571
- findErrorCode as findErrorCode10,
3572
- resolveRepositoryRoot as resolveRepositoryRoot12
5772
+ findErrorCode as findErrorCode12,
5773
+ resolveRepositoryRoot as resolveRepositoryRoot10
3573
5774
  } from "@basou/core";
3574
5775
  function registerStatsCommand(program) {
3575
5776
  program.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 +5788,8 @@ async function runStats(options, ctx = {}) {
3587
5788
  async function doRunStats(options, ctx) {
3588
5789
  const cwd = ctx.cwd ?? process.cwd();
3589
5790
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
3590
- const paths = basouPaths11(repositoryRoot);
3591
- await assertWorkspaceInitialized9(paths.root);
5791
+ const paths = basouPaths15(repositoryRoot);
5792
+ await assertWorkspaceInitialized11(paths.root);
3592
5793
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
3593
5794
  const result = await computeWorkStats({
3594
5795
  paths,
@@ -3672,7 +5873,7 @@ function formatInt(n) {
3672
5873
  }
3673
5874
  async function resolveRepositoryRootForStats(cwd) {
3674
5875
  try {
3675
- return await resolveRepositoryRoot12(cwd);
5876
+ return await resolveRepositoryRoot10(cwd);
3676
5877
  } catch (error) {
3677
5878
  if (error instanceof Error && error.message === "Not a git repository") {
3678
5879
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -3682,11 +5883,11 @@ async function resolveRepositoryRootForStats(cwd) {
3682
5883
  throw error;
3683
5884
  }
3684
5885
  }
3685
- async function assertWorkspaceInitialized9(basouRoot) {
5886
+ async function assertWorkspaceInitialized11(basouRoot) {
3686
5887
  try {
3687
- await assertBasouRootSafe11(basouRoot);
5888
+ await assertBasouRootSafe13(basouRoot);
3688
5889
  } catch (error) {
3689
- if (findErrorCode10(error, "ENOENT")) {
5890
+ if (findErrorCode12(error, "ENOENT")) {
3690
5891
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3691
5892
  }
3692
5893
  throw error;
@@ -3695,12 +5896,12 @@ async function assertWorkspaceInitialized9(basouRoot) {
3695
5896
 
3696
5897
  // src/commands/status.ts
3697
5898
  import {
3698
- assertBasouRootSafe as assertBasouRootSafe12,
3699
- basouPaths as basouPaths12,
5899
+ assertBasouRootSafe as assertBasouRootSafe14,
5900
+ basouPaths as basouPaths16,
3700
5901
  buildStatusSnapshot,
3701
- findErrorCode as findErrorCode11,
3702
- readManifest as readManifest6,
3703
- resolveRepositoryRoot as resolveRepositoryRoot13,
5902
+ findErrorCode as findErrorCode13,
5903
+ readManifest as readManifest8,
5904
+ resolveRepositoryRoot as resolveRepositoryRoot11,
3704
5905
  writeStatus
3705
5906
  } from "@basou/core";
3706
5907
  function registerStatusCommand(program) {
@@ -3719,20 +5920,20 @@ async function runStatus(options, ctx = {}) {
3719
5920
  async function doRunStatus(options, ctx) {
3720
5921
  const cwd = ctx.cwd ?? process.cwd();
3721
5922
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
3722
- const paths = basouPaths12(repositoryRoot);
5923
+ const paths = basouPaths16(repositoryRoot);
3723
5924
  try {
3724
- await assertBasouRootSafe12(paths.root);
5925
+ await assertBasouRootSafe14(paths.root);
3725
5926
  } catch (error) {
3726
- if (findErrorCode11(error, "ENOENT")) {
5927
+ if (findErrorCode13(error, "ENOENT")) {
3727
5928
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3728
5929
  }
3729
5930
  throw error;
3730
5931
  }
3731
5932
  let manifest;
3732
5933
  try {
3733
- manifest = await readManifest6(paths);
5934
+ manifest = await readManifest8(paths);
3734
5935
  } catch (error) {
3735
- if (findErrorCode11(error, "ENOENT")) {
5936
+ if (findErrorCode13(error, "ENOENT")) {
3736
5937
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3737
5938
  }
3738
5939
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -3756,7 +5957,7 @@ function renderTextStatus(s) {
3756
5957
  }
3757
5958
  async function resolveRepositoryRootForStatus(cwd) {
3758
5959
  try {
3759
- return await resolveRepositoryRoot13(cwd);
5960
+ return await resolveRepositoryRoot11(cwd);
3760
5961
  } catch (error) {
3761
5962
  if (error instanceof Error && error.message === "Not a git repository") {
3762
5963
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -3769,34 +5970,34 @@ async function resolveRepositoryRootForStatus(cwd) {
3769
5970
 
3770
5971
  // src/commands/task.ts
3771
5972
  import { readFile as readFile3 } from "fs/promises";
3772
- import { join as join7 } from "path";
5973
+ import { join as join9 } from "path";
3773
5974
  import {
3774
5975
  archiveTask,
3775
- assertBasouRootSafe as assertBasouRootSafe13,
3776
- basouPaths as basouPaths13,
5976
+ assertBasouRootSafe as assertBasouRootSafe15,
5977
+ basouPaths as basouPaths17,
3777
5978
  createTaskWithEvent,
3778
5979
  deleteTask,
3779
5980
  editTask,
3780
5981
  enumerateArchivedTaskIds,
3781
- findErrorCode as findErrorCode12,
5982
+ findErrorCode as findErrorCode14,
3782
5983
  loadSessionEntries as loadSessionEntries2,
3783
5984
  loadTaskEntries,
3784
5985
  prefixedUlid as prefixedUlid5,
3785
- readManifest as readManifest7,
5986
+ readManifest as readManifest9,
3786
5987
  readTaskFile,
3787
5988
  readTaskFileWithArchiveFallback,
3788
5989
  reconcileAllTasks,
3789
5990
  reconcileTask,
3790
5991
  refreshTaskLinkedSessions,
3791
5992
  replayEvents as replayEvents2,
3792
- resolveRepositoryRoot as resolveRepositoryRoot14,
3793
- resolveSessionId as resolveSessionId3,
5993
+ resolveRepositoryRoot as resolveRepositoryRoot12,
5994
+ resolveSessionId as resolveSessionId4,
3794
5995
  resolveTaskId as resolveTaskId2,
3795
5996
  TaskStatusSchema,
3796
5997
  TaskWriteAfterEventError,
3797
5998
  updateTaskStatusWithEvent
3798
5999
  } from "@basou/core";
3799
- import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
6000
+ import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
3800
6001
  var STATUS_VALUES3 = TaskStatusSchema.options;
3801
6002
  function registerTaskCommand(program) {
3802
6003
  const task = program.command("task").description("Manage Basou tasks (purpose units that span sessions)");
@@ -3875,14 +6076,14 @@ async function doRunTaskNew(options, ctx) {
3875
6076
  }
3876
6077
  const cwd = ctx.cwd ?? process.cwd();
3877
6078
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
3878
- const paths = basouPaths13(repositoryRoot);
3879
- await assertWorkspaceInitialized10(paths.root);
6079
+ const paths = basouPaths17(repositoryRoot);
6080
+ await assertWorkspaceInitialized12(paths.root);
3880
6081
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
3881
6082
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
3882
6083
  const occurredAt = now.toISOString();
3883
6084
  const taskId = prefixedUlid5("task");
3884
6085
  if (options.session !== void 0) {
3885
- const sessionId = await resolveSessionId3(paths, options.session);
6086
+ const sessionId = await resolveSessionId4(paths, options.session);
3886
6087
  const result2 = await createTaskWithEvent({
3887
6088
  mode: "attach",
3888
6089
  paths,
@@ -3910,7 +6111,7 @@ async function doRunTaskNew(options, ctx) {
3910
6111
  });
3911
6112
  return;
3912
6113
  }
3913
- const manifest = await readManifest7(paths);
6114
+ const manifest = await readManifest9(paths);
3914
6115
  const result = await createTaskWithEvent({
3915
6116
  mode: "ad-hoc",
3916
6117
  paths,
@@ -3984,8 +6185,8 @@ async function runTaskList(options, ctx = {}) {
3984
6185
  async function doRunTaskList(options, ctx) {
3985
6186
  const cwd = ctx.cwd ?? process.cwd();
3986
6187
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
3987
- const paths = basouPaths13(repositoryRoot);
3988
- await assertWorkspaceInitialized10(paths.root);
6188
+ const paths = basouPaths17(repositoryRoot);
6189
+ await assertWorkspaceInitialized12(paths.root);
3989
6190
  const entries = await loadTaskEntries(paths, {
3990
6191
  onSkip: (id, reason) => printTaskSkip(id, reason)
3991
6192
  });
@@ -4088,15 +6289,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
4088
6289
  async function doRunTaskShow(idInput, options, ctx) {
4089
6290
  const cwd = ctx.cwd ?? process.cwd();
4090
6291
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
4091
- const paths = basouPaths13(repositoryRoot);
4092
- await assertWorkspaceInitialized10(paths.root);
6292
+ const paths = basouPaths17(repositoryRoot);
6293
+ await assertWorkspaceInitialized12(paths.root);
4093
6294
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
4094
6295
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
4095
6296
  const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
4096
6297
  const events = [];
4097
6298
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
4098
6299
  for (const s of sessions) {
4099
- const sessionDir = join7(paths.sessions, s.sessionId);
6300
+ const sessionDir = join9(paths.sessions, s.sessionId);
4100
6301
  try {
4101
6302
  for await (const ev of replayEvents2(sessionDir, {
4102
6303
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -4232,13 +6433,13 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
4232
6433
  const newStatus = parseTaskStatusPositional(newStatusInput);
4233
6434
  const cwd = ctx.cwd ?? process.cwd();
4234
6435
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
4235
- const paths = basouPaths13(repositoryRoot);
4236
- await assertWorkspaceInitialized10(paths.root);
6436
+ const paths = basouPaths17(repositoryRoot);
6437
+ await assertWorkspaceInitialized12(paths.root);
4237
6438
  const taskId = await resolveTaskId2(paths, taskIdInput);
4238
6439
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4239
6440
  const occurredAt = now.toISOString();
4240
6441
  if (options.session !== void 0) {
4241
- const sessionId = await resolveSessionId3(paths, options.session);
6442
+ const sessionId = await resolveSessionId4(paths, options.session);
4242
6443
  const result2 = await updateTaskStatusWithEvent({
4243
6444
  mode: "attach",
4244
6445
  paths,
@@ -4258,7 +6459,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
4258
6459
  });
4259
6460
  return;
4260
6461
  }
4261
- const manifest = await readManifest7(paths);
6462
+ const manifest = await readManifest9(paths);
4262
6463
  const result = await updateTaskStatusWithEvent({
4263
6464
  mode: "ad-hoc",
4264
6465
  paths,
@@ -4309,9 +6510,9 @@ async function runTaskReconcile(options, ctx = {}) {
4309
6510
  async function doRunTaskReconcile(options, ctx) {
4310
6511
  const cwd = ctx.cwd ?? process.cwd();
4311
6512
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
4312
- const paths = basouPaths13(repositoryRoot);
4313
- await assertWorkspaceInitialized10(paths.root);
4314
- const manifest = await readManifest7(paths);
6513
+ const paths = basouPaths17(repositoryRoot);
6514
+ await assertWorkspaceInitialized12(paths.root);
6515
+ const manifest = await readManifest9(paths);
4315
6516
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
4316
6517
  const write = options.write === true;
4317
6518
  const verbose = isVerbose(options);
@@ -4489,9 +6690,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
4489
6690
  }
4490
6691
  const cwd = ctx.cwd ?? process.cwd();
4491
6692
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
4492
- const paths = basouPaths13(repositoryRoot);
4493
- await assertWorkspaceInitialized10(paths.root);
4494
- const manifest = await readManifest7(paths);
6693
+ const paths = basouPaths17(repositoryRoot);
6694
+ await assertWorkspaceInitialized12(paths.root);
6695
+ const manifest = await readManifest9(paths);
4495
6696
  const taskId = await resolveTaskId2(paths, taskIdInput);
4496
6697
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
4497
6698
  const write = options.write === true;
@@ -4569,9 +6770,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
4569
6770
  }
4570
6771
  const cwd = ctx.cwd ?? process.cwd();
4571
6772
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
4572
- const paths = basouPaths13(repositoryRoot);
4573
- await assertWorkspaceInitialized10(paths.root);
4574
- const manifest = await readManifest7(paths);
6773
+ const paths = basouPaths17(repositoryRoot);
6774
+ await assertWorkspaceInitialized12(paths.root);
6775
+ const manifest = await readManifest9(paths);
4575
6776
  const taskId = await resolveTaskId2(paths, taskIdInput);
4576
6777
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4577
6778
  const occurredAt = now.toISOString();
@@ -4625,9 +6826,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
4625
6826
  }
4626
6827
  const cwd = ctx.cwd ?? process.cwd();
4627
6828
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
4628
- const paths = basouPaths13(repositoryRoot);
4629
- await assertWorkspaceInitialized10(paths.root);
4630
- const manifest = await readManifest7(paths);
6829
+ const paths = basouPaths17(repositoryRoot);
6830
+ await assertWorkspaceInitialized12(paths.root);
6831
+ const manifest = await readManifest9(paths);
4631
6832
  const taskId = await resolveTaskId2(paths, taskIdInput);
4632
6833
  if (options.yes !== true) {
4633
6834
  await confirmDestructiveAction("delete", taskId);
@@ -4670,9 +6871,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
4670
6871
  }
4671
6872
  const cwd = ctx.cwd ?? process.cwd();
4672
6873
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
4673
- const paths = basouPaths13(repositoryRoot);
4674
- await assertWorkspaceInitialized10(paths.root);
4675
- const manifest = await readManifest7(paths);
6874
+ const paths = basouPaths17(repositoryRoot);
6875
+ await assertWorkspaceInitialized12(paths.root);
6876
+ const manifest = await readManifest9(paths);
4676
6877
  const taskId = await resolveTaskId2(paths, taskIdInput);
4677
6878
  if (options.yes !== true) {
4678
6879
  await confirmDestructiveAction("archive", taskId);
@@ -4725,20 +6926,20 @@ async function readSingleLineFromStdin() {
4725
6926
  }
4726
6927
  function parseTitle2(raw) {
4727
6928
  if (raw.length === 0) {
4728
- throw new InvalidArgumentError4("Title must not be empty");
6929
+ throw new InvalidArgumentError6("Title must not be empty");
4729
6930
  }
4730
6931
  return raw;
4731
6932
  }
4732
6933
  function parseLabel(raw) {
4733
6934
  if (raw.length === 0) {
4734
- throw new InvalidArgumentError4("Label must not be empty");
6935
+ throw new InvalidArgumentError6("Label must not be empty");
4735
6936
  }
4736
6937
  return raw;
4737
6938
  }
4738
6939
  function parseInitialTaskStatus(raw) {
4739
6940
  const result = TaskStatusSchema.safeParse(raw);
4740
6941
  if (!result.success) {
4741
- throw new InvalidArgumentError4(
6942
+ throw new InvalidArgumentError6(
4742
6943
  `Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
4743
6944
  );
4744
6945
  }
@@ -4747,7 +6948,7 @@ function parseInitialTaskStatus(raw) {
4747
6948
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
4748
6949
  function parseIsoTimestampOption(raw) {
4749
6950
  if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
4750
- throw new InvalidArgumentError4(
6951
+ throw new InvalidArgumentError6(
4751
6952
  "Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
4752
6953
  );
4753
6954
  }
@@ -4756,7 +6957,7 @@ function parseIsoTimestampOption(raw) {
4756
6957
  function parseTaskStatusFilter(raw) {
4757
6958
  const result = TaskStatusSchema.safeParse(raw);
4758
6959
  if (!result.success) {
4759
- throw new InvalidArgumentError4(
6960
+ throw new InvalidArgumentError6(
4760
6961
  `Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
4761
6962
  );
4762
6963
  }
@@ -4771,14 +6972,14 @@ function parseTaskStatusPositional(raw) {
4771
6972
  }
4772
6973
  function parseDescriptionOption(raw) {
4773
6974
  if (raw.length === 0) {
4774
- throw new InvalidArgumentError4("Description must not be empty");
6975
+ throw new InvalidArgumentError6("Description must not be empty");
4775
6976
  }
4776
6977
  return raw;
4777
6978
  }
4778
6979
  function parsePositiveInt2(raw) {
4779
6980
  const n = Number.parseInt(raw, 10);
4780
6981
  if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
4781
- throw new InvalidArgumentError4(`Invalid number: ${raw}`);
6982
+ throw new InvalidArgumentError6(`Invalid number: ${raw}`);
4782
6983
  }
4783
6984
  return n;
4784
6985
  }
@@ -4786,10 +6987,10 @@ async function readDescriptionFile(path) {
4786
6987
  try {
4787
6988
  return await readFile3(path, "utf8");
4788
6989
  } catch (error) {
4789
- if (findErrorCode12(error, "ENOENT")) {
6990
+ if (findErrorCode14(error, "ENOENT")) {
4790
6991
  throw new Error("Description source not found", { cause: error });
4791
6992
  }
4792
- if (findErrorCode12(error, "EISDIR")) {
6993
+ if (findErrorCode14(error, "EISDIR")) {
4793
6994
  throw new Error("Description source is not a file", { cause: error });
4794
6995
  }
4795
6996
  throw new Error("Failed to read description source", { cause: error });
@@ -4797,7 +6998,7 @@ async function readDescriptionFile(path) {
4797
6998
  }
4798
6999
  async function resolveRepositoryRootForTask(cwd, subcmd) {
4799
7000
  try {
4800
- return await resolveRepositoryRoot14(cwd);
7001
+ return await resolveRepositoryRoot12(cwd);
4801
7002
  } catch (error) {
4802
7003
  if (error instanceof Error && error.message === "Not a git repository") {
4803
7004
  throw new Error(
@@ -4808,11 +7009,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
4808
7009
  throw error;
4809
7010
  }
4810
7011
  }
4811
- async function assertWorkspaceInitialized10(basouRoot) {
7012
+ async function assertWorkspaceInitialized12(basouRoot) {
4812
7013
  try {
4813
- await assertBasouRootSafe13(basouRoot);
7014
+ await assertBasouRootSafe15(basouRoot);
4814
7015
  } catch (error) {
4815
- if (findErrorCode12(error, "ENOENT")) {
7016
+ if (findErrorCode14(error, "ENOENT")) {
4816
7017
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4817
7018
  }
4818
7019
  throw error;
@@ -4900,12 +7101,12 @@ function maxLen3(values, floor) {
4900
7101
 
4901
7102
  // src/commands/verify.ts
4902
7103
  import {
4903
- assertBasouRootSafe as assertBasouRootSafe14,
4904
- basouPaths as basouPaths14,
7104
+ assertBasouRootSafe as assertBasouRootSafe16,
7105
+ basouPaths as basouPaths18,
4905
7106
  enumerateSessionDirs as enumerateSessionDirs3,
4906
- findErrorCode as findErrorCode13,
4907
- resolveRepositoryRoot as resolveRepositoryRoot15,
4908
- resolveSessionId as resolveSessionId4,
7107
+ findErrorCode as findErrorCode15,
7108
+ resolveRepositoryRoot as resolveRepositoryRoot13,
7109
+ resolveSessionId as resolveSessionId5,
4909
7110
  verifyEventsChain
4910
7111
  } from "@basou/core";
4911
7112
  function registerVerifyCommand(program) {
@@ -4927,9 +7128,9 @@ async function doRunVerify(options, ctx) {
4927
7128
  }
4928
7129
  const cwd = ctx.cwd ?? process.cwd();
4929
7130
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
4930
- const paths = basouPaths14(repositoryRoot);
4931
- await assertWorkspaceInitialized11(paths.root);
4932
- const sessionIds = options.session !== void 0 ? [await resolveSessionId4(paths, options.session)] : await enumerateSessionDirs3(paths);
7131
+ const paths = basouPaths18(repositoryRoot);
7132
+ await assertWorkspaceInitialized13(paths.root);
7133
+ const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
4933
7134
  const rows = [];
4934
7135
  for (const sessionId of sessionIds) {
4935
7136
  const verdict = await verifyEventsChain(paths, sessionId);
@@ -4975,7 +7176,7 @@ function renderVerdict(row) {
4975
7176
  }
4976
7177
  async function resolveRepositoryRootForVerify(cwd) {
4977
7178
  try {
4978
- return await resolveRepositoryRoot15(cwd);
7179
+ return await resolveRepositoryRoot13(cwd);
4979
7180
  } catch (error) {
4980
7181
  if (error instanceof Error && error.message === "Not a git repository") {
4981
7182
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -4985,11 +7186,11 @@ async function resolveRepositoryRootForVerify(cwd) {
4985
7186
  throw error;
4986
7187
  }
4987
7188
  }
4988
- async function assertWorkspaceInitialized11(basouRoot) {
7189
+ async function assertWorkspaceInitialized13(basouRoot) {
4989
7190
  try {
4990
- await assertBasouRootSafe14(basouRoot);
7191
+ await assertBasouRootSafe16(basouRoot);
4991
7192
  } catch (error) {
4992
- if (findErrorCode13(error, "ENOENT")) {
7193
+ if (findErrorCode15(error, "ENOENT")) {
4993
7194
  throw new Error("Workspace not initialized. Run 'basou init' first.");
4994
7195
  }
4995
7196
  throw error;
@@ -4998,27 +7199,162 @@ async function assertWorkspaceInitialized11(basouRoot) {
4998
7199
 
4999
7200
  // src/commands/view.ts
5000
7201
  import { spawn } from "child_process";
5001
- import { assertBasouRootSafe as assertBasouRootSafe15, basouPaths as basouPaths15, findErrorCode as findErrorCode15, resolveRepositoryRoot as resolveRepositoryRoot16 } from "@basou/core";
5002
- import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
7202
+ import { createHash } from "crypto";
7203
+ import { basename as basename5, resolve as resolve7 } from "path";
7204
+ import {
7205
+ assertBasouRootSafe as assertBasouRootSafe17,
7206
+ basouPaths as basouPaths19,
7207
+ findErrorCode as findErrorCode17,
7208
+ readManifest as readManifest12,
7209
+ resolveRepositoryRoot as resolveRepositoryRoot14
7210
+ } from "@basou/core";
7211
+ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
7212
+
7213
+ // src/lib/portfolio-safety.ts
7214
+ import { execFile } from "child_process";
7215
+ import { lstat, realpath } from "fs/promises";
7216
+ import { isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve6 } from "path";
7217
+ import { promisify } from "util";
7218
+ import { readManifest as readManifest10 } from "@basou/core";
7219
+ var execFileAsync = promisify(execFile);
7220
+ function errorCode(error) {
7221
+ return error instanceof Error ? error.code : void 0;
7222
+ }
7223
+ async function canonical(p) {
7224
+ try {
7225
+ return await realpath(p);
7226
+ } catch {
7227
+ return resolve6(p);
7228
+ }
7229
+ }
7230
+ function isInside(child, parent) {
7231
+ const rel = relative4(parent, child);
7232
+ return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
7233
+ }
7234
+ function isBasouPath(p) {
7235
+ return p === ".basou" || p.startsWith(".basou/") || p.includes("/.basou/") || p.endsWith("/.basou");
7236
+ }
7237
+ async function inspectRepo(repoPath) {
7238
+ let hasEntry = false;
7239
+ try {
7240
+ await lstat(join10(repoPath, ".basou"));
7241
+ hasEntry = true;
7242
+ } catch (error) {
7243
+ if (errorCode(error) !== "ENOENT") {
7244
+ return {
7245
+ kind: "unverifiable",
7246
+ detail: `could not check for a .basou here (${errorCode(error) ?? "unknown error"}) \u2014 treat as unsafe`
7247
+ };
7248
+ }
7249
+ }
7250
+ try {
7251
+ const { stdout } = await execFileAsync("git", ["-C", repoPath, "ls-files", "-z"]);
7252
+ const tracked = stdout.split("\0").some((f) => f.length > 0 && isBasouPath(f));
7253
+ if (tracked) {
7254
+ return {
7255
+ kind: "footprint",
7256
+ detail: "a .basou/ entry is tracked by git here and would be pushed"
7257
+ };
7258
+ }
7259
+ } catch {
7260
+ }
7261
+ if (hasEntry) return { kind: "footprint", detail: "a .basou/ entry exists here" };
7262
+ return null;
7263
+ }
7264
+ async function checkPortfolioSafety(workspaces) {
7265
+ const findings = [];
7266
+ let monitoredReposChecked = 0;
7267
+ for (const ws of workspaces) {
7268
+ const wsReal = await canonical(ws.repoRoot);
7269
+ let sourceRoots = [];
7270
+ try {
7271
+ const manifest = await readManifest10(ws.paths);
7272
+ sourceRoots = manifest.import?.source_roots ?? [];
7273
+ } catch (error) {
7274
+ if (error instanceof Error && error.message === "YAML file not found") {
7275
+ sourceRoots = [];
7276
+ } else {
7277
+ findings.push({
7278
+ workspaceLabel: ws.label,
7279
+ workspaceRoot: ws.repoRoot,
7280
+ monitoredRepo: ws.repoRoot,
7281
+ kind: "unverifiable",
7282
+ detail: "the workspace manifest is present but unreadable \u2014 cannot determine which repos it monitors; treat as unsafe"
7283
+ });
7284
+ continue;
7285
+ }
7286
+ }
7287
+ const monitored = /* @__PURE__ */ new Map();
7288
+ for (const root of sourceRoots) {
7289
+ const display = resolve6(ws.repoRoot, root);
7290
+ const real = await canonical(display);
7291
+ if (real !== wsReal) monitored.set(real, display);
7292
+ }
7293
+ for (const [real, display] of monitored) {
7294
+ monitoredReposChecked++;
7295
+ if (isInside(wsReal, real)) {
7296
+ findings.push({
7297
+ workspaceLabel: ws.label,
7298
+ workspaceRoot: ws.repoRoot,
7299
+ monitoredRepo: display,
7300
+ kind: "overlap",
7301
+ detail: "the workspace (where .basou/ is written) is inside this monitored repo"
7302
+ });
7303
+ }
7304
+ const inspection = await inspectRepo(real);
7305
+ if (inspection !== null) {
7306
+ findings.push({
7307
+ workspaceLabel: ws.label,
7308
+ workspaceRoot: ws.repoRoot,
7309
+ monitoredRepo: display,
7310
+ kind: inspection.kind,
7311
+ detail: inspection.detail
7312
+ });
7313
+ }
7314
+ }
7315
+ }
7316
+ return { findings, workspacesChecked: workspaces.length, monitoredReposChecked };
7317
+ }
7318
+ function formatSafetyReport(result) {
7319
+ if (result.findings.length === 0) {
7320
+ if (result.monitoredReposChecked === 0) {
7321
+ return [
7322
+ `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).`
7323
+ ];
7324
+ }
7325
+ return [
7326
+ `Portfolio safety: OK. ${result.workspacesChecked} workspace(s), ${result.monitoredReposChecked} monitored repo(s) checked \u2014 no .basou footprint, no overlap.`
7327
+ ];
7328
+ }
7329
+ const lines = [`Portfolio safety: DANGER \u2014 ${result.findings.length} finding(s):`];
7330
+ for (const f of result.findings) {
7331
+ lines.push(` [${f.kind}] ${f.monitoredRepo} (workspace "${f.workspaceLabel}"): ${f.detail}`);
7332
+ }
7333
+ lines.push(
7334
+ "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."
7335
+ );
7336
+ return lines;
7337
+ }
5003
7338
 
5004
7339
  // src/lib/view-server.ts
5005
7340
  import { createServer } from "http";
5006
- import { join as join8 } from "path";
7341
+ import { join as join11 } from "path";
5007
7342
  import {
5008
7343
  computeWorkStats as computeWorkStats2,
5009
7344
  enumerateApprovals as enumerateApprovals2,
5010
- findErrorCode as findErrorCode14,
7345
+ findErrorCode as findErrorCode16,
5011
7346
  isLazyExpired as isLazyExpired2,
5012
7347
  loadApproval as loadApproval2,
5013
7348
  loadSessionEntries as loadSessionEntries3,
5014
7349
  loadTaskEntries as loadTaskEntries2,
5015
7350
  readAllEvents as readAllEvents2,
5016
- readManifest as readManifest8,
5017
- readMarkdownFile as readMarkdownFile4,
7351
+ readManifest as readManifest11,
7352
+ readMarkdownFile as readMarkdownFile5,
5018
7353
  readSessionYaml as readSessionYaml3,
5019
7354
  readTaskFile as readTaskFile2,
5020
7355
  renderDecisions as renderDecisions3,
5021
- renderHandoff as renderHandoff3
7356
+ renderHandoff as renderHandoff3,
7357
+ summarizeOrientation
5022
7358
  } from "@basou/core";
5023
7359
 
5024
7360
  // src/lib/view-ui.ts
@@ -5040,8 +7376,13 @@ var VIEW_HTML = `<!doctype html>
5040
7376
  button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
5041
7377
  button:disabled { opacity: .5; cursor: default; }
5042
7378
  label.chk { font-size: 13px; opacity: .85; }
7379
+ /* On the portfolio landing there is no selected workspace, so the per-workspace action bar is hidden. */
7380
+ body.landing #project, body.landing label.chk,
7381
+ body.landing #btn-refresh, body.landing #btn-import-claude, body.landing #btn-import-codex,
7382
+ body.landing #btn-gen-handoff, body.landing #btn-gen-decisions { display: none; }
5043
7383
  #status { padding: 6px 16px; font-size: 13px; min-height: 20px; border-bottom: 1px solid #8884; white-space: pre-wrap; }
5044
7384
  #status.err { color: #dc2626; }
7385
+ .err { color: #dc2626; }
5045
7386
  nav { display: flex; gap: 2px; padding: 6px 12px; border-bottom: 1px solid #8884; flex-wrap: wrap; }
5046
7387
  nav button { border: none; border-radius: 6px; background: transparent; }
5047
7388
  nav button.active { background: #2563eb22; font-weight: 600; }
@@ -5055,6 +7396,8 @@ var VIEW_HTML = `<!doctype html>
5055
7396
  #detail { padding: 12px 16px; overflow: auto; max-height: 80vh; }
5056
7397
  .badge { display: inline-block; padding: 0 6px; border-radius: 6px; background: #8882; font-size: 12px; }
5057
7398
  .badge.warn { background: #f59e0b33; }
7399
+ .badge.danger { background: #ef444433; }
7400
+ .badge.ok { background: #22c55e33; }
5058
7401
  pre { background: #8881; padding: 12px; border-radius: 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; }
5059
7402
  table.kv { border-collapse: collapse; }
5060
7403
  table.kv td { padding: 2px 10px 2px 0; vertical-align: top; }
@@ -5063,6 +7406,11 @@ var VIEW_HTML = `<!doctype html>
5063
7406
  .card { border: 1px solid #8884; border-radius: 8px; padding: 10px 14px; min-width: 120px; }
5064
7407
  .card .n { font-size: 22px; font-weight: 700; }
5065
7408
  .card .l { font-size: 12px; opacity: .7; }
7409
+ .pcard { min-width: 240px; max-width: 340px; }
7410
+ .pcard.open { cursor: pointer; }
7411
+ .pcard.open:hover { background: #8881; }
7412
+ .pcard .l { font-size: 14px; font-weight: 700; opacity: 1; margin-bottom: 4px; }
7413
+ .pcard .f { font-size: 13px; }
5066
7414
  .tl { border-left: 2px solid #8885; margin-left: 6px; padding-left: 12px; }
5067
7415
  .tl .ev { margin-bottom: 8px; }
5068
7416
  .tl .ev .t { font-size: 12px; opacity: .65; }
@@ -5072,6 +7420,7 @@ var VIEW_HTML = `<!doctype html>
5072
7420
  <body>
5073
7421
  <header>
5074
7422
  <h1>basou view</h1>
7423
+ <button id="btn-back" style="display:none">&larr; portfolio</button>
5075
7424
  <input type="text" id="project" placeholder="source root (optional override)" />
5076
7425
  <button class="primary" id="btn-refresh">Refresh all</button>
5077
7426
  <button id="btn-import-claude">Import claude-code</button>
@@ -5091,7 +7440,12 @@ var VIEW_HTML = `<!doctype html>
5091
7440
  <script>
5092
7441
  (function () {
5093
7442
  var TABS = ['overview', 'stats', 'sessions', 'tasks', 'decisions', 'approvals', 'handoff'];
5094
- var state = { tab: 'overview', repoRoot: '' };
7443
+ // base is the API prefix for the active workspace: '/api' in single mode,
7444
+ // '/api/ws/<key>' once a portfolio card is opened.
7445
+ // canAct gates the mutating action bar: true only when a concrete workspace
7446
+ // is active (single mode, or a portfolio card opened). It is the real safety
7447
+ // guard \u2014 body.landing also hides the buttons, but that is cosmetic.
7448
+ var state = { tab: 'overview', repoRoot: '', base: '/api', mode: 'single', wsKey: null, canAct: false };
5095
7449
 
5096
7450
  function $(id) { return document.getElementById(id); }
5097
7451
  function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
@@ -5155,7 +7509,15 @@ var VIEW_HTML = `<!doctype html>
5155
7509
  for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = busy;
5156
7510
  }
5157
7511
 
7512
+ // Enable the action bar only when a workspace is active; disabled buttons
7513
+ // cannot post to a stale/wrong workspace even if a CSS regression un-hides them.
7514
+ function updateActionBar() {
7515
+ var ids = ['btn-refresh', 'btn-import-claude', 'btn-import-codex', 'btn-gen-handoff', 'btn-gen-decisions'];
7516
+ for (var i = 0; i < ids.length; i++) $(ids[i]).disabled = !state.canAct;
7517
+ }
7518
+
5158
7519
  function post(path, label) {
7520
+ if (!state.canAct) { setStatus('Open a workspace first.', true); return; }
5159
7521
  setBusy(true);
5160
7522
  setStatus(label + '...', false);
5161
7523
  fetchJson(path, {
@@ -5174,7 +7536,9 @@ var VIEW_HTML = `<!doctype html>
5174
7536
  if (!data) return 'ok';
5175
7537
  if (data.claudeCode || data.codex) {
5176
7538
  return 'claude-code ' + imp(data.claudeCode) + ', codex ' + imp(data.codex)
5177
- + (data.handoff && data.handoff.status === 'generated' ? '; handoff+decisions regenerated' : '');
7539
+ + (data.handoff && data.handoff.status === 'generated'
7540
+ ? '; handoff regenerated, decisions: ' + (data.decisions ? data.decisions.decisionCount : 0)
7541
+ : '');
5178
7542
  }
5179
7543
  if (data.status === 'ran') return imp(data);
5180
7544
  if (data.status === 'skipped') return 'skipped (' + data.reason + ')';
@@ -5188,6 +7552,155 @@ var VIEW_HTML = `<!doctype html>
5188
7552
  return (o.dryRun ? 'would import ' : 'imported ') + o.importedCount + ' (' + o.eventTotal + ' events)';
5189
7553
  }
5190
7554
 
7555
+ // --- portfolio landing --------------------------------------------------
7556
+
7557
+ function boot() {
7558
+ fetchJson('/api/portfolio').then(function (d) {
7559
+ if (d && d.mode === 'portfolio') { state.mode = 'portfolio'; showLanding(d); }
7560
+ else { enterSingle(); }
7561
+ }).catch(function () {
7562
+ // First-load bootstrap failure: the single-workspace view is the safe default.
7563
+ enterSingle();
7564
+ });
7565
+ }
7566
+
7567
+ // Re-render the portfolio landing (the back button). Unlike boot(), a fetch
7568
+ // failure here keeps the inert landing and shows an error rather than silently
7569
+ // dropping into single mode pointed at the first workspace.
7570
+ function backToPortfolio() {
7571
+ enterLandingChrome();
7572
+ fetchJson('/api/portfolio').then(function (d) {
7573
+ if (d && d.workspaces) renderCards(d);
7574
+ else portfolioError('Portfolio unavailable.');
7575
+ }).catch(function (err) { portfolioError('Could not load portfolio: ' + err.message); });
7576
+ }
7577
+
7578
+ function enterSingle() {
7579
+ state.mode = 'single';
7580
+ state.base = '/api';
7581
+ state.wsKey = null;
7582
+ state.canAct = true;
7583
+ document.body.classList.remove('landing');
7584
+ $('btn-back').style.display = 'none';
7585
+ updateActionBar();
7586
+ buildTabs();
7587
+ loadTab('overview');
7588
+ }
7589
+
7590
+ // Landing chrome: no workspace is active, so actions are disabled (and hidden
7591
+ // by body.landing). The disable is the safety guard; the hide is cosmetic.
7592
+ function enterLandingChrome() {
7593
+ state.wsKey = null;
7594
+ state.canAct = false;
7595
+ document.body.classList.add('landing');
7596
+ $('btn-back').style.display = 'none';
7597
+ setStatus('', false);
7598
+ clear($('tabs'));
7599
+ updateActionBar();
7600
+ single(true);
7601
+ }
7602
+
7603
+ function showLanding(d) { enterLandingChrome(); renderCards(d); }
7604
+
7605
+ function renderCards(d) {
7606
+ var detail = $('detail');
7607
+ clear(detail);
7608
+ var ws = d.workspaces || [];
7609
+ detail.appendChild(el('p', { class: 'muted', text: 'Portfolio \u2014 ' + ws.length + ' workspace(s). Click a card to open it.' }));
7610
+ var cards = el('div', { class: 'cards' }, []);
7611
+ ws.forEach(function (w) { cards.appendChild(portfolioCard(w, d.generatedAt)); });
7612
+ detail.appendChild(cards);
7613
+ }
7614
+
7615
+ function portfolioError(msg) {
7616
+ var detail = $('detail');
7617
+ clear(detail);
7618
+ detail.appendChild(el('p', { class: 'err', text: msg }));
7619
+ detail.appendChild(el('button', { text: 'Retry', onclick: backToPortfolio }));
7620
+ }
7621
+
7622
+ function highestRisk(approvals) {
7623
+ var order = ['critical', 'high', 'medium', 'low'];
7624
+ for (var i = 0; i < order.length; i++) {
7625
+ for (var j = 0; j < approvals.length; j++) {
7626
+ if (approvals[j].risk === order[i]) return order[i];
7627
+ }
7628
+ }
7629
+ return approvals.length ? approvals[0].risk : '';
7630
+ }
7631
+
7632
+ // Human-readable age of an ISO timestamp relative to the portfolio's
7633
+ // generatedAt ("now"), so a stale capture reads as "3d ago" not a raw ISO.
7634
+ function relAge(iso, nowIso) {
7635
+ if (!iso) return '(none)';
7636
+ var ms = Date.parse(nowIso) - Date.parse(iso);
7637
+ if (!isFinite(ms)) return iso;
7638
+ if (ms < 60000) return 'just now';
7639
+ var m = Math.floor(ms / 60000); if (m < 60) return m + 'm ago';
7640
+ var h = Math.floor(m / 60); if (h < 48) return h + 'h ago';
7641
+ return Math.floor(h / 24) + 'd ago';
7642
+ }
7643
+
7644
+ // A "run refresh" badge when a dry-run found uncaptured/changed native sessions,
7645
+ // an "up to date" badge when the capture is current, and nothing loud when the
7646
+ // staleness probe could not run (degrades to a quiet note).
7647
+ function stalenessBadge(st) {
7648
+ if (!st) return null;
7649
+ if (!st.checked) return el('span', { class: 'badge', text: 'freshness unknown' });
7650
+ if (st.unverifiableSessions > 0)
7651
+ return el('span', { class: 'badge danger', text: '\u26A0 ' + st.unverifiableSessions + ' unverifiable \u2014 run verify' });
7652
+ if (st.newSessions > 0)
7653
+ return el('span', { class: 'badge danger', text: '\u26A0 ' + st.newSessions + ' uncaptured \u2014 run refresh' });
7654
+ if (st.updatedSessions > 0)
7655
+ return el('span', { class: 'badge warn', text: st.updatedSessions + ' updated \u2014 run refresh' });
7656
+ return el('span', { class: 'badge ok', text: 'up to date' });
7657
+ }
7658
+
7659
+ function portfolioCard(w, generatedAt) {
7660
+ if (!w.initialized) {
7661
+ return el('div', { class: 'card pcard muted' }, [
7662
+ el('div', { class: 'l', text: w.label }),
7663
+ el('div', { class: 'f', text: w.error ? ('unreadable: ' + w.error) : 'not initialized' })
7664
+ ]);
7665
+ }
7666
+ if (w.error) {
7667
+ return el('div', { class: 'card pcard' }, [
7668
+ el('div', { class: 'l', text: w.label }),
7669
+ el('div', { class: 'f' }, [el('span', { class: 'badge warn', text: 'unreadable: ' + w.error })])
7670
+ ]);
7671
+ }
7672
+ var pend = w.pendingApprovals || [];
7673
+ var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
7674
+ var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
7675
+ var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
7676
+ var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
7677
+ var badge = stalenessBadge(w.staleness);
7678
+ return el('div', { class: 'card pcard open', onclick: function () { openWorkspace(w.key, w.label); } }, [
7679
+ el('div', { class: 'l' }, [
7680
+ el('span', { text: w.label }),
7681
+ badge ? el('span', { text: ' ' }) : null,
7682
+ badge
7683
+ ]),
7684
+ el('div', { class: 'f', text: 'now: ' + now }),
7685
+ el('div', { class: 'f', text: 'latest: ' + dec }),
7686
+ el('div', { class: 'f', text: 'in-flight ' + w.inFlightCount + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
7687
+ el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
7688
+ ]);
7689
+ }
7690
+
7691
+ function openWorkspace(key, label) {
7692
+ state.mode = 'portfolio';
7693
+ state.wsKey = key;
7694
+ state.base = '/api/ws/' + encodeURIComponent(key);
7695
+ state.canAct = true;
7696
+ document.body.classList.remove('landing');
7697
+ $('btn-back').style.display = '';
7698
+ updateActionBar();
7699
+ setStatus('workspace: ' + label, false);
7700
+ buildTabs();
7701
+ loadTab('overview');
7702
+ }
7703
+
5191
7704
  // --- tabs ---------------------------------------------------------------
5192
7705
 
5193
7706
  function buildTabs() {
@@ -5211,16 +7724,16 @@ var VIEW_HTML = `<!doctype html>
5211
7724
  if (name === 'stats') return loadStats();
5212
7725
  if (name === 'sessions') return loadSessions();
5213
7726
  if (name === 'tasks') return loadTasks();
5214
- if (name === 'decisions') return loadMarkdown('/api/decisions', 'decisions');
7727
+ if (name === 'decisions') return loadMarkdown(state.base + '/decisions', 'decisions');
5215
7728
  if (name === 'approvals') return loadApprovals();
5216
- if (name === 'handoff') return loadMarkdown('/api/handoff', 'handoff');
7729
+ if (name === 'handoff') return loadMarkdown(state.base + '/handoff', 'handoff');
5217
7730
  }
5218
7731
 
5219
7732
  function fail(err) { setStatus(err.message, true); }
5220
7733
 
5221
7734
  function loadOverview() {
5222
7735
  single(true);
5223
- fetchJson('/api/overview').then(function (d) {
7736
+ fetchJson(state.base + '/overview').then(function (d) {
5224
7737
  var detail = $('detail');
5225
7738
  if (!d || d.initialized === false) {
5226
7739
  detail.appendChild(el('p', { class: 'muted', text: 'Workspace not initialized.' }));
@@ -5269,7 +7782,7 @@ var VIEW_HTML = `<!doctype html>
5269
7782
 
5270
7783
  function loadStats() {
5271
7784
  single(true);
5272
- fetchJson('/api/stats').then(function (d) {
7785
+ fetchJson(state.base + '/stats').then(function (d) {
5273
7786
  var detail = $('detail');
5274
7787
  var t = d.totals;
5275
7788
  detail.appendChild(el('p', { text: 'Sessions: ' + t.sessionCount }));
@@ -5330,7 +7843,7 @@ var VIEW_HTML = `<!doctype html>
5330
7843
 
5331
7844
  function loadSessions() {
5332
7845
  single(false);
5333
- fetchJson('/api/sessions').then(function (d) {
7846
+ fetchJson(state.base + '/sessions').then(function (d) {
5334
7847
  var list = $('list');
5335
7848
  var rows = (d && d.sessions) || [];
5336
7849
  if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no sessions' })); return; }
@@ -5349,7 +7862,7 @@ var VIEW_HTML = `<!doctype html>
5349
7862
  row.classList.add('active');
5350
7863
  var detail = $('detail');
5351
7864
  clear(detail);
5352
- fetchJson('/api/sessions/' + encodeURIComponent(id)).then(function (d) {
7865
+ fetchJson(state.base + '/sessions/' + encodeURIComponent(id)).then(function (d) {
5353
7866
  var s = d.session.session;
5354
7867
  detail.appendChild(el('h3', { text: s.label || id }));
5355
7868
  detail.appendChild(kv([
@@ -5382,7 +7895,7 @@ var VIEW_HTML = `<!doctype html>
5382
7895
 
5383
7896
  function loadTasks() {
5384
7897
  single(false);
5385
- fetchJson('/api/tasks').then(function (d) {
7898
+ fetchJson(state.base + '/tasks').then(function (d) {
5386
7899
  var list = $('list');
5387
7900
  var rows = (d && d.tasks) || [];
5388
7901
  if (rows.length === 0) { list.appendChild(el('div', { class: 'row muted', text: 'no tasks' })); return; }
@@ -5401,7 +7914,7 @@ var VIEW_HTML = `<!doctype html>
5401
7914
  row.classList.add('active');
5402
7915
  var detail = $('detail');
5403
7916
  clear(detail);
5404
- fetchJson('/api/tasks/' + encodeURIComponent(id)).then(function (d) {
7917
+ fetchJson(state.base + '/tasks/' + encodeURIComponent(id)).then(function (d) {
5405
7918
  detail.appendChild(el('h3', { text: (d.task && (d.task.title || d.task.label)) || id }));
5406
7919
  detail.appendChild(el('pre', { text: JSON.stringify(d.task, null, 2) }));
5407
7920
  if (d.body) detail.appendChild(el('pre', { text: d.body }));
@@ -5420,7 +7933,7 @@ var VIEW_HTML = `<!doctype html>
5420
7933
 
5421
7934
  function loadApprovals() {
5422
7935
  single(true);
5423
- fetchJson('/api/approvals').then(function (d) {
7936
+ fetchJson(state.base + '/approvals').then(function (d) {
5424
7937
  var detail = $('detail');
5425
7938
  var groups = [['pending', d.pending || []], ['resolved', d.resolved || []]];
5426
7939
  groups.forEach(function (g) {
@@ -5445,14 +7958,14 @@ var VIEW_HTML = `<!doctype html>
5445
7958
 
5446
7959
  // --- wire up ------------------------------------------------------------
5447
7960
 
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'); });
7961
+ $('btn-back').addEventListener('click', function () { backToPortfolio(); });
7962
+ $('btn-refresh').addEventListener('click', function () { post(state.base + '/refresh', 'Refresh all'); });
7963
+ $('btn-import-claude').addEventListener('click', function () { post(state.base + '/import/claude-code', 'Import claude-code'); });
7964
+ $('btn-import-codex').addEventListener('click', function () { post(state.base + '/import/codex', 'Import codex'); });
7965
+ $('btn-gen-handoff').addEventListener('click', function () { post(state.base + '/handoff/generate', 'Regenerate handoff'); });
7966
+ $('btn-gen-decisions').addEventListener('click', function () { post(state.base + '/decisions/generate', 'Regenerate decisions'); });
5453
7967
 
5454
- buildTabs();
5455
- loadTab('overview');
7968
+ boot();
5456
7969
  })();
5457
7970
  </script>
5458
7971
  </body>
@@ -5467,6 +7980,8 @@ var HttpError = class extends Error {
5467
7980
  status;
5468
7981
  };
5469
7982
  var MAX_BODY_BYTES = 64 * 1024;
7983
+ var API_PREFIX = "/api/";
7984
+ var WS_PREFIX = "/api/ws/";
5470
7985
  function startViewServer(opts) {
5471
7986
  const { port, host = "127.0.0.1", deps } = opts;
5472
7987
  let actionQueue = Promise.resolve();
@@ -5480,7 +7995,7 @@ function startViewServer(opts) {
5480
7995
  };
5481
7996
  let boundPort = port;
5482
7997
  const getPort = () => boundPort;
5483
- return new Promise((resolve4, reject) => {
7998
+ return new Promise((resolve8, reject) => {
5484
7999
  const server = createServer((req, res) => {
5485
8000
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
5486
8001
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -5491,7 +8006,7 @@ function startViewServer(opts) {
5491
8006
  const address = server.address();
5492
8007
  boundPort = isAddressInfo(address) ? address.port : port;
5493
8008
  server.off("error", reject);
5494
- resolve4({
8009
+ resolve8({
5495
8010
  url: `http://${host}:${boundPort}`,
5496
8011
  port: boundPort,
5497
8012
  close: () => closeServer(server)
@@ -5503,8 +8018,8 @@ function isAddressInfo(value) {
5503
8018
  return value !== null && typeof value === "object";
5504
8019
  }
5505
8020
  function closeServer(server) {
5506
- return new Promise((resolve4) => {
5507
- server.close(() => resolve4());
8021
+ return new Promise((resolve8) => {
8022
+ server.close(() => resolve8());
5508
8023
  server.closeAllConnections();
5509
8024
  });
5510
8025
  }
@@ -5536,90 +8051,204 @@ async function handleGet(res, pathname, deps) {
5536
8051
  sendHtml(res, VIEW_HTML);
5537
8052
  return;
5538
8053
  }
5539
- if (pathname === "/api/overview") {
5540
- sendJson(res, 200, await overview(deps));
8054
+ if (pathname === "/api/portfolio") {
8055
+ sendJson(res, 200, await portfolio(deps));
5541
8056
  return;
5542
8057
  }
5543
- if (pathname === "/api/sessions") {
5544
- sendJson(res, 200, await sessionsList(deps));
8058
+ const scoped = matchWsRoute(pathname);
8059
+ if (scoped !== null) {
8060
+ const ws = findWorkspace(deps, scoped.key);
8061
+ if (ws === null) {
8062
+ sendError(res, 404, "Unknown workspace");
8063
+ return;
8064
+ }
8065
+ if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
8066
+ sendError(res, 404, "Not found");
8067
+ }
5545
8068
  return;
5546
8069
  }
5547
- const sessionId = matchId(pathname, "/api/sessions/");
5548
- if (sessionId !== null) {
5549
- sendJson(res, 200, await sessionDetail(deps, sessionId));
8070
+ if (pathname.startsWith(API_PREFIX)) {
8071
+ const sub = pathname.slice(API_PREFIX.length);
8072
+ if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
8073
+ sendError(res, 404, "Not found");
8074
+ }
5550
8075
  return;
5551
8076
  }
5552
- if (pathname === "/api/tasks") {
5553
- sendJson(res, 200, await tasksList(deps));
8077
+ sendError(res, 404, "Not found");
8078
+ }
8079
+ async function handlePost(res, pathname, body, deps, runExclusive) {
8080
+ const scoped = matchWsRoute(pathname);
8081
+ if (scoped !== null) {
8082
+ const ws = findWorkspace(deps, scoped.key);
8083
+ if (ws === null) {
8084
+ sendError(res, 404, "Unknown workspace");
8085
+ return;
8086
+ }
8087
+ if (!await handleWorkspacePost(res, scoped.sub, ws, body, deps, runExclusive)) {
8088
+ sendError(res, 404, "Not found");
8089
+ }
5554
8090
  return;
5555
8091
  }
5556
- const taskId = matchId(pathname, "/api/tasks/");
5557
- if (taskId !== null) {
5558
- sendJson(res, 200, await taskDetail(deps, taskId));
8092
+ if (pathname.startsWith(API_PREFIX)) {
8093
+ const sub = pathname.slice(API_PREFIX.length);
8094
+ if (!await handleWorkspacePost(res, sub, primaryWorkspace(deps), body, deps, runExclusive)) {
8095
+ sendError(res, 404, "Not found");
8096
+ }
5559
8097
  return;
5560
8098
  }
5561
- if (pathname === "/api/decisions") {
5562
- sendJson(res, 200, await decisionsView(deps));
5563
- return;
8099
+ sendError(res, 404, "Not found");
8100
+ }
8101
+ async function handleWorkspaceGet(res, sub, ws, nowProvider) {
8102
+ if (sub === "overview") {
8103
+ sendJson(res, 200, await overview(ws, nowProvider));
8104
+ return true;
5564
8105
  }
5565
- if (pathname === "/api/approvals") {
5566
- sendJson(res, 200, await approvalsView(deps));
5567
- return;
8106
+ if (sub === "sessions") {
8107
+ sendJson(res, 200, await sessionsList(ws, nowProvider));
8108
+ return true;
5568
8109
  }
5569
- if (pathname === "/api/handoff") {
5570
- sendJson(res, 200, await handoffView(deps));
5571
- return;
8110
+ const sessionId = matchId(sub, "sessions/");
8111
+ if (sessionId !== null) {
8112
+ sendJson(res, 200, await sessionDetail(ws, sessionId));
8113
+ return true;
5572
8114
  }
5573
- if (pathname === "/api/stats") {
5574
- sendJson(res, 200, await computeWorkStats2({ paths: deps.paths, now: deps.nowProvider() }));
5575
- return;
8115
+ if (sub === "tasks") {
8116
+ sendJson(res, 200, await tasksList(ws));
8117
+ return true;
5576
8118
  }
5577
- sendError(res, 404, "Not found");
8119
+ const taskId = matchId(sub, "tasks/");
8120
+ if (taskId !== null) {
8121
+ sendJson(res, 200, await taskDetail(ws, taskId));
8122
+ return true;
8123
+ }
8124
+ if (sub === "decisions") {
8125
+ sendJson(res, 200, await decisionsView(ws, nowProvider));
8126
+ return true;
8127
+ }
8128
+ if (sub === "approvals") {
8129
+ sendJson(res, 200, await approvalsView(ws, nowProvider));
8130
+ return true;
8131
+ }
8132
+ if (sub === "handoff") {
8133
+ sendJson(res, 200, await handoffView(ws, nowProvider));
8134
+ return true;
8135
+ }
8136
+ if (sub === "stats") {
8137
+ sendJson(res, 200, await computeWorkStats2({ paths: ws.paths, now: nowProvider() }));
8138
+ return true;
8139
+ }
8140
+ return false;
5578
8141
  }
5579
- async function handlePost(res, pathname, body, deps, runExclusive) {
8142
+ async function handleWorkspacePost(res, sub, ws, body, deps, runExclusive) {
5580
8143
  const nowIso = deps.nowProvider().toISOString();
5581
8144
  const actionOptions = readActionOptions(body);
5582
- if (pathname === "/api/refresh") {
8145
+ if (sub === "refresh") {
5583
8146
  const result = await runExclusive(
5584
- () => refreshAll({ options: actionOptions, ctx: deps.importCtx, paths: deps.paths, nowIso })
8147
+ () => refreshAll({ options: actionOptions, ctx: ws.importCtx, paths: ws.paths, nowIso })
5585
8148
  );
5586
8149
  sendJson(res, 200, result);
5587
- return;
8150
+ return true;
5588
8151
  }
5589
- if (pathname === "/api/import/claude-code") {
5590
- sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, deps.importCtx)));
5591
- return;
8152
+ if (sub === "import/claude-code") {
8153
+ sendJson(res, 200, await runExclusive(() => importClaudeCode(actionOptions, ws.importCtx)));
8154
+ return true;
5592
8155
  }
5593
- if (pathname === "/api/import/codex") {
5594
- sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, deps.importCtx)));
5595
- return;
8156
+ if (sub === "import/codex") {
8157
+ sendJson(res, 200, await runExclusive(() => importCodex(actionOptions, ws.importCtx)));
8158
+ return true;
5596
8159
  }
5597
- if (pathname === "/api/handoff/generate") {
5598
- sendJson(res, 200, await runExclusive(() => regenerateHandoff(deps.paths, nowIso)));
5599
- return;
8160
+ if (sub === "handoff/generate") {
8161
+ sendJson(res, 200, await runExclusive(() => regenerateHandoff(ws.paths, nowIso)));
8162
+ return true;
5600
8163
  }
5601
- if (pathname === "/api/decisions/generate") {
5602
- sendJson(res, 200, await runExclusive(() => regenerateDecisions(deps.paths, nowIso)));
5603
- return;
8164
+ if (sub === "decisions/generate") {
8165
+ sendJson(res, 200, await runExclusive(() => regenerateDecisions(ws.paths, nowIso)));
8166
+ return true;
5604
8167
  }
5605
- sendError(res, 404, "Not found");
8168
+ return false;
8169
+ }
8170
+ function primaryWorkspace(deps) {
8171
+ const first = deps.workspaces[0];
8172
+ if (first === void 0) throw new HttpError(500, "No workspace configured");
8173
+ return first;
8174
+ }
8175
+ function findWorkspace(deps, key) {
8176
+ return deps.workspaces.find((w) => w.key === key) ?? null;
8177
+ }
8178
+ function matchWsRoute(pathname) {
8179
+ if (!pathname.startsWith(WS_PREFIX)) return null;
8180
+ const rest = pathname.slice(WS_PREFIX.length);
8181
+ const slash = rest.indexOf("/");
8182
+ if (slash <= 0) return null;
8183
+ const sub = rest.slice(slash + 1);
8184
+ if (sub.length === 0) return null;
8185
+ let key;
8186
+ try {
8187
+ key = decodeURIComponent(rest.slice(0, slash));
8188
+ } catch {
8189
+ return null;
8190
+ }
8191
+ if (key.length === 0 || key.includes("/") || key.includes("\0")) return null;
8192
+ return { key, sub };
8193
+ }
8194
+ async function portfolio(deps) {
8195
+ const nowIso = deps.nowProvider().toISOString();
8196
+ const workspaces = await Promise.all(deps.workspaces.map((ws) => portfolioCard(ws, nowIso)));
8197
+ for (let i = 0; i < deps.workspaces.length; i++) {
8198
+ const card = workspaces[i];
8199
+ const ws = deps.workspaces[i];
8200
+ if (ws !== void 0 && card !== void 0 && card.initialized === true && card.error === void 0) {
8201
+ card.staleness = await captureStaleness(ws, nowIso);
8202
+ }
8203
+ }
8204
+ return { mode: deps.mode, generatedAt: nowIso, workspaces };
8205
+ }
8206
+ async function portfolioCard(ws, nowIso) {
8207
+ const base = { key: ws.key, label: ws.label, repoRoot: ws.repoRoot };
8208
+ if (!ws.initialized) {
8209
+ return ws.manifestError !== void 0 ? { ...base, initialized: false, error: ws.manifestError } : { ...base, initialized: false };
8210
+ }
8211
+ try {
8212
+ const s = await summarizeOrientation({ paths: ws.paths, nowIso });
8213
+ return {
8214
+ ...base,
8215
+ initialized: true,
8216
+ sessionCount: s.sessionCount,
8217
+ suspectCount: s.suspects.length,
8218
+ inFlightCount: s.inFlightTasks.length,
8219
+ pendingApprovals: s.pendingApprovals.map((a) => ({
8220
+ risk: a.risk,
8221
+ kind: a.kind,
8222
+ expired: a.expired
8223
+ })),
8224
+ latestDecision: s.latestDecision !== null ? { title: s.latestDecision.title } : null,
8225
+ latestSession: s.latestSession !== null ? { label: s.latestSession.label, status: s.latestSession.status } : null,
8226
+ freshness: { newestStartedAt: s.freshness.newestStartedAt, bySource: s.freshness.bySource }
8227
+ };
8228
+ } catch (error) {
8229
+ return { ...base, initialized: true, error: pathlessMessage(error) };
8230
+ }
8231
+ }
8232
+ async function captureStaleness(ws, nowIso) {
8233
+ const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
8234
+ return probe === null ? { checked: false } : { checked: true, ...probe };
5606
8235
  }
5607
- async function overview(deps) {
8236
+ async function overview(ws, nowProvider) {
5608
8237
  let manifest;
5609
8238
  try {
5610
- manifest = await readManifest8(deps.paths);
8239
+ manifest = await readManifest11(ws.paths);
5611
8240
  } catch (error) {
5612
- if (findErrorCode14(error, "ENOENT")) {
5613
- return { initialized: false, repoRoot: deps.repoRoot };
8241
+ if (findErrorCode16(error, "ENOENT")) {
8242
+ return { initialized: false, repoRoot: ws.repoRoot };
5614
8243
  }
5615
8244
  throw error;
5616
8245
  }
5617
- const nowIso = deps.nowProvider().toISOString();
5618
- const handoff = await renderHandoff3({ paths: deps.paths, nowIso });
5619
- const approvals = await enumerateApprovals2(deps.paths);
8246
+ const nowIso = nowProvider().toISOString();
8247
+ const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
8248
+ const approvals = await enumerateApprovals2(ws.paths);
5620
8249
  return {
5621
8250
  initialized: true,
5622
- repoRoot: deps.repoRoot,
8251
+ repoRoot: ws.repoRoot,
5623
8252
  workspace: {
5624
8253
  id: manifest.workspace.id,
5625
8254
  name: manifest.workspace.name,
@@ -5637,8 +8266,8 @@ async function overview(deps) {
5637
8266
  generatedAt: nowIso
5638
8267
  };
5639
8268
  }
5640
- async function sessionsList(deps) {
5641
- const entries = await loadSessionEntries3(deps.paths, { now: deps.nowProvider() });
8269
+ async function sessionsList(ws, nowProvider) {
8270
+ const entries = await loadSessionEntries3(ws.paths, { now: nowProvider() });
5642
8271
  const sessions = entries.map((entry) => ({
5643
8272
  sessionId: entry.sessionId,
5644
8273
  label: entry.session.session.label ?? null,
@@ -5653,10 +8282,10 @@ async function sessionsList(deps) {
5653
8282
  })).reverse();
5654
8283
  return { sessions };
5655
8284
  }
5656
- async function sessionDetail(deps, sessionId) {
8285
+ async function sessionDetail(ws, sessionId) {
5657
8286
  let session;
5658
8287
  try {
5659
- session = await readSessionYaml3(deps.paths, sessionId);
8288
+ session = await readSessionYaml3(ws.paths, sessionId);
5660
8289
  } catch (error) {
5661
8290
  if (error instanceof Error && error.message === "YAML file not found") {
5662
8291
  throw new HttpError(404, "Session not found");
@@ -5664,19 +8293,19 @@ async function sessionDetail(deps, sessionId) {
5664
8293
  throw error;
5665
8294
  }
5666
8295
  try {
5667
- const events = await readAllEvents2(join8(deps.paths.sessions, sessionId));
8296
+ const events = await readAllEvents2(join11(ws.paths.sessions, sessionId));
5668
8297
  return { session, events };
5669
8298
  } catch {
5670
8299
  return { session, events: [], degraded: true };
5671
8300
  }
5672
8301
  }
5673
- async function tasksList(deps) {
5674
- const entries = await loadTaskEntries2(deps.paths);
8302
+ async function tasksList(ws) {
8303
+ const entries = await loadTaskEntries2(ws.paths);
5675
8304
  return { tasks: entries.map((entry) => entry.task).reverse() };
5676
8305
  }
5677
- async function taskDetail(deps, taskId) {
8306
+ async function taskDetail(ws, taskId) {
5678
8307
  try {
5679
- const doc = await readTaskFile2(deps.paths, taskId);
8308
+ const doc = await readTaskFile2(ws.paths, taskId);
5680
8309
  return { task: doc.task, body: doc.body };
5681
8310
  } catch (error) {
5682
8311
  if (error instanceof Error && error.message === "Task file not found") {
@@ -5685,22 +8314,22 @@ async function taskDetail(deps, taskId) {
5685
8314
  throw error;
5686
8315
  }
5687
8316
  }
5688
- async function decisionsView(deps) {
5689
- const fromDisk = await readMarkdownFile4(deps.paths.files.decisions);
8317
+ async function decisionsView(ws, nowProvider) {
8318
+ const fromDisk = await readMarkdownFile5(ws.paths.files.decisions);
5690
8319
  if (fromDisk !== null) {
5691
8320
  return { body: fromDisk, fromDisk: true };
5692
8321
  }
5693
- const nowIso = deps.nowProvider().toISOString();
5694
- const result = await renderDecisions3({ paths: deps.paths, nowIso });
8322
+ const nowIso = nowProvider().toISOString();
8323
+ const result = await renderDecisions3({ paths: ws.paths, nowIso });
5695
8324
  return { body: result.body, decisionCount: result.decisionCount, fromDisk: false };
5696
8325
  }
5697
- async function approvalsView(deps) {
5698
- const now = deps.nowProvider();
5699
- const ids = await enumerateApprovals2(deps.paths);
8326
+ async function approvalsView(ws, nowProvider) {
8327
+ const now = nowProvider();
8328
+ const ids = await enumerateApprovals2(ws.paths);
5700
8329
  const toViews = async (list) => {
5701
8330
  const views = [];
5702
8331
  for (const id of list) {
5703
- const loaded = await loadApproval2(deps.paths, id);
8332
+ const loaded = await loadApproval2(ws.paths, id);
5704
8333
  if (loaded === null) continue;
5705
8334
  views.push({ id, expired: isLazyExpired2(loaded.approval, now), approval: loaded.approval });
5706
8335
  }
@@ -5708,13 +8337,13 @@ async function approvalsView(deps) {
5708
8337
  };
5709
8338
  return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
5710
8339
  }
5711
- async function handoffView(deps) {
5712
- const fromDisk = await readMarkdownFile4(deps.paths.files.handoff);
8340
+ async function handoffView(ws, nowProvider) {
8341
+ const fromDisk = await readMarkdownFile5(ws.paths.files.handoff);
5713
8342
  if (fromDisk !== null) {
5714
8343
  return { body: fromDisk, fromDisk: true };
5715
8344
  }
5716
- const nowIso = deps.nowProvider().toISOString();
5717
- const result = await renderHandoff3({ paths: deps.paths, nowIso });
8345
+ const nowIso = nowProvider().toISOString();
8346
+ const result = await renderHandoff3({ paths: ws.paths, nowIso });
5718
8347
  return { body: result.body, fromDisk: false };
5719
8348
  }
5720
8349
  function readActionOptions(body) {
@@ -5804,12 +8433,22 @@ var DEFAULT_PORT = 4319;
5804
8433
  function parsePort(value) {
5805
8434
  const port = Number.parseInt(value, 10);
5806
8435
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
5807
- throw new InvalidArgumentError5("Port must be an integer between 1 and 65535.");
8436
+ throw new InvalidArgumentError7("Port must be an integer between 1 and 65535.");
5808
8437
  }
5809
8438
  return port;
5810
8439
  }
8440
+ function collectPath3(value, previous = []) {
8441
+ return [...previous, value];
8442
+ }
5811
8443
  function registerViewCommand(program) {
5812
- program.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) => {
8444
+ program.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(
8445
+ "--portfolio",
8446
+ "Serve every workspace listed in ~/.basou/portfolio.yaml (cross-repo orientation)"
8447
+ ).option(
8448
+ "--workspace <path>",
8449
+ "Workspace repo path to include (repeatable; implies portfolio mode; resolved against the cwd)",
8450
+ collectPath3
8451
+ ).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
8452
  await runView(options);
5814
8453
  });
5815
8454
  }
@@ -5823,23 +8462,37 @@ async function runView(options, ctx = {}) {
5823
8462
  }
5824
8463
  async function doRunView(options, ctx) {
5825
8464
  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
- };
8465
+ const workspaceFlags = options.workspace ?? [];
8466
+ const isPortfolio = workspaceFlags.length > 0 || options.portfolio === true;
8467
+ const deps = isPortfolio ? await buildPortfolioDeps(workspaceFlags, ctx, cwd) : await buildSingleDeps(ctx, cwd);
8468
+ if (options.check === true) {
8469
+ const result = await checkPortfolioSafety(deps.workspaces);
8470
+ for (const line of formatSafetyReport(result)) console.log(line);
8471
+ if (result.findings.length > 0) process.exitCode = 1;
8472
+ return;
8473
+ }
8474
+ if (deps.mode === "portfolio" && options.skipSafetyCheck !== true) {
8475
+ const result = await checkPortfolioSafety(deps.workspaces);
8476
+ const blocking = result.findings.filter((f) => f.kind === "footprint" || f.kind === "overlap");
8477
+ if (blocking.length > 0) {
8478
+ for (const line of formatSafetyReport(result)) console.error(line);
8479
+ throw new Error(
8480
+ "Portfolio safety preflight failed (see findings above). Fix the monitored repos, or re-run with --skip-safety-check to override."
8481
+ );
8482
+ }
8483
+ if (result.findings.length > 0) {
8484
+ console.error(
8485
+ `Portfolio safety: ${result.findings.length} unverifiable item(s) \u2014 the read-only view will still open; run 'basou view --check' for detail.`
8486
+ );
8487
+ }
8488
+ }
5839
8489
  const port = options.port ?? DEFAULT_PORT;
5840
8490
  const handle = await startListening(port, deps);
5841
8491
  try {
5842
8492
  console.log(`basou view running at ${handle.url}`);
8493
+ if (deps.mode === "portfolio") {
8494
+ console.log(`Portfolio mode: ${deps.workspaces.length} workspace(s).`);
8495
+ }
5843
8496
  console.log(
5844
8497
  "Localhost only, no authentication. Do not expose this port beyond your machine. Press Ctrl+C to stop."
5845
8498
  );
@@ -5852,11 +8505,69 @@ async function doRunView(options, ctx) {
5852
8505
  await handle.close();
5853
8506
  }
5854
8507
  }
8508
+ async function buildSingleDeps(ctx, cwd) {
8509
+ const repositoryRoot = await resolveRepositoryRootForView(cwd);
8510
+ const paths = basouPaths19(repositoryRoot);
8511
+ await assertWorkspaceInitialized14(paths.root);
8512
+ const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
8513
+ return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
8514
+ }
8515
+ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
8516
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve7(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
8517
+ const entries = [];
8518
+ const seenPath = /* @__PURE__ */ new Set();
8519
+ const seenKey = /* @__PURE__ */ new Set();
8520
+ for (const spec of specs) {
8521
+ const repoRoot = resolve7(spec.path);
8522
+ if (seenPath.has(repoRoot)) continue;
8523
+ seenPath.add(repoRoot);
8524
+ const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
8525
+ let key = entry.key;
8526
+ for (let n = 1; seenKey.has(key); n++) key = `${entry.key}-${n}`;
8527
+ seenKey.add(key);
8528
+ entries.push({ ...entry, key });
8529
+ }
8530
+ if (entries.length === 0) throw new Error("No workspaces to show.");
8531
+ return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
8532
+ }
8533
+ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
8534
+ const paths = basouPaths19(repoRoot);
8535
+ const importCtx = {
8536
+ cwd: repoRoot,
8537
+ ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
8538
+ ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
8539
+ };
8540
+ try {
8541
+ const manifest = await readManifest12(paths);
8542
+ return {
8543
+ key: manifest.workspace.id,
8544
+ label: labelOverride ?? manifest.workspace.name,
8545
+ paths,
8546
+ repoRoot,
8547
+ importCtx,
8548
+ initialized: true
8549
+ };
8550
+ } catch (error) {
8551
+ const notFound = error instanceof Error && error.message === "YAML file not found";
8552
+ return {
8553
+ key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
8554
+ label: labelOverride ?? basename5(repoRoot),
8555
+ paths,
8556
+ repoRoot,
8557
+ importCtx,
8558
+ initialized: false,
8559
+ ...notFound ? {} : { manifestError: "manifest unreadable or invalid" }
8560
+ };
8561
+ }
8562
+ }
8563
+ function nowProviderOf(ctx) {
8564
+ return ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
8565
+ }
5855
8566
  async function startListening(port, deps) {
5856
8567
  try {
5857
8568
  return await startViewServer({ port, deps });
5858
8569
  } catch (error) {
5859
- if (findErrorCode15(error, "EADDRINUSE")) {
8570
+ if (findErrorCode17(error, "EADDRINUSE")) {
5860
8571
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
5861
8572
  cause: error
5862
8573
  });
@@ -5879,7 +8590,7 @@ function openInBrowser(url, override) {
5879
8590
  }
5880
8591
  }
5881
8592
  function waitForShutdown(signal) {
5882
- return new Promise((resolve4) => {
8593
+ return new Promise((resolve8) => {
5883
8594
  const cleanup = () => {
5884
8595
  process.off("SIGINT", onSignal);
5885
8596
  process.off("SIGTERM", onSignal);
@@ -5887,18 +8598,18 @@ function waitForShutdown(signal) {
5887
8598
  };
5888
8599
  const onSignal = () => {
5889
8600
  cleanup();
5890
- resolve4();
8601
+ resolve8();
5891
8602
  };
5892
8603
  const onAbort = () => {
5893
8604
  cleanup();
5894
- resolve4();
8605
+ resolve8();
5895
8606
  };
5896
8607
  process.on("SIGINT", onSignal);
5897
8608
  process.on("SIGTERM", onSignal);
5898
8609
  if (signal !== void 0) {
5899
8610
  if (signal.aborted) {
5900
8611
  cleanup();
5901
- resolve4();
8612
+ resolve8();
5902
8613
  return;
5903
8614
  }
5904
8615
  signal.addEventListener("abort", onAbort);
@@ -5907,7 +8618,7 @@ function waitForShutdown(signal) {
5907
8618
  }
5908
8619
  async function resolveRepositoryRootForView(cwd) {
5909
8620
  try {
5910
- return await resolveRepositoryRoot16(cwd);
8621
+ return await resolveRepositoryRoot14(cwd);
5911
8622
  } catch (error) {
5912
8623
  if (error instanceof Error && error.message === "Not a git repository") {
5913
8624
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
@@ -5917,11 +8628,11 @@ async function resolveRepositoryRootForView(cwd) {
5917
8628
  throw error;
5918
8629
  }
5919
8630
  }
5920
- async function assertWorkspaceInitialized12(basouRoot) {
8631
+ async function assertWorkspaceInitialized14(basouRoot) {
5921
8632
  try {
5922
- await assertBasouRootSafe15(basouRoot);
8633
+ await assertBasouRootSafe17(basouRoot);
5923
8634
  } catch (error) {
5924
- if (findErrorCode15(error, "ENOENT")) {
8635
+ if (findErrorCode17(error, "ENOENT")) {
5925
8636
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5926
8637
  }
5927
8638
  throw error;
@@ -5947,10 +8658,14 @@ function buildProgram() {
5947
8658
  registerViewCommand(program);
5948
8659
  registerApprovalCommand(program);
5949
8660
  registerDecisionCommand(program);
8661
+ registerNoteCommand(program);
5950
8662
  registerTaskCommand(program);
5951
8663
  registerHandoffCommand(program);
5952
8664
  registerDecisionsCommand(program);
5953
8665
  registerReportCommand(program);
8666
+ registerOrientCommand(program);
8667
+ registerReviewGapsCommand(program);
8668
+ registerProjectCommand(program);
5954
8669
  return program;
5955
8670
  }
5956
8671
  export {