@basou/cli 0.12.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/index.js CHANGED
@@ -2008,11 +2008,177 @@ async function resolveRepositoryRootForInit(cwd) {
2008
2008
  }
2009
2009
  }
2010
2010
 
2011
- // src/commands/orient.ts
2011
+ // src/commands/note.ts
2012
2012
  import {
2013
+ acquireLock as acquireLock4,
2014
+ appendEventToExistingSession as appendEventToExistingSession2,
2013
2015
  assertBasouRootSafe as assertBasouRootSafe7,
2014
2016
  basouPaths as basouPaths7,
2017
+ createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
2015
2018
  findErrorCode as findErrorCode6,
2019
+ readManifest as readManifest4,
2020
+ resolveSessionId as resolveSessionId2
2021
+ } from "@basou/core";
2022
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
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(program2) {
2046
+ program2.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,
2016
2182
  renderOrientation as renderOrientation2,
2017
2183
  writeMarkdownFile as writeMarkdownFile4
2018
2184
  } from "@basou/core";
@@ -2163,126 +2329,1785 @@ async function refreshAll(args) {
2163
2329
  }
2164
2330
  }
2165
2331
  );
2166
- return {
2167
- claudeCode,
2168
- codex,
2169
- handoff: { status: "generated", ...handoffCounts },
2170
- decisions: { status: "generated", ...decisionCounts },
2171
- orientation: { status: "generated", ...orientationCounts },
2172
- dryRun
2173
- };
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(program2) {
2370
+ program2.command("orient").description("Show the workspace's current position (also writes .basou/orientation.md)").option("-q, --quiet", "Write the file without printing the body").option("-v, --verbose", "Show error causes").action(async (opts) => {
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(program2) {
2465
+ const project = program2.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");
2174
3517
  }
2175
- function wouldImport(outcome) {
2176
- return outcome.status === "ran" ? outcome.importedCount : 0;
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
+ }
2177
3525
  }
2178
- function wouldUpdate(outcome) {
2179
- return outcome.status === "ran" ? outcome.reimportedCount + outcome.replacedCount : 0;
3526
+ function canonicalFileFor(anchorReal, canonicalName) {
3527
+ return join4(anchorReal, "agents", canonicalName, CANONICAL_FILE);
2180
3528
  }
2181
- function wouldBlock(outcome) {
2182
- return outcome.status === "ran" ? outcome.skippedUnverifiable : 0;
3529
+ function canonicalLabelFor(canonicalName) {
3530
+ return join4("agents", canonicalName, CANONICAL_FILE);
2183
3531
  }
2184
- async function probeStaleness(args) {
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;
2185
3540
  try {
2186
- const dry = await refreshAll({
2187
- options: { dryRun: true },
2188
- ctx: args.ctx,
2189
- paths: args.paths,
2190
- nowIso: args.nowIso
2191
- });
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 {
2192
3556
  return {
2193
- newSessions: wouldImport(dry.claudeCode) + wouldImport(dry.codex),
2194
- updatedSessions: wouldUpdate(dry.claudeCode) + wouldUpdate(dry.codex),
2195
- unverifiableSessions: wouldBlock(dry.claudeCode) + wouldBlock(dry.codex)
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
2196
3572
  };
2197
- } catch {
2198
- return null;
2199
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
+ };
2200
3585
  }
2201
-
2202
- // src/lib/repo-root.ts
2203
- import { resolveBasouRepositoryRoot } from "@basou/core";
2204
- async function resolveBasouRootForCommand(cwd, commandName) {
3586
+ async function applyPresetPlan(anchorReal, plan) {
3587
+ const file = canonicalFileFor(anchorReal, plan.canonicalName);
3588
+ const label = canonicalLabelFor(plan.canonicalName);
3589
+ let isLink = false;
2205
3590
  try {
2206
- return await resolveBasouRepositoryRoot(cwd, {
2207
- onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
2208
- });
2209
- } catch (error) {
2210
- if (error instanceof Error && error.message === "Not a git repository") {
2211
- throw new Error(
2212
- `Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
2213
- { cause: error }
2214
- );
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
+ }
2215
3629
  }
2216
- throw error;
2217
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;
2218
3643
  }
2219
-
2220
- // src/commands/orient.ts
2221
- function registerOrientCommand(program2) {
2222
- program2.command("orient").description("Show the workspace's current position (also writes .basou/orientation.md)").option("-q, --quiet", "Write the file without printing the body").option("-v, --verbose", "Show error causes").action(async (opts) => {
2223
- await runOrient(opts);
2224
- });
3644
+ function presetActionLabel(action) {
3645
+ return action === "create" ? "\u65B0\u898F\u4F5C\u6210" : "\u66F4\u65B0";
2225
3646
  }
2226
- async function runOrient(options, ctx = {}) {
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 = {}) {
2227
3753
  try {
2228
- await doRunOrient(options, ctx);
3754
+ await doRunProjectArchive(target, options, ctx);
2229
3755
  } catch (error) {
2230
3756
  renderCliError(error, { verbose: isVerbose(options) });
2231
3757
  process.exitCode = 1;
2232
3758
  }
2233
3759
  }
2234
- async function doRunOrient(options, ctx) {
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) {
2235
3831
  const cwd = ctx.cwd ?? process.cwd();
2236
- const repositoryRoot = await resolveBasouRootForCommand(cwd, "orient");
2237
- const paths = basouPaths7(repositoryRoot);
2238
- await assertWorkspaceInitialized6(paths.root);
2239
- const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2240
- const probeCtx = { cwd: repositoryRoot };
2241
- if (ctx.claudeProjectsDir !== void 0) probeCtx.claudeProjectsDir = ctx.claudeProjectsDir;
2242
- if (ctx.codexSessionsDir !== void 0) probeCtx.codexSessionsDir = ctx.codexSessionsDir;
2243
- const staleness = await probeStaleness({ ctx: probeCtx, paths, nowIso });
2244
- const result = await renderOrientation2({
2245
- paths,
2246
- nowIso,
2247
- staleness,
2248
- verbose: options.verbose === true,
2249
- onWarning: (w, sid) => printReplayWarning(w, sid),
2250
- onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
2251
- onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
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
2252
3847
  });
2253
- await writeMarkdownFile4(paths.files.orientation, `${result.body}
2254
- `);
2255
- if (options.quiet === true) {
2256
- console.log(
2257
- `Generated .basou/orientation.md (sessions: ${result.sessionCount}, in-flight tasks: ${result.inFlightTaskCount}, pending approvals: ${result.pendingApprovalsCount}, suspect: ${result.suspectCount})`
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`
2258
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`);
2259
3897
  } else {
2260
- console.log(result.body);
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("");
2261
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");
2262
3942
  }
2263
- async function assertWorkspaceInitialized6(basouRoot) {
3943
+ async function runProjectRename(oldPath, newPath, options, ctx = {}) {
2264
3944
  try {
2265
- await assertBasouRootSafe7(basouRoot);
3945
+ await doRunProjectRename(oldPath, newPath, options, ctx);
2266
3946
  } catch (error) {
2267
- if (findErrorCode6(error, "ENOENT")) {
2268
- throw new Error("Workspace not initialized. Run 'basou init' first.");
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 {
2269
3966
  }
2270
- throw error;
2271
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");
2272
4097
  }
2273
4098
 
2274
4099
  // src/commands/refresh.ts
2275
- import { assertBasouRootSafe as assertBasouRootSafe8, basouPaths as basouPaths8, findErrorCode as findErrorCode8 } from "@basou/core";
2276
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
4100
+ import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths10, findErrorCode as findErrorCode9 } from "@basou/core";
4101
+ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
2277
4102
 
2278
4103
  // src/lib/portfolio-config.ts
2279
4104
  import { homedir as homedir3 } from "os";
2280
- import { isAbsolute, join as join4, resolve as resolve3 } from "path";
4105
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
2281
4106
  import { readYamlFile as readYamlFile3 } from "@basou/core";
2282
- var DEFAULT_PORTFOLIO_CONFIG_PATH = join4(homedir3(), ".basou", "portfolio.yaml");
4107
+ var DEFAULT_PORTFOLIO_CONFIG_PATH = join5(homedir3(), ".basou", "portfolio.yaml");
2283
4108
  function expandTilde(p) {
2284
4109
  if (p === "~") return homedir3();
2285
- if (p.startsWith("~/")) return join4(homedir3(), p.slice(2));
4110
+ if (p.startsWith("~/")) return join5(homedir3(), p.slice(2));
2286
4111
  return p;
2287
4112
  }
2288
4113
  function isRecord(value) {
@@ -2316,12 +4141,12 @@ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
2316
4141
  throw new Error("A portfolio workspace 'label' must be a string when present.");
2317
4142
  }
2318
4143
  const expanded = expandTilde(entry.path.trim());
2319
- if (!isAbsolute(expanded)) {
4144
+ if (!isAbsolute2(expanded)) {
2320
4145
  throw new Error(
2321
4146
  "Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
2322
4147
  );
2323
4148
  }
2324
- const abs = resolve3(expanded);
4149
+ const abs = resolve4(expanded);
2325
4150
  if (seen.has(abs)) continue;
2326
4151
  seen.add(abs);
2327
4152
  result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
@@ -2335,15 +4160,15 @@ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
2335
4160
  // src/commands/refresh-watch.ts
2336
4161
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
2337
4162
  import { homedir as homedir4 } from "os";
2338
- import { join as join5 } from "path";
2339
- import { findErrorCode as findErrorCode7 } from "@basou/core";
4163
+ import { join as join6 } from "path";
4164
+ import { findErrorCode as findErrorCode8 } from "@basou/core";
2340
4165
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
2341
4166
  var MIN_WATCH_INTERVAL_SEC = 5;
2342
4167
  var MAX_WATCH_INTERVAL_SEC = 86400;
2343
4168
  function watchedRoots(ctx) {
2344
4169
  return [
2345
- ctx.codexSessionsDir ?? join5(homedir4(), ".codex", "sessions"),
2346
- ctx.claudeProjectsDir ?? join5(homedir4(), ".claude", "projects")
4170
+ ctx.codexSessionsDir ?? join6(homedir4(), ".codex", "sessions"),
4171
+ ctx.claudeProjectsDir ?? join6(homedir4(), ".claude", "projects")
2347
4172
  ];
2348
4173
  }
2349
4174
  async function scanSourceLogs(roots) {
@@ -2353,11 +4178,11 @@ async function scanSourceLogs(roots) {
2353
4178
  try {
2354
4179
  entries = await readdir2(dir, { withFileTypes: true });
2355
4180
  } catch (error) {
2356
- if (findErrorCode7(error, "ENOENT") || findErrorCode7(error, "ENOTDIR")) return;
4181
+ if (findErrorCode8(error, "ENOENT") || findErrorCode8(error, "ENOTDIR")) return;
2357
4182
  throw new Error("Failed to read a source log directory", { cause: error });
2358
4183
  }
2359
4184
  for (const entry of entries) {
2360
- const full = join5(dir, entry.name);
4185
+ const full = join6(dir, entry.name);
2361
4186
  if (entry.isDirectory()) {
2362
4187
  await walk(full);
2363
4188
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -2365,7 +4190,7 @@ async function scanSourceLogs(roots) {
2365
4190
  const info = await stat2(full);
2366
4191
  out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
2367
4192
  } catch (error) {
2368
- if (findErrorCode7(error, "ENOENT")) continue;
4193
+ if (findErrorCode8(error, "ENOENT")) continue;
2369
4194
  throw new Error("Failed to stat a source log file", { cause: error });
2370
4195
  }
2371
4196
  }
@@ -2457,26 +4282,26 @@ function collectPath2(value, previous) {
2457
4282
  function parseInterval(value) {
2458
4283
  const seconds = Number(value);
2459
4284
  if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
2460
- throw new InvalidArgumentError2(
4285
+ throw new InvalidArgumentError3(
2461
4286
  `--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
2462
4287
  );
2463
4288
  }
2464
4289
  return seconds;
2465
4290
  }
2466
4291
  function abortableSleep(ms, signal) {
2467
- return new Promise((resolve7) => {
4292
+ return new Promise((resolve8) => {
2468
4293
  if (signal.aborted) {
2469
- resolve7();
4294
+ resolve8();
2470
4295
  return;
2471
4296
  }
2472
4297
  let timer;
2473
4298
  const onAbort = () => {
2474
4299
  clearTimeout(timer);
2475
- resolve7();
4300
+ resolve8();
2476
4301
  };
2477
4302
  timer = setTimeout(() => {
2478
4303
  signal.removeEventListener("abort", onAbort);
2479
- resolve7();
4304
+ resolve8();
2480
4305
  }, ms);
2481
4306
  signal.addEventListener("abort", onAbort, { once: true });
2482
4307
  });
@@ -2567,8 +4392,8 @@ async function doRunRefreshWatch(options, ctx) {
2567
4392
  if (options.force === true) throw new Error("--watch cannot be combined with --force.");
2568
4393
  const cwd = ctx.cwd ?? process.cwd();
2569
4394
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
2570
- const paths = basouPaths8(repositoryRoot);
2571
- await assertWorkspaceInitialized7(paths.root);
4395
+ const paths = basouPaths10(repositoryRoot);
4396
+ await assertWorkspaceInitialized8(paths.root);
2572
4397
  const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
2573
4398
  const controller = new AbortController();
2574
4399
  const onSignal = () => controller.abort();
@@ -2595,8 +4420,8 @@ async function doRunRefreshWatch(options, ctx) {
2595
4420
  async function computeRefresh(options, ctx) {
2596
4421
  const cwd = ctx.cwd ?? process.cwd();
2597
4422
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
2598
- const paths = basouPaths8(repositoryRoot);
2599
- await assertWorkspaceInitialized7(paths.root);
4423
+ const paths = basouPaths10(repositoryRoot);
4424
+ await assertWorkspaceInitialized8(paths.root);
2600
4425
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2601
4426
  return refreshAll({
2602
4427
  options: {
@@ -2644,7 +4469,14 @@ function printRefreshSummary(result) {
2644
4469
  console.log(`handoff: skipped (${result.handoff.reason})`);
2645
4470
  }
2646
4471
  if (result.decisions.status === "generated") {
2647
- 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
+ }
2648
4480
  } else {
2649
4481
  console.log(`decisions: skipped (${result.decisions.reason})`);
2650
4482
  }
@@ -2656,11 +4488,11 @@ function printRefreshSummary(result) {
2656
4488
  console.log(`orientation: skipped (${result.orientation.reason})`);
2657
4489
  }
2658
4490
  }
2659
- async function assertWorkspaceInitialized7(basouRoot) {
4491
+ async function assertWorkspaceInitialized8(basouRoot) {
2660
4492
  try {
2661
- await assertBasouRootSafe8(basouRoot);
4493
+ await assertBasouRootSafe9(basouRoot);
2662
4494
  } catch (error) {
2663
- if (findErrorCode8(error, "ENOENT")) {
4495
+ if (findErrorCode9(error, "ENOENT")) {
2664
4496
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2665
4497
  }
2666
4498
  throw error;
@@ -2668,14 +4500,14 @@ async function assertWorkspaceInitialized7(basouRoot) {
2668
4500
  }
2669
4501
 
2670
4502
  // src/commands/report.ts
2671
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
4503
+ import { isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
2672
4504
  import {
2673
- assertBasouRootSafe as assertBasouRootSafe9,
2674
- basouPaths as basouPaths9,
2675
- findErrorCode as findErrorCode9,
4505
+ assertBasouRootSafe as assertBasouRootSafe10,
4506
+ basouPaths as basouPaths11,
4507
+ findErrorCode as findErrorCode10,
2676
4508
  renderReport,
2677
4509
  resolveRepositoryRoot as resolveRepositoryRoot8,
2678
- writeMarkdownFile as writeMarkdownFile5
4510
+ writeMarkdownFile as writeMarkdownFile6
2679
4511
  } from "@basou/core";
2680
4512
  function registerReportCommand(program2) {
2681
4513
  const report = program2.command("report").description(
@@ -2696,8 +4528,8 @@ async function runReportGenerate(options, ctx = {}) {
2696
4528
  async function doRunReportGenerate(options, ctx) {
2697
4529
  const cwd = ctx.cwd ?? process.cwd();
2698
4530
  const repositoryRoot = await resolveRepositoryRootForReport(cwd);
2699
- const paths = basouPaths9(repositoryRoot);
2700
- await assertWorkspaceInitialized8(paths.root);
4531
+ const paths = basouPaths11(repositoryRoot);
4532
+ await assertWorkspaceInitialized9(paths.root);
2701
4533
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2702
4534
  const result = await renderReport({
2703
4535
  paths,
@@ -2708,8 +4540,8 @@ async function doRunReportGenerate(options, ctx) {
2708
4540
  onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2709
4541
  });
2710
4542
  if (options.out !== void 0) {
2711
- const outPath = isAbsolute2(options.out) ? options.out : resolve4(cwd, options.out);
2712
- await writeMarkdownFile5(outPath, result.body);
4543
+ const outPath = isAbsolute3(options.out) ? options.out : resolve5(cwd, options.out);
4544
+ await writeMarkdownFile6(outPath, result.body);
2713
4545
  const { sessions, decisions, tasks } = result.data;
2714
4546
  console.error(
2715
4547
  `Wrote report to ${options.out} (sessions: ${sessions.total}, decisions: ${decisions.count}, tasks: ${tasks.total})`
@@ -2734,25 +4566,149 @@ async function resolveRepositoryRootForReport(cwd) {
2734
4566
  throw error;
2735
4567
  }
2736
4568
  }
2737
- async function assertWorkspaceInitialized8(basouRoot) {
4569
+ async function assertWorkspaceInitialized9(basouRoot) {
2738
4570
  try {
2739
- await assertBasouRootSafe9(basouRoot);
4571
+ await assertBasouRootSafe10(basouRoot);
2740
4572
  } catch (error) {
2741
- if (findErrorCode9(error, "ENOENT")) {
4573
+ if (findErrorCode10(error, "ENOENT")) {
2742
4574
  throw new Error("Workspace not initialized. Run 'basou init' first.");
2743
4575
  }
2744
4576
  throw error;
2745
4577
  }
2746
4578
  }
2747
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(program2) {
4597
+ program2.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
+
2748
4704
  // src/commands/run.ts
2749
4705
  import { mkdir as mkdir2 } from "fs/promises";
2750
4706
  import { homedir as homedir5 } from "os";
2751
- import { join as join6 } from "path";
4707
+ import { join as join7 } from "path";
2752
4708
  import {
2753
- acquireLock as acquireLock4,
2754
- assertBasouRootSafe as assertBasouRootSafe10,
2755
- basouPaths as basouPaths10,
4709
+ acquireLock as acquireLock5,
4710
+ assertBasouRootSafe as assertBasouRootSafe11,
4711
+ basouPaths as basouPaths13,
2756
4712
  ChildProcessRunner as ChildProcessRunner2,
2757
4713
  claudeCodeAdapterMetadata,
2758
4714
  appendChainedEvent as coreAppendChainedEvent2,
@@ -2761,7 +4717,7 @@ import {
2761
4717
  getSnapshot as getSnapshot2,
2762
4718
  overwriteYamlFile as overwriteYamlFile2,
2763
4719
  prefixedUlid as prefixedUlid4,
2764
- readManifest as readManifest4,
4720
+ readManifest as readManifest6,
2765
4721
  readYamlFile as readYamlFile4,
2766
4722
  resolveClaudeCodeCommand,
2767
4723
  resolveRepositoryRoot as resolveRepositoryRoot9,
@@ -2797,17 +4753,17 @@ async function runClaudeCode(args, options, ctx = {}) {
2797
4753
  const { command } = await resolveCommand();
2798
4754
  const cwd = options.cwd ?? process.cwd();
2799
4755
  const repoRoot = await resolveRepositoryRootForRun(cwd);
2800
- const paths = basouPaths10(repoRoot);
2801
- await assertBasouRootSafe10(paths.root);
2802
- const manifest = await readManifest4(paths);
4756
+ const paths = basouPaths13(repoRoot);
4757
+ await assertBasouRootSafe11(paths.root);
4758
+ const manifest = await readManifest6(paths);
2803
4759
  const sessionId = prefixedUlid4("ses");
2804
- const sessionDir = join6(paths.sessions, sessionId);
4760
+ const sessionDir = join7(paths.sessions, sessionId);
2805
4761
  await mkdir2(sessionDir, { recursive: true });
2806
4762
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
2807
4763
  await coreAppendChainedEvent2(paths, sessionId, event);
2808
4764
  });
2809
4765
  const startedAt = now().toISOString();
2810
- const sessionYamlPath = join6(sessionDir, "session.yaml");
4766
+ const sessionYamlPath = join7(sessionDir, "session.yaml");
2811
4767
  const session = buildInitialSession2({
2812
4768
  id: sessionId,
2813
4769
  command,
@@ -2840,7 +4796,7 @@ async function runClaudeCode(args, options, ctx = {}) {
2840
4796
  from: "initialized",
2841
4797
  to: "running"
2842
4798
  });
2843
- const runningLock = await acquireLock4(paths, "session", sessionId);
4799
+ const runningLock = await acquireLock5(paths, "session", sessionId);
2844
4800
  try {
2845
4801
  await mutateSessionYaml2(sessionYamlPath, (s) => {
2846
4802
  s.session.status = "running";
@@ -3150,29 +5106,28 @@ async function resolveRepositoryRootForRun(cwd) {
3150
5106
 
3151
5107
  // src/commands/session.ts
3152
5108
  import { readFile as readFile2 } from "fs/promises";
3153
- import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2 } from "path";
5109
+ import { basename as basename4, isAbsolute as isAbsolute4, join as join8, relative as relative3 } from "path";
3154
5110
  import {
3155
- acquireLock as acquireLock5,
3156
- appendEventToExistingSession as appendEventToExistingSession2,
3157
- assertBasouRootSafe as assertBasouRootSafe11,
3158
- basouPaths as basouPaths11,
5111
+ acquireLock as acquireLock6,
5112
+ appendEventToExistingSession as appendEventToExistingSession3,
5113
+ assertBasouRootSafe as assertBasouRootSafe12,
5114
+ basouPaths as basouPaths14,
3159
5115
  enumerateSessionDirs as enumerateSessionDirs2,
3160
- findErrorCode as findErrorCode10,
5116
+ findErrorCode as findErrorCode11,
3161
5117
  importSessionFromJson as importSessionFromJson2,
3162
5118
  loadSessionEntries,
3163
5119
  readAllEvents,
3164
- readManifest as readManifest5,
5120
+ readManifest as readManifest7,
3165
5121
  readYamlFile as readYamlFile5,
3166
5122
  rechainSessionInPlace,
3167
- resolveRepositoryRoot as resolveRepositoryRoot10,
3168
- resolveSessionId as resolveSessionId2,
5123
+ resolveSessionId as resolveSessionId3,
3169
5124
  resolveTaskId,
3170
5125
  SessionImportPayloadSchema as SessionImportPayloadSchema2,
3171
5126
  SessionSchema as SessionSchema3,
3172
5127
  SessionStatusSchema,
3173
5128
  sessionWorkStatsFromEvents
3174
5129
  } from "@basou/core";
3175
- import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
5130
+ import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
3176
5131
 
3177
5132
  // src/lib/format-duration.ts
3178
5133
  import { formatDurationMs } from "@basou/core";
@@ -3221,8 +5176,8 @@ async function runSessionList(options, ctx = {}) {
3221
5176
  async function doRunSessionList(options, ctx) {
3222
5177
  const cwd = ctx.cwd ?? process.cwd();
3223
5178
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
3224
- const paths = basouPaths11(repositoryRoot);
3225
- await assertWorkspaceInitialized9(paths.root);
5179
+ const paths = basouPaths14(repositoryRoot);
5180
+ await assertWorkspaceInitialized10(paths.root);
3226
5181
  const now = /* @__PURE__ */ new Date();
3227
5182
  const records = (await loadSessionEntries(paths, {
3228
5183
  now,
@@ -3273,17 +5228,17 @@ async function runSessionShow(idInput, options, ctx = {}) {
3273
5228
  async function doRunSessionShow(idInput, options, ctx) {
3274
5229
  const cwd = ctx.cwd ?? process.cwd();
3275
5230
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
3276
- const paths = basouPaths11(repositoryRoot);
3277
- await assertWorkspaceInitialized9(paths.root);
3278
- const sessionId = await resolveSessionId2(paths, idInput);
3279
- const sessionDir = join7(paths.sessions, sessionId);
3280
- const sessionYamlPath = join7(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");
3281
5236
  let session;
3282
5237
  try {
3283
5238
  const raw = await readYamlFile5(sessionYamlPath);
3284
5239
  session = SessionSchema3.parse(raw);
3285
5240
  } catch (error) {
3286
- if (findErrorCode10(error, "ENOENT")) {
5241
+ if (findErrorCode11(error, "ENOENT")) {
3287
5242
  throw new Error(`Session not found: ${idInput}`);
3288
5243
  }
3289
5244
  throw new Error("Failed to read session", { cause: error });
@@ -3398,12 +5353,12 @@ function formatSessionWork(session, events, now) {
3398
5353
  }
3399
5354
  function formatWorkingDir(workingDir, repositoryRoot, options) {
3400
5355
  if (options.fullPath === true) return workingDir;
3401
- if (!isAbsolute3(workingDir)) {
5356
+ if (!isAbsolute4(workingDir)) {
3402
5357
  if (workingDir === ".") return "<repository_root>";
3403
5358
  return workingDir;
3404
5359
  }
3405
5360
  if (workingDir === repositoryRoot) return "<repository_root>";
3406
- const rel = relative2(repositoryRoot, workingDir);
5361
+ const rel = relative3(repositoryRoot, workingDir);
3407
5362
  if (rel.length === 0 || rel === ".") return "<repository_root>";
3408
5363
  if (rel.startsWith("..")) return rel;
3409
5364
  return `./${rel}`;
@@ -3517,23 +5472,13 @@ function maxLen2(values, floor) {
3517
5472
  return max;
3518
5473
  }
3519
5474
  async function resolveRepositoryRootForSession(cwd, subcmd) {
3520
- try {
3521
- return await resolveRepositoryRoot10(cwd);
3522
- } catch (error) {
3523
- if (error instanceof Error && error.message === "Not a git repository") {
3524
- throw new Error(
3525
- `Not a git repository. Run 'git init' first, then re-run 'basou session ${subcmd}'.`,
3526
- { cause: error }
3527
- );
3528
- }
3529
- throw error;
3530
- }
5475
+ return resolveBasouRootForCommand(cwd, `session ${subcmd}`);
3531
5476
  }
3532
- async function assertWorkspaceInitialized9(basouRoot) {
5477
+ async function assertWorkspaceInitialized10(basouRoot) {
3533
5478
  try {
3534
- await assertBasouRootSafe11(basouRoot);
5479
+ await assertBasouRootSafe12(basouRoot);
3535
5480
  } catch (error) {
3536
- if (findErrorCode10(error, "ENOENT")) {
5481
+ if (findErrorCode11(error, "ENOENT")) {
3537
5482
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3538
5483
  }
3539
5484
  throw error;
@@ -3571,9 +5516,9 @@ async function runSessionImport(options, ctx = {}) {
3571
5516
  async function doRunSessionImport(options, ctx) {
3572
5517
  const cwd = ctx.cwd ?? process.cwd();
3573
5518
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
3574
- const paths = basouPaths11(repositoryRoot);
3575
- await assertWorkspaceInitialized9(paths.root);
3576
- const manifest = await readManifest5(paths);
5519
+ const paths = basouPaths14(repositoryRoot);
5520
+ await assertWorkspaceInitialized10(paths.root);
5521
+ const manifest = await readManifest7(paths);
3577
5522
  const rawBody = await readInputFile(options.from);
3578
5523
  const json = parseJsonStrict(rawBody);
3579
5524
  const parsed = SessionImportPayloadSchema2.safeParse(json);
@@ -3602,10 +5547,10 @@ async function readInputFile(path) {
3602
5547
  try {
3603
5548
  return await readFile2(path, "utf8");
3604
5549
  } catch (error) {
3605
- if (findErrorCode10(error, "ENOENT")) {
5550
+ if (findErrorCode11(error, "ENOENT")) {
3606
5551
  throw new Error("Import source not found", { cause: error });
3607
5552
  }
3608
- if (findErrorCode10(error, "EISDIR")) {
5553
+ if (findErrorCode11(error, "EISDIR")) {
3609
5554
  throw new Error("Import source is not a file", { cause: error });
3610
5555
  }
3611
5556
  throw new Error("Failed to read import source", { cause: error });
@@ -3620,19 +5565,19 @@ function parseJsonStrict(body) {
3620
5565
  }
3621
5566
  function parseImportFormat(raw) {
3622
5567
  if (raw !== "json") {
3623
- throw new InvalidArgumentError3(`Unsupported format: ${raw}. Valid values: json`);
5568
+ throw new InvalidArgumentError5(`Unsupported format: ${raw}. Valid values: json`);
3624
5569
  }
3625
5570
  return "json";
3626
5571
  }
3627
5572
  function parseLabelOverride(raw) {
3628
5573
  if (raw.length === 0) {
3629
- throw new InvalidArgumentError3("Label must not be empty");
5574
+ throw new InvalidArgumentError5("Label must not be empty");
3630
5575
  }
3631
5576
  return raw;
3632
5577
  }
3633
5578
  function parseTaskIdOverride(raw) {
3634
5579
  if (raw.length === 0) {
3635
- throw new InvalidArgumentError3("Task id is empty");
5580
+ throw new InvalidArgumentError5("Task id is empty");
3636
5581
  }
3637
5582
  return raw;
3638
5583
  }
@@ -3658,7 +5603,7 @@ function printSessionImportResult(options, result) {
3658
5603
  return;
3659
5604
  }
3660
5605
  console.log(
3661
- `Imported session ${sid} (${result.eventCount} events) from ${basename3(options.from)}`
5606
+ `Imported session ${sid} (${result.eventCount} events) from ${basename4(options.from)}`
3662
5607
  );
3663
5608
  }
3664
5609
  var NOTE_BODY_PREVIEW_LIMIT = 80;
@@ -3685,19 +5630,19 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
3685
5630
  }
3686
5631
  const cwd = ctx.cwd ?? process.cwd();
3687
5632
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
3688
- const paths = basouPaths11(repositoryRoot);
3689
- await assertWorkspaceInitialized9(paths.root);
3690
- const sessionId = await resolveSessionId2(paths, sessionIdInput);
5633
+ const paths = basouPaths14(repositoryRoot);
5634
+ await assertWorkspaceInitialized10(paths.root);
5635
+ const sessionId = await resolveSessionId3(paths, sessionIdInput);
3691
5636
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
3692
5637
  if (body.length === 0) {
3693
5638
  throw new Error("Note body is empty");
3694
5639
  }
3695
5640
  const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
3696
5641
  const sesId = sessionId;
3697
- const sessionLock = await acquireLock5(paths, "session", sesId);
5642
+ const sessionLock = await acquireLock6(paths, "session", sesId);
3698
5643
  let result;
3699
5644
  try {
3700
- result = await appendEventToExistingSession2({
5645
+ result = await appendEventToExistingSession3({
3701
5646
  paths,
3702
5647
  sessionId: sesId,
3703
5648
  eventBuilder: (eventId) => ({
@@ -3719,10 +5664,10 @@ async function readNoteFile(path) {
3719
5664
  try {
3720
5665
  return await readFile2(path, "utf8");
3721
5666
  } catch (error) {
3722
- if (findErrorCode10(error, "ENOENT")) {
5667
+ if (findErrorCode11(error, "ENOENT")) {
3723
5668
  throw new Error("Note source not found", { cause: error });
3724
5669
  }
3725
- if (findErrorCode10(error, "EISDIR")) {
5670
+ if (findErrorCode11(error, "EISDIR")) {
3726
5671
  throw new Error("Note source is not a file", { cause: error });
3727
5672
  }
3728
5673
  throw new Error("Failed to read note source", { cause: error });
@@ -3730,7 +5675,7 @@ async function readNoteFile(path) {
3730
5675
  }
3731
5676
  function parseNoteBodyOption(raw) {
3732
5677
  if (raw.length === 0) {
3733
- throw new InvalidArgumentError3("--body must not be empty");
5678
+ throw new InvalidArgumentError5("--body must not be empty");
3734
5679
  }
3735
5680
  return raw;
3736
5681
  }
@@ -3767,9 +5712,9 @@ async function doRunSessionRechain(options, ctx) {
3767
5712
  }
3768
5713
  const cwd = ctx.cwd ?? process.cwd();
3769
5714
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
3770
- const paths = basouPaths11(repositoryRoot);
3771
- await assertWorkspaceInitialized9(paths.root);
3772
- 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);
3773
5718
  const dryRun = options.dryRun === true;
3774
5719
  const rows = [];
3775
5720
  for (const sessionId of sessionIds) {
@@ -3821,11 +5766,11 @@ function renderRechainRow(row, dryRun) {
3821
5766
 
3822
5767
  // src/commands/stats.ts
3823
5768
  import {
3824
- assertBasouRootSafe as assertBasouRootSafe12,
3825
- basouPaths as basouPaths12,
5769
+ assertBasouRootSafe as assertBasouRootSafe13,
5770
+ basouPaths as basouPaths15,
3826
5771
  computeWorkStats,
3827
- findErrorCode as findErrorCode11,
3828
- resolveRepositoryRoot as resolveRepositoryRoot11
5772
+ findErrorCode as findErrorCode12,
5773
+ resolveRepositoryRoot as resolveRepositoryRoot10
3829
5774
  } from "@basou/core";
3830
5775
  function registerStatsCommand(program2) {
3831
5776
  program2.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
@@ -3843,8 +5788,8 @@ async function runStats(options, ctx = {}) {
3843
5788
  async function doRunStats(options, ctx) {
3844
5789
  const cwd = ctx.cwd ?? process.cwd();
3845
5790
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
3846
- const paths = basouPaths12(repositoryRoot);
3847
- await assertWorkspaceInitialized10(paths.root);
5791
+ const paths = basouPaths15(repositoryRoot);
5792
+ await assertWorkspaceInitialized11(paths.root);
3848
5793
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
3849
5794
  const result = await computeWorkStats({
3850
5795
  paths,
@@ -3928,7 +5873,7 @@ function formatInt(n) {
3928
5873
  }
3929
5874
  async function resolveRepositoryRootForStats(cwd) {
3930
5875
  try {
3931
- return await resolveRepositoryRoot11(cwd);
5876
+ return await resolveRepositoryRoot10(cwd);
3932
5877
  } catch (error) {
3933
5878
  if (error instanceof Error && error.message === "Not a git repository") {
3934
5879
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -3938,11 +5883,11 @@ async function resolveRepositoryRootForStats(cwd) {
3938
5883
  throw error;
3939
5884
  }
3940
5885
  }
3941
- async function assertWorkspaceInitialized10(basouRoot) {
5886
+ async function assertWorkspaceInitialized11(basouRoot) {
3942
5887
  try {
3943
- await assertBasouRootSafe12(basouRoot);
5888
+ await assertBasouRootSafe13(basouRoot);
3944
5889
  } catch (error) {
3945
- if (findErrorCode11(error, "ENOENT")) {
5890
+ if (findErrorCode12(error, "ENOENT")) {
3946
5891
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3947
5892
  }
3948
5893
  throw error;
@@ -3951,12 +5896,12 @@ async function assertWorkspaceInitialized10(basouRoot) {
3951
5896
 
3952
5897
  // src/commands/status.ts
3953
5898
  import {
3954
- assertBasouRootSafe as assertBasouRootSafe13,
3955
- basouPaths as basouPaths13,
5899
+ assertBasouRootSafe as assertBasouRootSafe14,
5900
+ basouPaths as basouPaths16,
3956
5901
  buildStatusSnapshot,
3957
- findErrorCode as findErrorCode12,
3958
- readManifest as readManifest6,
3959
- resolveRepositoryRoot as resolveRepositoryRoot12,
5902
+ findErrorCode as findErrorCode13,
5903
+ readManifest as readManifest8,
5904
+ resolveRepositoryRoot as resolveRepositoryRoot11,
3960
5905
  writeStatus
3961
5906
  } from "@basou/core";
3962
5907
  function registerStatusCommand(program2) {
@@ -3975,20 +5920,20 @@ async function runStatus(options, ctx = {}) {
3975
5920
  async function doRunStatus(options, ctx) {
3976
5921
  const cwd = ctx.cwd ?? process.cwd();
3977
5922
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
3978
- const paths = basouPaths13(repositoryRoot);
5923
+ const paths = basouPaths16(repositoryRoot);
3979
5924
  try {
3980
- await assertBasouRootSafe13(paths.root);
5925
+ await assertBasouRootSafe14(paths.root);
3981
5926
  } catch (error) {
3982
- if (findErrorCode12(error, "ENOENT")) {
5927
+ if (findErrorCode13(error, "ENOENT")) {
3983
5928
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3984
5929
  }
3985
5930
  throw error;
3986
5931
  }
3987
5932
  let manifest;
3988
5933
  try {
3989
- manifest = await readManifest6(paths);
5934
+ manifest = await readManifest8(paths);
3990
5935
  } catch (error) {
3991
- if (findErrorCode12(error, "ENOENT")) {
5936
+ if (findErrorCode13(error, "ENOENT")) {
3992
5937
  throw new Error("Workspace not initialized. Run 'basou init' first.");
3993
5938
  }
3994
5939
  throw new Error("Failed to read workspace manifest", { cause: error });
@@ -4012,7 +5957,7 @@ function renderTextStatus(s) {
4012
5957
  }
4013
5958
  async function resolveRepositoryRootForStatus(cwd) {
4014
5959
  try {
4015
- return await resolveRepositoryRoot12(cwd);
5960
+ return await resolveRepositoryRoot11(cwd);
4016
5961
  } catch (error) {
4017
5962
  if (error instanceof Error && error.message === "Not a git repository") {
4018
5963
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -4025,34 +5970,34 @@ async function resolveRepositoryRootForStatus(cwd) {
4025
5970
 
4026
5971
  // src/commands/task.ts
4027
5972
  import { readFile as readFile3 } from "fs/promises";
4028
- import { join as join8 } from "path";
5973
+ import { join as join9 } from "path";
4029
5974
  import {
4030
5975
  archiveTask,
4031
- assertBasouRootSafe as assertBasouRootSafe14,
4032
- basouPaths as basouPaths14,
5976
+ assertBasouRootSafe as assertBasouRootSafe15,
5977
+ basouPaths as basouPaths17,
4033
5978
  createTaskWithEvent,
4034
5979
  deleteTask,
4035
5980
  editTask,
4036
5981
  enumerateArchivedTaskIds,
4037
- findErrorCode as findErrorCode13,
5982
+ findErrorCode as findErrorCode14,
4038
5983
  loadSessionEntries as loadSessionEntries2,
4039
5984
  loadTaskEntries,
4040
5985
  prefixedUlid as prefixedUlid5,
4041
- readManifest as readManifest7,
5986
+ readManifest as readManifest9,
4042
5987
  readTaskFile,
4043
5988
  readTaskFileWithArchiveFallback,
4044
5989
  reconcileAllTasks,
4045
5990
  reconcileTask,
4046
5991
  refreshTaskLinkedSessions,
4047
5992
  replayEvents as replayEvents2,
4048
- resolveRepositoryRoot as resolveRepositoryRoot13,
4049
- resolveSessionId as resolveSessionId3,
5993
+ resolveRepositoryRoot as resolveRepositoryRoot12,
5994
+ resolveSessionId as resolveSessionId4,
4050
5995
  resolveTaskId as resolveTaskId2,
4051
5996
  TaskStatusSchema,
4052
5997
  TaskWriteAfterEventError,
4053
5998
  updateTaskStatusWithEvent
4054
5999
  } from "@basou/core";
4055
- import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
6000
+ import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
4056
6001
  var STATUS_VALUES3 = TaskStatusSchema.options;
4057
6002
  function registerTaskCommand(program2) {
4058
6003
  const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
@@ -4131,14 +6076,14 @@ async function doRunTaskNew(options, ctx) {
4131
6076
  }
4132
6077
  const cwd = ctx.cwd ?? process.cwd();
4133
6078
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
4134
- const paths = basouPaths14(repositoryRoot);
4135
- await assertWorkspaceInitialized11(paths.root);
6079
+ const paths = basouPaths17(repositoryRoot);
6080
+ await assertWorkspaceInitialized12(paths.root);
4136
6081
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
4137
6082
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4138
6083
  const occurredAt = now.toISOString();
4139
6084
  const taskId = prefixedUlid5("task");
4140
6085
  if (options.session !== void 0) {
4141
- const sessionId = await resolveSessionId3(paths, options.session);
6086
+ const sessionId = await resolveSessionId4(paths, options.session);
4142
6087
  const result2 = await createTaskWithEvent({
4143
6088
  mode: "attach",
4144
6089
  paths,
@@ -4166,7 +6111,7 @@ async function doRunTaskNew(options, ctx) {
4166
6111
  });
4167
6112
  return;
4168
6113
  }
4169
- const manifest = await readManifest7(paths);
6114
+ const manifest = await readManifest9(paths);
4170
6115
  const result = await createTaskWithEvent({
4171
6116
  mode: "ad-hoc",
4172
6117
  paths,
@@ -4240,8 +6185,8 @@ async function runTaskList(options, ctx = {}) {
4240
6185
  async function doRunTaskList(options, ctx) {
4241
6186
  const cwd = ctx.cwd ?? process.cwd();
4242
6187
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
4243
- const paths = basouPaths14(repositoryRoot);
4244
- await assertWorkspaceInitialized11(paths.root);
6188
+ const paths = basouPaths17(repositoryRoot);
6189
+ await assertWorkspaceInitialized12(paths.root);
4245
6190
  const entries = await loadTaskEntries(paths, {
4246
6191
  onSkip: (id, reason) => printTaskSkip(id, reason)
4247
6192
  });
@@ -4344,15 +6289,15 @@ async function runTaskShow(idInput, options, ctx = {}) {
4344
6289
  async function doRunTaskShow(idInput, options, ctx) {
4345
6290
  const cwd = ctx.cwd ?? process.cwd();
4346
6291
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
4347
- const paths = basouPaths14(repositoryRoot);
4348
- await assertWorkspaceInitialized11(paths.root);
6292
+ const paths = basouPaths17(repositoryRoot);
6293
+ await assertWorkspaceInitialized12(paths.root);
4349
6294
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
4350
6295
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
4351
6296
  const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
4352
6297
  const events = [];
4353
6298
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
4354
6299
  for (const s of sessions) {
4355
- const sessionDir = join8(paths.sessions, s.sessionId);
6300
+ const sessionDir = join9(paths.sessions, s.sessionId);
4356
6301
  try {
4357
6302
  for await (const ev of replayEvents2(sessionDir, {
4358
6303
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -4488,13 +6433,13 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
4488
6433
  const newStatus = parseTaskStatusPositional(newStatusInput);
4489
6434
  const cwd = ctx.cwd ?? process.cwd();
4490
6435
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
4491
- const paths = basouPaths14(repositoryRoot);
4492
- await assertWorkspaceInitialized11(paths.root);
6436
+ const paths = basouPaths17(repositoryRoot);
6437
+ await assertWorkspaceInitialized12(paths.root);
4493
6438
  const taskId = await resolveTaskId2(paths, taskIdInput);
4494
6439
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4495
6440
  const occurredAt = now.toISOString();
4496
6441
  if (options.session !== void 0) {
4497
- const sessionId = await resolveSessionId3(paths, options.session);
6442
+ const sessionId = await resolveSessionId4(paths, options.session);
4498
6443
  const result2 = await updateTaskStatusWithEvent({
4499
6444
  mode: "attach",
4500
6445
  paths,
@@ -4514,7 +6459,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
4514
6459
  });
4515
6460
  return;
4516
6461
  }
4517
- const manifest = await readManifest7(paths);
6462
+ const manifest = await readManifest9(paths);
4518
6463
  const result = await updateTaskStatusWithEvent({
4519
6464
  mode: "ad-hoc",
4520
6465
  paths,
@@ -4565,9 +6510,9 @@ async function runTaskReconcile(options, ctx = {}) {
4565
6510
  async function doRunTaskReconcile(options, ctx) {
4566
6511
  const cwd = ctx.cwd ?? process.cwd();
4567
6512
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
4568
- const paths = basouPaths14(repositoryRoot);
4569
- await assertWorkspaceInitialized11(paths.root);
4570
- const manifest = await readManifest7(paths);
6513
+ const paths = basouPaths17(repositoryRoot);
6514
+ await assertWorkspaceInitialized12(paths.root);
6515
+ const manifest = await readManifest9(paths);
4571
6516
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
4572
6517
  const write = options.write === true;
4573
6518
  const verbose = isVerbose(options);
@@ -4745,9 +6690,9 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
4745
6690
  }
4746
6691
  const cwd = ctx.cwd ?? process.cwd();
4747
6692
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
4748
- const paths = basouPaths14(repositoryRoot);
4749
- await assertWorkspaceInitialized11(paths.root);
4750
- const manifest = await readManifest7(paths);
6693
+ const paths = basouPaths17(repositoryRoot);
6694
+ await assertWorkspaceInitialized12(paths.root);
6695
+ const manifest = await readManifest9(paths);
4751
6696
  const taskId = await resolveTaskId2(paths, taskIdInput);
4752
6697
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
4753
6698
  const write = options.write === true;
@@ -4825,9 +6770,9 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
4825
6770
  }
4826
6771
  const cwd = ctx.cwd ?? process.cwd();
4827
6772
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
4828
- const paths = basouPaths14(repositoryRoot);
4829
- await assertWorkspaceInitialized11(paths.root);
4830
- const manifest = await readManifest7(paths);
6773
+ const paths = basouPaths17(repositoryRoot);
6774
+ await assertWorkspaceInitialized12(paths.root);
6775
+ const manifest = await readManifest9(paths);
4831
6776
  const taskId = await resolveTaskId2(paths, taskIdInput);
4832
6777
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4833
6778
  const occurredAt = now.toISOString();
@@ -4881,9 +6826,9 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
4881
6826
  }
4882
6827
  const cwd = ctx.cwd ?? process.cwd();
4883
6828
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
4884
- const paths = basouPaths14(repositoryRoot);
4885
- await assertWorkspaceInitialized11(paths.root);
4886
- const manifest = await readManifest7(paths);
6829
+ const paths = basouPaths17(repositoryRoot);
6830
+ await assertWorkspaceInitialized12(paths.root);
6831
+ const manifest = await readManifest9(paths);
4887
6832
  const taskId = await resolveTaskId2(paths, taskIdInput);
4888
6833
  if (options.yes !== true) {
4889
6834
  await confirmDestructiveAction("delete", taskId);
@@ -4926,9 +6871,9 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
4926
6871
  }
4927
6872
  const cwd = ctx.cwd ?? process.cwd();
4928
6873
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
4929
- const paths = basouPaths14(repositoryRoot);
4930
- await assertWorkspaceInitialized11(paths.root);
4931
- const manifest = await readManifest7(paths);
6874
+ const paths = basouPaths17(repositoryRoot);
6875
+ await assertWorkspaceInitialized12(paths.root);
6876
+ const manifest = await readManifest9(paths);
4932
6877
  const taskId = await resolveTaskId2(paths, taskIdInput);
4933
6878
  if (options.yes !== true) {
4934
6879
  await confirmDestructiveAction("archive", taskId);
@@ -4981,20 +6926,20 @@ async function readSingleLineFromStdin() {
4981
6926
  }
4982
6927
  function parseTitle2(raw) {
4983
6928
  if (raw.length === 0) {
4984
- throw new InvalidArgumentError4("Title must not be empty");
6929
+ throw new InvalidArgumentError6("Title must not be empty");
4985
6930
  }
4986
6931
  return raw;
4987
6932
  }
4988
6933
  function parseLabel(raw) {
4989
6934
  if (raw.length === 0) {
4990
- throw new InvalidArgumentError4("Label must not be empty");
6935
+ throw new InvalidArgumentError6("Label must not be empty");
4991
6936
  }
4992
6937
  return raw;
4993
6938
  }
4994
6939
  function parseInitialTaskStatus(raw) {
4995
6940
  const result = TaskStatusSchema.safeParse(raw);
4996
6941
  if (!result.success) {
4997
- throw new InvalidArgumentError4(
6942
+ throw new InvalidArgumentError6(
4998
6943
  `Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
4999
6944
  );
5000
6945
  }
@@ -5003,7 +6948,7 @@ function parseInitialTaskStatus(raw) {
5003
6948
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
5004
6949
  function parseIsoTimestampOption(raw) {
5005
6950
  if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
5006
- throw new InvalidArgumentError4(
6951
+ throw new InvalidArgumentError6(
5007
6952
  "Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
5008
6953
  );
5009
6954
  }
@@ -5012,7 +6957,7 @@ function parseIsoTimestampOption(raw) {
5012
6957
  function parseTaskStatusFilter(raw) {
5013
6958
  const result = TaskStatusSchema.safeParse(raw);
5014
6959
  if (!result.success) {
5015
- throw new InvalidArgumentError4(
6960
+ throw new InvalidArgumentError6(
5016
6961
  `Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
5017
6962
  );
5018
6963
  }
@@ -5027,14 +6972,14 @@ function parseTaskStatusPositional(raw) {
5027
6972
  }
5028
6973
  function parseDescriptionOption(raw) {
5029
6974
  if (raw.length === 0) {
5030
- throw new InvalidArgumentError4("Description must not be empty");
6975
+ throw new InvalidArgumentError6("Description must not be empty");
5031
6976
  }
5032
6977
  return raw;
5033
6978
  }
5034
6979
  function parsePositiveInt2(raw) {
5035
6980
  const n = Number.parseInt(raw, 10);
5036
6981
  if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
5037
- throw new InvalidArgumentError4(`Invalid number: ${raw}`);
6982
+ throw new InvalidArgumentError6(`Invalid number: ${raw}`);
5038
6983
  }
5039
6984
  return n;
5040
6985
  }
@@ -5042,10 +6987,10 @@ async function readDescriptionFile(path) {
5042
6987
  try {
5043
6988
  return await readFile3(path, "utf8");
5044
6989
  } catch (error) {
5045
- if (findErrorCode13(error, "ENOENT")) {
6990
+ if (findErrorCode14(error, "ENOENT")) {
5046
6991
  throw new Error("Description source not found", { cause: error });
5047
6992
  }
5048
- if (findErrorCode13(error, "EISDIR")) {
6993
+ if (findErrorCode14(error, "EISDIR")) {
5049
6994
  throw new Error("Description source is not a file", { cause: error });
5050
6995
  }
5051
6996
  throw new Error("Failed to read description source", { cause: error });
@@ -5053,7 +6998,7 @@ async function readDescriptionFile(path) {
5053
6998
  }
5054
6999
  async function resolveRepositoryRootForTask(cwd, subcmd) {
5055
7000
  try {
5056
- return await resolveRepositoryRoot13(cwd);
7001
+ return await resolveRepositoryRoot12(cwd);
5057
7002
  } catch (error) {
5058
7003
  if (error instanceof Error && error.message === "Not a git repository") {
5059
7004
  throw new Error(
@@ -5064,11 +7009,11 @@ async function resolveRepositoryRootForTask(cwd, subcmd) {
5064
7009
  throw error;
5065
7010
  }
5066
7011
  }
5067
- async function assertWorkspaceInitialized11(basouRoot) {
7012
+ async function assertWorkspaceInitialized12(basouRoot) {
5068
7013
  try {
5069
- await assertBasouRootSafe14(basouRoot);
7014
+ await assertBasouRootSafe15(basouRoot);
5070
7015
  } catch (error) {
5071
- if (findErrorCode13(error, "ENOENT")) {
7016
+ if (findErrorCode14(error, "ENOENT")) {
5072
7017
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5073
7018
  }
5074
7019
  throw error;
@@ -5156,12 +7101,12 @@ function maxLen3(values, floor) {
5156
7101
 
5157
7102
  // src/commands/verify.ts
5158
7103
  import {
5159
- assertBasouRootSafe as assertBasouRootSafe15,
5160
- basouPaths as basouPaths15,
7104
+ assertBasouRootSafe as assertBasouRootSafe16,
7105
+ basouPaths as basouPaths18,
5161
7106
  enumerateSessionDirs as enumerateSessionDirs3,
5162
- findErrorCode as findErrorCode14,
5163
- resolveRepositoryRoot as resolveRepositoryRoot14,
5164
- resolveSessionId as resolveSessionId4,
7107
+ findErrorCode as findErrorCode15,
7108
+ resolveRepositoryRoot as resolveRepositoryRoot13,
7109
+ resolveSessionId as resolveSessionId5,
5165
7110
  verifyEventsChain
5166
7111
  } from "@basou/core";
5167
7112
  function registerVerifyCommand(program2) {
@@ -5183,9 +7128,9 @@ async function doRunVerify(options, ctx) {
5183
7128
  }
5184
7129
  const cwd = ctx.cwd ?? process.cwd();
5185
7130
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
5186
- const paths = basouPaths15(repositoryRoot);
5187
- await assertWorkspaceInitialized12(paths.root);
5188
- 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);
5189
7134
  const rows = [];
5190
7135
  for (const sessionId of sessionIds) {
5191
7136
  const verdict = await verifyEventsChain(paths, sessionId);
@@ -5231,7 +7176,7 @@ function renderVerdict(row) {
5231
7176
  }
5232
7177
  async function resolveRepositoryRootForVerify(cwd) {
5233
7178
  try {
5234
- return await resolveRepositoryRoot14(cwd);
7179
+ return await resolveRepositoryRoot13(cwd);
5235
7180
  } catch (error) {
5236
7181
  if (error instanceof Error && error.message === "Not a git repository") {
5237
7182
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -5241,11 +7186,11 @@ async function resolveRepositoryRootForVerify(cwd) {
5241
7186
  throw error;
5242
7187
  }
5243
7188
  }
5244
- async function assertWorkspaceInitialized12(basouRoot) {
7189
+ async function assertWorkspaceInitialized13(basouRoot) {
5245
7190
  try {
5246
- await assertBasouRootSafe15(basouRoot);
7191
+ await assertBasouRootSafe16(basouRoot);
5247
7192
  } catch (error) {
5248
- if (findErrorCode14(error, "ENOENT")) {
7193
+ if (findErrorCode15(error, "ENOENT")) {
5249
7194
  throw new Error("Workspace not initialized. Run 'basou init' first.");
5250
7195
  }
5251
7196
  throw error;
@@ -5255,22 +7200,22 @@ async function assertWorkspaceInitialized12(basouRoot) {
5255
7200
  // src/commands/view.ts
5256
7201
  import { spawn } from "child_process";
5257
7202
  import { createHash } from "crypto";
5258
- import { basename as basename4, resolve as resolve6 } from "path";
7203
+ import { basename as basename5, resolve as resolve7 } from "path";
5259
7204
  import {
5260
- assertBasouRootSafe as assertBasouRootSafe16,
5261
- basouPaths as basouPaths16,
5262
- findErrorCode as findErrorCode16,
5263
- readManifest as readManifest10,
5264
- resolveRepositoryRoot as resolveRepositoryRoot15
7205
+ assertBasouRootSafe as assertBasouRootSafe17,
7206
+ basouPaths as basouPaths19,
7207
+ findErrorCode as findErrorCode17,
7208
+ readManifest as readManifest12,
7209
+ resolveRepositoryRoot as resolveRepositoryRoot14
5265
7210
  } from "@basou/core";
5266
- import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
7211
+ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
5267
7212
 
5268
7213
  // src/lib/portfolio-safety.ts
5269
7214
  import { execFile } from "child_process";
5270
7215
  import { lstat, realpath } from "fs/promises";
5271
- import { isAbsolute as isAbsolute4, join as join9, relative as relative3, resolve as resolve5 } from "path";
7216
+ import { isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve6 } from "path";
5272
7217
  import { promisify } from "util";
5273
- import { readManifest as readManifest8 } from "@basou/core";
7218
+ import { readManifest as readManifest10 } from "@basou/core";
5274
7219
  var execFileAsync = promisify(execFile);
5275
7220
  function errorCode(error) {
5276
7221
  return error instanceof Error ? error.code : void 0;
@@ -5279,12 +7224,12 @@ async function canonical(p) {
5279
7224
  try {
5280
7225
  return await realpath(p);
5281
7226
  } catch {
5282
- return resolve5(p);
7227
+ return resolve6(p);
5283
7228
  }
5284
7229
  }
5285
7230
  function isInside(child, parent) {
5286
- const rel = relative3(parent, child);
5287
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
7231
+ const rel = relative4(parent, child);
7232
+ return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
5288
7233
  }
5289
7234
  function isBasouPath(p) {
5290
7235
  return p === ".basou" || p.startsWith(".basou/") || p.includes("/.basou/") || p.endsWith("/.basou");
@@ -5292,7 +7237,7 @@ function isBasouPath(p) {
5292
7237
  async function inspectRepo(repoPath) {
5293
7238
  let hasEntry = false;
5294
7239
  try {
5295
- await lstat(join9(repoPath, ".basou"));
7240
+ await lstat(join10(repoPath, ".basou"));
5296
7241
  hasEntry = true;
5297
7242
  } catch (error) {
5298
7243
  if (errorCode(error) !== "ENOENT") {
@@ -5323,7 +7268,7 @@ async function checkPortfolioSafety(workspaces) {
5323
7268
  const wsReal = await canonical(ws.repoRoot);
5324
7269
  let sourceRoots = [];
5325
7270
  try {
5326
- const manifest = await readManifest8(ws.paths);
7271
+ const manifest = await readManifest10(ws.paths);
5327
7272
  sourceRoots = manifest.import?.source_roots ?? [];
5328
7273
  } catch (error) {
5329
7274
  if (error instanceof Error && error.message === "YAML file not found") {
@@ -5341,7 +7286,7 @@ async function checkPortfolioSafety(workspaces) {
5341
7286
  }
5342
7287
  const monitored = /* @__PURE__ */ new Map();
5343
7288
  for (const root of sourceRoots) {
5344
- const display = resolve5(ws.repoRoot, root);
7289
+ const display = resolve6(ws.repoRoot, root);
5345
7290
  const real = await canonical(display);
5346
7291
  if (real !== wsReal) monitored.set(real, display);
5347
7292
  }
@@ -5393,18 +7338,18 @@ function formatSafetyReport(result) {
5393
7338
 
5394
7339
  // src/lib/view-server.ts
5395
7340
  import { createServer } from "http";
5396
- import { join as join10 } from "path";
7341
+ import { join as join11 } from "path";
5397
7342
  import {
5398
7343
  computeWorkStats as computeWorkStats2,
5399
7344
  enumerateApprovals as enumerateApprovals2,
5400
- findErrorCode as findErrorCode15,
7345
+ findErrorCode as findErrorCode16,
5401
7346
  isLazyExpired as isLazyExpired2,
5402
7347
  loadApproval as loadApproval2,
5403
7348
  loadSessionEntries as loadSessionEntries3,
5404
7349
  loadTaskEntries as loadTaskEntries2,
5405
7350
  readAllEvents as readAllEvents2,
5406
- readManifest as readManifest9,
5407
- readMarkdownFile as readMarkdownFile4,
7351
+ readManifest as readManifest11,
7352
+ readMarkdownFile as readMarkdownFile5,
5408
7353
  readSessionYaml as readSessionYaml3,
5409
7354
  readTaskFile as readTaskFile2,
5410
7355
  renderDecisions as renderDecisions3,
@@ -5591,7 +7536,9 @@ var VIEW_HTML = `<!doctype html>
5591
7536
  if (!data) return 'ok';
5592
7537
  if (data.claudeCode || data.codex) {
5593
7538
  return 'claude-code ' + imp(data.claudeCode) + ', codex ' + imp(data.codex)
5594
- + (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
+ : '');
5595
7542
  }
5596
7543
  if (data.status === 'ran') return imp(data);
5597
7544
  if (data.status === 'skipped') return 'skipped (' + data.reason + ')';
@@ -6048,7 +7995,7 @@ function startViewServer(opts) {
6048
7995
  };
6049
7996
  let boundPort = port;
6050
7997
  const getPort = () => boundPort;
6051
- return new Promise((resolve7, reject) => {
7998
+ return new Promise((resolve8, reject) => {
6052
7999
  const server = createServer((req, res) => {
6053
8000
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
6054
8001
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -6059,7 +8006,7 @@ function startViewServer(opts) {
6059
8006
  const address = server.address();
6060
8007
  boundPort = isAddressInfo(address) ? address.port : port;
6061
8008
  server.off("error", reject);
6062
- resolve7({
8009
+ resolve8({
6063
8010
  url: `http://${host}:${boundPort}`,
6064
8011
  port: boundPort,
6065
8012
  close: () => closeServer(server)
@@ -6071,8 +8018,8 @@ function isAddressInfo(value) {
6071
8018
  return value !== null && typeof value === "object";
6072
8019
  }
6073
8020
  function closeServer(server) {
6074
- return new Promise((resolve7) => {
6075
- server.close(() => resolve7());
8021
+ return new Promise((resolve8) => {
8022
+ server.close(() => resolve8());
6076
8023
  server.closeAllConnections();
6077
8024
  });
6078
8025
  }
@@ -6289,9 +8236,9 @@ async function captureStaleness(ws, nowIso) {
6289
8236
  async function overview(ws, nowProvider) {
6290
8237
  let manifest;
6291
8238
  try {
6292
- manifest = await readManifest9(ws.paths);
8239
+ manifest = await readManifest11(ws.paths);
6293
8240
  } catch (error) {
6294
- if (findErrorCode15(error, "ENOENT")) {
8241
+ if (findErrorCode16(error, "ENOENT")) {
6295
8242
  return { initialized: false, repoRoot: ws.repoRoot };
6296
8243
  }
6297
8244
  throw error;
@@ -6346,7 +8293,7 @@ async function sessionDetail(ws, sessionId) {
6346
8293
  throw error;
6347
8294
  }
6348
8295
  try {
6349
- const events = await readAllEvents2(join10(ws.paths.sessions, sessionId));
8296
+ const events = await readAllEvents2(join11(ws.paths.sessions, sessionId));
6350
8297
  return { session, events };
6351
8298
  } catch {
6352
8299
  return { session, events: [], degraded: true };
@@ -6368,7 +8315,7 @@ async function taskDetail(ws, taskId) {
6368
8315
  }
6369
8316
  }
6370
8317
  async function decisionsView(ws, nowProvider) {
6371
- const fromDisk = await readMarkdownFile4(ws.paths.files.decisions);
8318
+ const fromDisk = await readMarkdownFile5(ws.paths.files.decisions);
6372
8319
  if (fromDisk !== null) {
6373
8320
  return { body: fromDisk, fromDisk: true };
6374
8321
  }
@@ -6391,7 +8338,7 @@ async function approvalsView(ws, nowProvider) {
6391
8338
  return { pending: await toViews(ids.pending), resolved: await toViews(ids.resolved) };
6392
8339
  }
6393
8340
  async function handoffView(ws, nowProvider) {
6394
- const fromDisk = await readMarkdownFile4(ws.paths.files.handoff);
8341
+ const fromDisk = await readMarkdownFile5(ws.paths.files.handoff);
6395
8342
  if (fromDisk !== null) {
6396
8343
  return { body: fromDisk, fromDisk: true };
6397
8344
  }
@@ -6486,7 +8433,7 @@ var DEFAULT_PORT = 4319;
6486
8433
  function parsePort(value) {
6487
8434
  const port = Number.parseInt(value, 10);
6488
8435
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
6489
- 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.");
6490
8437
  }
6491
8438
  return port;
6492
8439
  }
@@ -6560,18 +8507,18 @@ async function doRunView(options, ctx) {
6560
8507
  }
6561
8508
  async function buildSingleDeps(ctx, cwd) {
6562
8509
  const repositoryRoot = await resolveRepositoryRootForView(cwd);
6563
- const paths = basouPaths16(repositoryRoot);
6564
- await assertWorkspaceInitialized13(paths.root);
8510
+ const paths = basouPaths19(repositoryRoot);
8511
+ await assertWorkspaceInitialized14(paths.root);
6565
8512
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
6566
8513
  return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
6567
8514
  }
6568
8515
  async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
6569
- const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve6(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
8516
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve7(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
6570
8517
  const entries = [];
6571
8518
  const seenPath = /* @__PURE__ */ new Set();
6572
8519
  const seenKey = /* @__PURE__ */ new Set();
6573
8520
  for (const spec of specs) {
6574
- const repoRoot = resolve6(spec.path);
8521
+ const repoRoot = resolve7(spec.path);
6575
8522
  if (seenPath.has(repoRoot)) continue;
6576
8523
  seenPath.add(repoRoot);
6577
8524
  const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
@@ -6584,14 +8531,14 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
6584
8531
  return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
6585
8532
  }
6586
8533
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
6587
- const paths = basouPaths16(repoRoot);
8534
+ const paths = basouPaths19(repoRoot);
6588
8535
  const importCtx = {
6589
8536
  cwd: repoRoot,
6590
8537
  ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
6591
8538
  ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
6592
8539
  };
6593
8540
  try {
6594
- const manifest = await readManifest10(paths);
8541
+ const manifest = await readManifest12(paths);
6595
8542
  return {
6596
8543
  key: manifest.workspace.id,
6597
8544
  label: labelOverride ?? manifest.workspace.name,
@@ -6604,7 +8551,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
6604
8551
  const notFound = error instanceof Error && error.message === "YAML file not found";
6605
8552
  return {
6606
8553
  key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
6607
- label: labelOverride ?? basename4(repoRoot),
8554
+ label: labelOverride ?? basename5(repoRoot),
6608
8555
  paths,
6609
8556
  repoRoot,
6610
8557
  importCtx,
@@ -6620,7 +8567,7 @@ async function startListening(port, deps) {
6620
8567
  try {
6621
8568
  return await startViewServer({ port, deps });
6622
8569
  } catch (error) {
6623
- if (findErrorCode16(error, "EADDRINUSE")) {
8570
+ if (findErrorCode17(error, "EADDRINUSE")) {
6624
8571
  throw new Error(`Port ${port} is already in use. Pass --port <n> to choose another.`, {
6625
8572
  cause: error
6626
8573
  });
@@ -6643,7 +8590,7 @@ function openInBrowser(url, override) {
6643
8590
  }
6644
8591
  }
6645
8592
  function waitForShutdown(signal) {
6646
- return new Promise((resolve7) => {
8593
+ return new Promise((resolve8) => {
6647
8594
  const cleanup = () => {
6648
8595
  process.off("SIGINT", onSignal);
6649
8596
  process.off("SIGTERM", onSignal);
@@ -6651,18 +8598,18 @@ function waitForShutdown(signal) {
6651
8598
  };
6652
8599
  const onSignal = () => {
6653
8600
  cleanup();
6654
- resolve7();
8601
+ resolve8();
6655
8602
  };
6656
8603
  const onAbort = () => {
6657
8604
  cleanup();
6658
- resolve7();
8605
+ resolve8();
6659
8606
  };
6660
8607
  process.on("SIGINT", onSignal);
6661
8608
  process.on("SIGTERM", onSignal);
6662
8609
  if (signal !== void 0) {
6663
8610
  if (signal.aborted) {
6664
8611
  cleanup();
6665
- resolve7();
8612
+ resolve8();
6666
8613
  return;
6667
8614
  }
6668
8615
  signal.addEventListener("abort", onAbort);
@@ -6671,7 +8618,7 @@ function waitForShutdown(signal) {
6671
8618
  }
6672
8619
  async function resolveRepositoryRootForView(cwd) {
6673
8620
  try {
6674
- return await resolveRepositoryRoot15(cwd);
8621
+ return await resolveRepositoryRoot14(cwd);
6675
8622
  } catch (error) {
6676
8623
  if (error instanceof Error && error.message === "Not a git repository") {
6677
8624
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
@@ -6681,11 +8628,11 @@ async function resolveRepositoryRootForView(cwd) {
6681
8628
  throw error;
6682
8629
  }
6683
8630
  }
6684
- async function assertWorkspaceInitialized13(basouRoot) {
8631
+ async function assertWorkspaceInitialized14(basouRoot) {
6685
8632
  try {
6686
- await assertBasouRootSafe16(basouRoot);
8633
+ await assertBasouRootSafe17(basouRoot);
6687
8634
  } catch (error) {
6688
- if (findErrorCode16(error, "ENOENT")) {
8635
+ if (findErrorCode17(error, "ENOENT")) {
6689
8636
  throw new Error("Workspace not initialized. Run 'basou init' first.");
6690
8637
  }
6691
8638
  throw error;
@@ -6711,11 +8658,14 @@ function buildProgram() {
6711
8658
  registerViewCommand(program2);
6712
8659
  registerApprovalCommand(program2);
6713
8660
  registerDecisionCommand(program2);
8661
+ registerNoteCommand(program2);
6714
8662
  registerTaskCommand(program2);
6715
8663
  registerHandoffCommand(program2);
6716
8664
  registerDecisionsCommand(program2);
6717
8665
  registerReportCommand(program2);
6718
8666
  registerOrientCommand(program2);
8667
+ registerReviewGapsCommand(program2);
8668
+ registerProjectCommand(program2);
6719
8669
  return program2;
6720
8670
  }
6721
8671