@basou/cli 0.25.0 → 0.27.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
@@ -3364,9 +3364,14 @@ async function assertWorkspaceInitialized7(basouRoot) {
3364
3364
 
3365
3365
  // src/commands/project.ts
3366
3366
  import {
3367
+ closeSync,
3368
+ copyFileSync,
3367
3369
  existsSync,
3370
+ constants as fsConstants,
3371
+ ftruncateSync,
3368
3372
  lstatSync,
3369
3373
  mkdirSync,
3374
+ openSync,
3370
3375
  readdirSync,
3371
3376
  readFileSync,
3372
3377
  readlinkSync,
@@ -3374,11 +3379,16 @@ import {
3374
3379
  statSync,
3375
3380
  symlinkSync,
3376
3381
  unlinkSync,
3377
- writeFileSync
3382
+ writeFileSync,
3383
+ writeSync
3378
3384
  } from "fs";
3379
3385
  import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
3380
3386
  import {
3387
+ appendBasouGitignore as appendBasouGitignore2,
3381
3388
  basouPaths as basouPaths10,
3389
+ classifyRetrofit,
3390
+ createManifest as createManifest2,
3391
+ ensureBasouDirectory as ensureBasouDirectory2,
3382
3392
  GENERATED_END,
3383
3393
  GENERATED_START,
3384
3394
  isGitNotFound,
@@ -3392,7 +3402,9 @@ import {
3392
3402
  readManifest as readManifest6,
3393
3403
  readMarkdownFile as readMarkdownFile4,
3394
3404
  reconcileSourceRoots,
3405
+ removeMarkerSection,
3395
3406
  renderWithMarkers as renderWithMarkers4,
3407
+ resolveRepositoryRoot as resolveRepositoryRoot8,
3396
3408
  safeSimpleGit,
3397
3409
  summarizePresetPlan,
3398
3410
  summarizeRosterDrift,
@@ -3474,6 +3486,38 @@ function registerProjectCommand(program2) {
3474
3486
  ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (oldPath, newPath, opts) => {
3475
3487
  await runProjectRename(oldPath, newPath, opts);
3476
3488
  });
3489
+ project.command("teardown").argument("<repo>", "The repo path whose basou-generated wiring to tear down (e.g. ../takuhon)").description(
3490
+ "Remove the basou-generated wiring for one repo: its instruction symlinks (AGENTS.md / CLAUDE.md / copilot), its `.gitignore` patterns, its workspace view symlink, and the generated block in the anchor's canonical. Dry-run by default (a classified plan: removable / foreign / blocked); pass --apply to remove ONLY the verified-basou artifacts, re-checking each just before it acts \u2014 a real file, a foreign symlink, or hand-authored canonical prose is never touched. This is the destructive counterpart to `archive` (which only drops the manifest declaration): archive first, then teardown to clean the on-disk wiring. The anchor (`.`) is refused. Not reversible \u2014 the manifest is git-tracked but the removed symlinks/lines are not"
3491
+ ).option("--apply", "Remove the verified-basou artifacts (default: dry-run classified preview)").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
3492
+ await runProjectTeardown(repo, opts);
3493
+ });
3494
+ project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
3495
+ "Scaffold a new project from scratch at the current Git repository (the anchor): create `.basou/` and seed the manifest with a candidate `repos` roster (the anchor plus any given repos, which must already be git repositories) and a `workspace.view` placeholder. Dry-run by default; pass --apply to write. Pass --no-view for a solo project. The greenfield entry point \u2014 declare visibility/language per repo afterward, then run `basou project derive --apply` to materialize the wiring"
3496
+ ).option("--apply", "Create `.basou/` and write the seeded manifest (default: dry-run preview)").option(
3497
+ "--view <path>",
3498
+ "Override the workspace view path (default: a <name>-workspace sibling)"
3499
+ ).option("--no-view", "Solo project: declare no workspace view").option(
3500
+ "--local-only",
3501
+ "Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
3502
+ ).option("-f, --force", "Overwrite an existing manifest").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repos, opts) => {
3503
+ await runProjectNew(repos, opts);
3504
+ });
3505
+ project.command("derive").description(
3506
+ "Materialize a project's full wiring from the declared manifest: sync `source_roots` to the roster, generate each repo's canonical preset block, its instruction-file symlinks, the workspace view, and each public repo's .gitignore \u2014 in dependency order. Dry-run by default; pass --apply to write. The greenfield counterpart to `new` (run after declaring visibility/language) and a one-shot maintenance pass. Re-runnable: each step is idempotent, so a partial apply recovers on a second run"
3507
+ ).option("--apply", "Run every step in apply mode (default: dry-run preview)").option("-v, --verbose", "Show error causes").action(async (opts) => {
3508
+ await runProjectDerive(opts);
3509
+ });
3510
+ project.command("retrofit").argument(
3511
+ "<repo>",
3512
+ "The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo)"
3513
+ ).description(
3514
+ "Fold an existing repo's hand-authored AGENTS.md into the project topology: move the repo's regular-file `AGENTS.md` to the anchor canonical (`agents/<repo>/AGENTS.md`) and replace it with a symlink, so the prose lives at the single source of truth. Dry-run by default; pass --apply to relocate. The onboarding counterpart to `new` for a repo that already carries its own AGENTS.md \u2014 run it before `basou project derive`, which then adds the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore. Non-destructive: it refuses when the destination canonical already exists (it never clobbers it), and skips a repo whose AGENTS.md is already a symlink or absent. The anchor (`.`) is refused"
3515
+ ).option(
3516
+ "--apply",
3517
+ "Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
3518
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
3519
+ await runProjectRetrofit(repo, opts);
3520
+ });
3477
3521
  }
3478
3522
  async function runProjectCheck(options, ctx = {}) {
3479
3523
  try {
@@ -3489,7 +3533,7 @@ function effectiveSourceRoots(manifest) {
3489
3533
  function preservedUnknownLines(fields) {
3490
3534
  if (fields.length === 0) return [];
3491
3535
  return [
3492
- `\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(", ")}`,
3536
+ `\u2139\uFE0F Preserving ${fields.length} unrecognized top-level manifest field${fields.length === 1 ? "" : "s"} (kept on write, never dropped): ${fields.join(", ")}`,
3493
3537
  ""
3494
3538
  ];
3495
3539
  }
@@ -3511,39 +3555,41 @@ async function doRunProjectCheck(options, ctx) {
3511
3555
  }
3512
3556
  function renderProjectCheck(summary) {
3513
3557
  const lines = [];
3514
- lines.push("# \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210\u30C1\u30A7\u30C3\u30AF(\u5BA3\u8A00 vs \u6355\u6349)");
3558
+ lines.push("# Project composition check (declared vs captured)");
3515
3559
  lines.push("");
3516
3560
  if (summary.declaredCount === 0) {
3517
3561
  lines.push(
3518
- "\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"
3562
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Running on `source_roots` alone, so there is nothing to compare the declaration against."
3519
3563
  );
3520
3564
  if (summary.extra.length > 0) {
3521
3565
  lines.push("");
3522
- lines.push(`\u6355\u6349\u4E2D\u306E source_roots (${summary.extra.length}):`);
3566
+ lines.push(`Captured source_roots (${summary.extra.length}):`);
3523
3567
  for (const p of summary.extra) lines.push(`- ${p}`);
3524
3568
  }
3525
3569
  return lines.join("\n");
3526
3570
  }
3527
3571
  if (summary.gaps.length === 0) {
3528
3572
  lines.push(
3529
- `\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`
3573
+ `\u2705 All ${summary.declaredCount} declared repo${summary.declaredCount === 1 ? " is" : "s are"} covered by the capture config (source_roots).`
3530
3574
  );
3531
3575
  } else {
3532
- 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)`);
3576
+ lines.push(
3577
+ `\u26A0\uFE0F Declared but not captured: ${summary.gaps.length} repo${summary.gaps.length === 1 ? "" : "s"}`
3578
+ );
3533
3579
  for (const g of summary.gaps) {
3534
- lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 source_roots \u306B\u672A\u767B\u9332`);
3580
+ lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 not in source_roots`);
3535
3581
  }
3536
3582
  }
3537
3583
  lines.push("");
3538
3584
  if (summary.extra.length > 0) {
3539
3585
  lines.push(
3540
- `## \u5BA3\u8A00\u5916\u306E\u6355\u6349\u5BFE\u8C61 (${summary.extra.length}) \u2014 workspace view \u304B\u3001\u5BA3\u8A00\u6F0F\u308C\u306E\u53EF\u80FD\u6027`
3586
+ `## Captured but undeclared (${summary.extra.length}) \u2014 the workspace view, or a missing declaration`
3541
3587
  );
3542
3588
  for (const p of summary.extra) lines.push(`- ${p}`);
3543
3589
  lines.push("");
3544
3590
  }
3545
3591
  lines.push(
3546
- "\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"
3592
+ "Note: read-only advisory. It only shows the difference between the declaration (repos) and the capture config (source_roots); it does not enforce."
3547
3593
  );
3548
3594
  return lines.join("\n");
3549
3595
  }
@@ -3593,29 +3639,33 @@ async function doRunProjectSync(options, ctx) {
3593
3639
  }
3594
3640
  function renderProjectSync(result) {
3595
3641
  const lines = [];
3596
- lines.push("# source_roots \u540C\u671F(\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC \u2192 \u6355\u6349\u8A2D\u5B9A)");
3642
+ lines.push("# source_roots sync (declared roster \u2192 capture config)");
3597
3643
  lines.push("");
3598
3644
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
3599
3645
  if (!result.hasRoster) {
3600
3646
  lines.push(
3601
- "\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"
3647
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). There is no declaration to sync from, so nothing changes."
3602
3648
  );
3603
3649
  return lines.join("\n");
3604
3650
  }
3605
3651
  if (result.unchanged) {
3606
- 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");
3652
+ lines.push("\u2705 source_roots already covers the entire declared roster (nothing to sync).");
3607
3653
  return lines.join("\n");
3608
3654
  }
3609
3655
  if (result.applied) {
3610
- lines.push(`\u2705 source_roots \u306B ${result.added.length} \u4EF6\u8FFD\u52A0\u3057\u307E\u3057\u305F:`);
3656
+ lines.push(
3657
+ `\u2705 Added ${result.added.length} entr${result.added.length === 1 ? "y" : "ies"} to source_roots:`
3658
+ );
3611
3659
  for (const p of result.added) lines.push(`- ${p}`);
3612
3660
  } else {
3613
3661
  lines.push(
3614
- `${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):`
3662
+ `${result.added.length} repo${result.added.length === 1 ? " is" : "s are"} not in source_roots. To add (dry-run; pass --apply to write):`
3615
3663
  );
3616
3664
  for (const p of result.added) lines.push(`- ${p}`);
3617
3665
  lines.push("");
3618
- 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");
3666
+ lines.push(
3667
+ "Note: existing source_roots are kept; only the missing entries are appended (nothing is removed)."
3668
+ );
3619
3669
  }
3620
3670
  return lines.join("\n");
3621
3671
  }
@@ -3675,37 +3725,41 @@ async function doRunProjectAdopt(options, ctx) {
3675
3725
  }
3676
3726
  function renderProjectAdopt(result) {
3677
3727
  const lines = [];
3678
- lines.push("# repo \u30ED\u30FC\u30B9\u30BF\u30FC\u306E bootstrap(source_roots \u2192 repos)");
3728
+ lines.push("# Bootstrap repo roster (source_roots \u2192 repos)");
3679
3729
  lines.push("");
3680
3730
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
3681
3731
  if (result.alreadyDeclared) {
3682
3732
  lines.push(
3683
- "\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"
3733
+ "\u2139\uFE0F A repo roster (manifest `repos`) is already declared. adopt is a one-time bootstrap, so it writes nothing. Use `project check` / `project sync` for ongoing maintenance."
3684
3734
  );
3685
3735
  return lines.join("\n");
3686
3736
  }
3687
3737
  if (result.repos.length === 0) {
3688
- 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");
3738
+ lines.push("\u2139\uFE0F No git repo found in source_roots (nothing to bootstrap).");
3689
3739
  } else if (result.applied) {
3690
- lines.push(`\u2705 ${result.repos.length} repo \u3092 repos \u30ED\u30FC\u30B9\u30BF\u30FC\u306B\u66F8\u304D\u8FBC\u307F\u307E\u3057\u305F:`);
3740
+ lines.push(
3741
+ `\u2705 Wrote ${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to the repos roster:`
3742
+ );
3691
3743
  for (const r of result.repos) lines.push(`- ${r.path}`);
3692
3744
  lines.push("");
3693
3745
  lines.push(
3694
- "\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"
3746
+ "Note: visibility is unset. Assign public / private / future-public to each repo manually."
3695
3747
  );
3696
3748
  } else {
3697
3749
  lines.push(
3698
- `${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):`
3750
+ `${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to declare in the repos roster (dry-run; pass --apply to write):`
3699
3751
  );
3700
3752
  for (const r of result.repos) lines.push(`- ${r.path}`);
3701
3753
  lines.push("");
3702
- 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");
3754
+ lines.push("Note: visibility is proposed unset; assign it manually after applying.");
3703
3755
  }
3704
3756
  if (result.excluded.length > 0) {
3705
3757
  lines.push("");
3706
- lines.push(`## \u9664\u5916 (${result.excluded.length}) \u2014 git repo \u3067\u306F\u306A\u3044\u305F\u3081 repos \u306B\u542B\u3081\u307E\u305B\u3093`);
3758
+ lines.push(
3759
+ `## Excluded (${result.excluded.length}) \u2014 not a git repo, so not included in repos`
3760
+ );
3707
3761
  for (const e of result.excluded) {
3708
- const reason = e.kind === "non-repo" ? "\u975E repo(workspace view / tmp \u7B49)" : "\u89E3\u6C7A\u4E0D\u80FD(\u30D1\u30B9\u304C\u5B58\u5728\u3057\u306A\u3044)";
3762
+ const reason = e.kind === "non-repo" ? "not a repo (workspace view / tmp, etc.)" : "unresolvable (path does not exist)";
3709
3763
  lines.push(`- ${e.path} \u2014 ${reason}`);
3710
3764
  }
3711
3765
  }
@@ -3773,50 +3827,56 @@ async function doRunProjectWiring(options, ctx) {
3773
3827
  }
3774
3828
  function renderProjectWiring(result) {
3775
3829
  const lines = [];
3776
- 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)");
3830
+ lines.push(
3831
+ "# Instruction-file wiring check (declared roster \xD7 instruction-file presence / git tracking)"
3832
+ );
3777
3833
  lines.push("");
3778
3834
  if (!result.hasRoster) {
3779
3835
  lines.push(
3780
- "\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"
3836
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
3781
3837
  );
3782
3838
  return lines.join("\n");
3783
3839
  }
3784
3840
  if (result.risks.length > 0) {
3785
3841
  lines.push(
3786
- `\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)`
3842
+ `\u26A0\uFE0F Instruction files tracked by git in public-facing repos: ${result.risks.length} (canonical leak risk)`
3787
3843
  );
3788
3844
  for (const r of result.risks) {
3789
3845
  lines.push(
3790
- `- ${r.repo} [${r.visibility}] \u2014 ${r.file} \u304C tracked(gitignore \u3055\u308C\u305F symlink \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059)`
3846
+ `- ${r.repo} [${r.visibility}] \u2014 ${r.file} is tracked (it should be a gitignored symlink)`
3791
3847
  );
3792
3848
  }
3793
3849
  } else if (result.ok) {
3794
- 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");
3850
+ lines.push(
3851
+ "\u2705 No instruction file is tracked by git in a public-facing repo (no privacy risk)."
3852
+ );
3795
3853
  } else {
3796
3854
  lines.push(
3797
- "\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"
3855
+ "\u2139\uFE0F No confirmed privacy risk, but some repos are unjudgeable / unreachable (see below)."
3798
3856
  );
3799
3857
  }
3800
3858
  lines.push("");
3801
3859
  if (result.unknown.length > 0) {
3802
3860
  lines.push(
3803
- `## 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`
3861
+ `## Visibility unset (${result.unknown.length}) \u2014 privacy cannot be judged. Assign visibility in the manifest repos`
3804
3862
  );
3805
3863
  for (const p of result.unknown) lines.push(`- ${p}`);
3806
3864
  lines.push("");
3807
3865
  }
3808
3866
  if (result.incomplete.length > 0) {
3809
- 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`);
3867
+ lines.push(
3868
+ `## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
3869
+ );
3810
3870
  for (const i of result.incomplete) lines.push(`- ${i.repo} \u2014 ${i.missing.join(", ")}`);
3811
3871
  lines.push("");
3812
3872
  }
3813
3873
  if (result.unreachable.length > 0) {
3814
- lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
3874
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
3815
3875
  for (const p of result.unreachable) lines.push(`- ${p}`);
3816
3876
  lines.push("");
3817
3877
  }
3818
3878
  lines.push(
3819
- "\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"
3879
+ "Note: read-only advisory. It only shows instruction-file presence and git-tracking status; it neither generates nor enforces (for the .basou footprint, use `basou view --check`)."
3820
3880
  );
3821
3881
  return lines.join("\n");
3822
3882
  }
@@ -3895,45 +3955,47 @@ async function doRunProjectGitignore(options, ctx) {
3895
3955
  }
3896
3956
  function renderProjectGitignore(result) {
3897
3957
  const lines = [];
3898
- lines.push("# .gitignore \u751F\u6210(\u516C\u958B\u7CFB repo \u306E\u6307\u793A\u66F8\u3092\u9664\u5916)");
3958
+ lines.push("# .gitignore generation (exclude instruction files in public-facing repos)");
3899
3959
  lines.push("");
3900
3960
  if (!result.hasRoster) {
3901
3961
  lines.push(
3902
- "\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"
3962
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
3903
3963
  );
3904
3964
  return lines.join("\n");
3905
3965
  }
3906
3966
  if (result.plans.length > 0) {
3907
- const verb = result.applied ? "\u8FFD\u52A0\u3057\u307E\u3057\u305F" : "\u8FFD\u52A0\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply)";
3967
+ const verb = result.applied ? "Added to" : "To add to (dry-run; pass --apply to write)";
3908
3968
  lines.push(
3909
- `${result.applied ? "\u2705 " : ""}${result.plans.length} repo \u306E .gitignore \u306B${verb}:`
3969
+ `${result.applied ? "\u2705 " : ""}${verb} the .gitignore of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"}:`
3910
3970
  );
3911
3971
  for (const p of result.plans) lines.push(`- ${p.path} \u2014 ${p.toAdd.join(", ")}`);
3912
3972
  } else if (result.ok) {
3913
- 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");
3973
+ lines.push(
3974
+ "\u2705 Public-facing repos already exclude every instruction file in .gitignore (nothing to add)."
3975
+ );
3914
3976
  } else {
3915
3977
  lines.push(
3916
- "\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"
3978
+ "\u2139\uFE0F No public-facing repo needs an addition, but some repos are unjudgeable / unreachable (see below)."
3917
3979
  );
3918
3980
  }
3919
3981
  lines.push("");
3920
3982
  if (result.unknown.length > 0) {
3921
3983
  lines.push(
3922
- `## 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`
3984
+ `## Visibility unset (${result.unknown.length}) \u2014 skipped. Assign visibility in the manifest repos`
3923
3985
  );
3924
3986
  for (const p of result.unknown) lines.push(`- ${p}`);
3925
3987
  lines.push("");
3926
3988
  }
3927
3989
  if (result.unreachable.length > 0) {
3928
- lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
3990
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
3929
3991
  for (const p of result.unreachable) lines.push(`- ${p}`);
3930
3992
  lines.push("");
3931
3993
  }
3932
3994
  lines.push(
3933
- "\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"
3995
+ "Note: existing .gitignore lines are kept; only the missing patterns are appended (nothing is removed). private / visibility-unset repos are skipped."
3934
3996
  );
3935
3997
  lines.push(
3936
- "\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"
3998
+ "Note: appending to .gitignore does not untrack files already tracked by git. Detect tracked instruction files with `basou project wiring` and remove them with `git rm --cached <file>`."
3937
3999
  );
3938
4000
  return lines.join("\n");
3939
4001
  }
@@ -4061,11 +4123,11 @@ async function doRunProjectSymlinks(options, ctx) {
4061
4123
  }
4062
4124
  function renderProjectSymlinks(result) {
4063
4125
  const lines = [];
4064
- lines.push("# \u6307\u793A\u66F8 symlink \u751F\u6210(\u5404 repo \u2192 anchor \u306E canonical)");
4126
+ lines.push("# Instruction-file symlink generation (each repo \u2192 the anchor's canonical)");
4065
4127
  lines.push("");
4066
4128
  if (!result.hasRoster) {
4067
4129
  lines.push(
4068
- "\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"
4130
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
4069
4131
  );
4070
4132
  return lines.join("\n");
4071
4133
  }
@@ -4073,14 +4135,14 @@ function renderProjectSymlinks(result) {
4073
4135
  const attempted = result.applied || result.failures.length > 0;
4074
4136
  if (!attempted) {
4075
4137
  lines.push(
4076
- `${result.plans.length} repo \u306B\u6307\u793A\u66F8 symlink \u3092\u4F5C\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
4138
+ `Instruction-file symlinks to create in ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
4077
4139
  );
4078
4140
  for (const p of result.plans) {
4079
4141
  lines.push(`- ${p.path}`);
4080
4142
  for (const c of p.toCreate) lines.push(` ${c.name} -> ${c.target}`);
4081
4143
  }
4082
4144
  } else {
4083
- 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):";
4145
+ const header = result.failures.length === 0 ? "\u2705 Created instruction-file symlinks:" : result.applied ? "Created instruction-file symlinks (some failed, see below):" : "Could not create instruction-file symlinks (see below):";
4084
4146
  lines.push(header);
4085
4147
  for (const p of result.plans) {
4086
4148
  const failedFiles = new Set(
@@ -4093,31 +4155,35 @@ function renderProjectSymlinks(result) {
4093
4155
  }
4094
4156
  }
4095
4157
  } else if (result.ok) {
4096
- 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");
4158
+ lines.push(
4159
+ "\u2705 Every declared repo's instruction-file symlinks are correctly wired (nothing to generate)."
4160
+ );
4097
4161
  } else {
4098
4162
  lines.push(
4099
- "\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"
4163
+ "\u2139\uFE0F No symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos (see below)."
4100
4164
  );
4101
4165
  }
4102
4166
  lines.push("");
4103
4167
  if (result.failures.length > 0) {
4104
- 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`);
4168
+ lines.push(
4169
+ `## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
4170
+ );
4105
4171
  for (const f of result.failures) lines.push(`- ${f.repo} \u2014 ${f.file}: ${f.message}`);
4106
4172
  lines.push("");
4107
4173
  }
4108
4174
  if (result.conflicts.length > 0) {
4109
4175
  lines.push(
4110
- `## \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`
4176
+ `## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
4111
4177
  );
4112
4178
  for (const c of result.conflicts) {
4113
- 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)";
4179
+ const detail = c.reason === "mismatch" ? `a symlink pointing elsewhere (currently: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "a real file/directory, not a symlink" : "an uninspectable path (a parent component is not a directory, etc.)";
4114
4180
  lines.push(`- ${c.repo} \u2014 ${c.file}: ${detail}`);
4115
4181
  }
4116
4182
  lines.push("");
4117
4183
  }
4118
4184
  if (result.collisions.length > 0) {
4119
4185
  lines.push(
4120
- `## canonical \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u540D canonical \u3092\u5171\u6709(\u81EA\u52D5\u914D\u7DDA\u3057\u307E\u305B\u3093)`
4186
+ `## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-wired)`
4121
4187
  );
4122
4188
  for (const c of result.collisions) {
4123
4189
  lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
@@ -4126,18 +4192,18 @@ function renderProjectSymlinks(result) {
4126
4192
  }
4127
4193
  if (result.missingCanonical.length > 0) {
4128
4194
  lines.push(
4129
- `## 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`
4195
+ `## Canonical missing (${result.missingCanonical.length}) \u2014 the anchor has no agents/<repo>/AGENTS.md, so nothing can be generated`
4130
4196
  );
4131
4197
  for (const p of result.missingCanonical) lines.push(`- ${p}`);
4132
4198
  lines.push("");
4133
4199
  }
4134
4200
  if (result.unreachable.length > 0) {
4135
- lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
4201
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4136
4202
  for (const p of result.unreachable) lines.push(`- ${p}`);
4137
4203
  lines.push("");
4138
4204
  }
4139
4205
  lines.push(
4140
- "\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"
4206
+ "Note: an existing file or a symlink pointing elsewhere is never overwritten; only the missing links are created (GEMINI.md is discontinued and not generated)."
4141
4207
  );
4142
4208
  return lines.join("\n");
4143
4209
  }
@@ -4239,7 +4305,7 @@ function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4239
4305
  names = readdirSync(viewDir);
4240
4306
  } catch (error) {
4241
4307
  if (hasErrorCode(error) && error.code === "ENOENT") return [];
4242
- 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)", {
4308
+ throw new Error("Cannot scan the workspace view (check the path / its type)", {
4243
4309
  cause: error
4244
4310
  });
4245
4311
  }
@@ -4261,7 +4327,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4261
4327
  if (c === null || c.kind !== "repo") {
4262
4328
  failed.push({
4263
4329
  name,
4264
- 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)"
4330
+ message: "the target changed since the scan (no longer a basou-generated stray repo link; re-run)"
4265
4331
  });
4266
4332
  continue;
4267
4333
  }
@@ -4350,11 +4416,11 @@ async function doRunProjectWorkspace(options, ctx) {
4350
4416
  }
4351
4417
  function renderProjectWorkspace(result) {
4352
4418
  const lines = [];
4353
- lines.push("# workspace view \u751F\u6210(roster repo \u3092\u96C6\u7D04)");
4419
+ lines.push("# workspace view generation (aggregate the roster repos)");
4354
4420
  lines.push("");
4355
4421
  if (!result.hasView) {
4356
4422
  lines.push(
4357
- "\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"
4423
+ "\u2139\uFE0F No view declared (manifest `workspace.view`). Declare the aggregation directory, then re-run."
4358
4424
  );
4359
4425
  return lines.join("\n");
4360
4426
  }
@@ -4362,12 +4428,12 @@ function renderProjectWorkspace(result) {
4362
4428
  const attempted = result.applied || result.failures.length > 0;
4363
4429
  if (!attempted) {
4364
4430
  lines.push(
4365
- `${result.toCreate.length} \u4EF6\u306E repo symlink \u3092 view \u306B\u4F5C\u6210\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
4431
+ `Repo symlinks to create in the view: ${result.toCreate.length} (dry-run; pass --apply to write):`
4366
4432
  );
4367
4433
  for (const c of result.toCreate) lines.push(` ${c.name} -> ${c.target}`);
4368
4434
  } else {
4369
4435
  const failed = new Set(result.failures.map((f) => f.name));
4370
- 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):";
4436
+ const header = result.failures.length === 0 ? "\u2705 Created repo symlinks in the view:" : result.applied ? "Created repo symlinks in the view (some failed, see below):" : "Could not create repo symlinks in the view (see below):";
4371
4437
  lines.push(header);
4372
4438
  for (const c of result.toCreate) {
4373
4439
  if (failed.has(c.name)) continue;
@@ -4376,16 +4442,18 @@ function renderProjectWorkspace(result) {
4376
4442
  }
4377
4443
  } else if (result.ok) {
4378
4444
  lines.push(
4379
- `\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`
4445
+ `\u2705 The view aggregates the entire declared roster (${result.correctCount} links; nothing to generate).`
4380
4446
  );
4381
4447
  } else {
4382
4448
  lines.push(
4383
- "\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"
4449
+ "\u2139\uFE0F No symlink needs creating, but there are items needing attention (stray / conflict / collision / unreachable repo, see below)."
4384
4450
  );
4385
4451
  }
4386
4452
  lines.push("");
4387
4453
  if (result.failures.length > 0) {
4388
- 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`);
4454
+ lines.push(
4455
+ `## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
4456
+ );
4389
4457
  for (const f of result.failures) lines.push(`- ${f.name}: ${f.message}`);
4390
4458
  lines.push("");
4391
4459
  }
@@ -4393,17 +4461,17 @@ function renderProjectWorkspace(result) {
4393
4461
  const attempted = result.pruned || result.pruneFailures.length > 0;
4394
4462
  if (result.pruneWithheld) {
4395
4463
  lines.push(
4396
- `${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):`
4464
+ `${result.toPrune.length} stray repo symlink${result.toPrune.length === 1 ? "" : "s"} were due to be pruned, but pruning was withheld because some repos are unreachable (an unreachable repo's link cannot be told apart from a stray; resolve or archive the repos below, then re-run):`
4397
4465
  );
4398
4466
  for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
4399
4467
  } else if (!attempted) {
4400
4468
  lines.push(
4401
- `${result.toPrune.length} \u4EF6\u306E stray repo symlink \u3092\u64A4\u53BB\u4E88\u5B9A(dry-run\u3001\u64A4\u53BB\u3059\u308B\u306B\u306F --prune):`
4469
+ `Stray repo symlinks to prune: ${result.toPrune.length} (dry-run; pass --prune to remove):`
4402
4470
  );
4403
4471
  for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
4404
4472
  } else {
4405
4473
  const failed = new Set(result.pruneFailures.map((f) => f.name));
4406
- 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):";
4474
+ const header = result.pruneFailures.length === 0 ? "\u{1F9F9} Pruned stray repo symlinks:" : result.pruned ? "Pruned stray repo symlinks (some failed, see below):" : "Could not prune stray repo symlinks (see below):";
4407
4475
  lines.push(header);
4408
4476
  for (const p of result.toPrune) {
4409
4477
  if (failed.has(p.name)) continue;
@@ -4414,47 +4482,47 @@ function renderProjectWorkspace(result) {
4414
4482
  }
4415
4483
  if (result.pruneFailures.length > 0) {
4416
4484
  lines.push(
4417
- `## \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`
4485
+ `## Pruning failed (${result.pruneFailures.length}) \u2014 some stray symlinks could not be pruned`
4418
4486
  );
4419
4487
  for (const f of result.pruneFailures) lines.push(`- ${f.name}: ${f.message}`);
4420
4488
  lines.push("");
4421
4489
  }
4422
4490
  if (result.conflicts.length > 0) {
4423
4491
  lines.push(
4424
- `## \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`
4492
+ `## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
4425
4493
  );
4426
4494
  for (const c of result.conflicts) {
4427
- 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)";
4495
+ const detail = c.reason === "mismatch" ? `a symlink pointing elsewhere (currently: ${c.actualTarget ?? "?"})` : c.reason === "occupied" ? "a real file/directory, not a symlink" : "an uninspectable path (a parent component is not a directory, etc.)";
4428
4496
  lines.push(`- ${c.name}: ${detail}`);
4429
4497
  }
4430
4498
  lines.push("");
4431
4499
  }
4432
4500
  if (result.collisions.length > 0) {
4433
4501
  lines.push(
4434
- `## 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)`
4502
+ `## Basename collisions (${result.collisions.length}) \u2014 another repo claims the same view name (not auto-wired)`
4435
4503
  );
4436
4504
  for (const c of result.collisions) lines.push(`- ${c.linkName} \u2190 ${c.repos.join(", ")}`);
4437
4505
  lines.push("");
4438
4506
  }
4439
4507
  if (result.unreachable.length > 0) {
4440
4508
  lines.push(
4441
- `## \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`
4509
+ `## Unreachable (${result.unreachable.length}) \u2014 path unresolved, or it resolves to the view itself, so it cannot be aggregated`
4442
4510
  );
4443
4511
  for (const p of result.unreachable) lines.push(`- ${p}`);
4444
4512
  lines.push("");
4445
4513
  }
4446
4514
  if (result.strayUnknown.length > 0) {
4447
4515
  lines.push(
4448
- `## \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`
4516
+ `## Strays left in place (${result.strayUnknown.length}) \u2014 not confirmed to be a basou-generated repo link, so not pruned. Check them manually`
4449
4517
  );
4450
4518
  for (const s of result.strayUnknown) {
4451
- 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)";
4519
+ const detail = s.reason === "broken" ? "broken link (target does not resolve)" : s.reason === "non-repo" ? "non-git-repo target (a file, or a directory without .git)" : "absolute-path target (basou generates relative links only)";
4452
4520
  lines.push(`- ${s.name} -> ${s.target}: ${detail}`);
4453
4521
  }
4454
4522
  lines.push("");
4455
4523
  }
4456
4524
  lines.push(
4457
- "\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"
4525
+ "Note: creation (--apply) never overwrites an existing entry. Stray repo links are pruned with --prune (only the symlink is removed, never the referenced repo). A stray not confirmed to be basou-generated (broken / non-repo / absolute path) is left in place."
4458
4526
  );
4459
4527
  return lines.join("\n");
4460
4528
  }
@@ -4585,15 +4653,17 @@ async function doRunProjectPreset(options, ctx) {
4585
4653
  return result;
4586
4654
  }
4587
4655
  function presetActionLabel(action) {
4588
- return action === "create" ? "\u65B0\u898F\u4F5C\u6210" : "\u66F4\u65B0";
4656
+ return action === "create" ? "create" : "update";
4589
4657
  }
4590
4658
  function renderProjectPreset(result) {
4591
4659
  const lines = [];
4592
- lines.push("# \u6307\u793A\u66F8 A \u30D7\u30EA\u30BB\u30C3\u30C8\u751F\u6210(\u5BA3\u8A00 \u2192 canonical \u306E\u751F\u6210\u9818\u57DF)");
4660
+ lines.push(
4661
+ "# Instruction-file preset generation (declaration \u2192 the canonical's generated region)"
4662
+ );
4593
4663
  lines.push("");
4594
4664
  if (!result.hasRoster) {
4595
4665
  lines.push(
4596
- "\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"
4666
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
4597
4667
  );
4598
4668
  return lines.join("\n");
4599
4669
  }
@@ -4601,7 +4671,7 @@ function renderProjectPreset(result) {
4601
4671
  const attempted = result.applied || result.failures.length > 0;
4602
4672
  if (!attempted) {
4603
4673
  lines.push(
4604
- `${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):`
4674
+ `Preset blocks to generate in the canonical of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
4605
4675
  );
4606
4676
  for (const p of result.plans) {
4607
4677
  lines.push(
@@ -4611,7 +4681,7 @@ function renderProjectPreset(result) {
4611
4681
  }
4612
4682
  } else {
4613
4683
  const failed = new Set(result.failures.map((f) => f.repo));
4614
- 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):";
4684
+ const header = result.failures.length === 0 ? "\u2705 Generated preset blocks in the canonical:" : result.applied ? "Generated preset blocks (some failed, see below):" : "Could not generate preset blocks (see below):";
4615
4685
  lines.push(header);
4616
4686
  for (const p of result.plans) {
4617
4687
  if (failed.has(p.path)) continue;
@@ -4621,47 +4691,49 @@ function renderProjectPreset(result) {
4621
4691
  }
4622
4692
  }
4623
4693
  } else if (result.ok) {
4624
- 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");
4694
+ lines.push(
4695
+ "\u2705 Every declared repo's preset block is in sync with its canonical (nothing to generate)."
4696
+ );
4625
4697
  } else {
4626
4698
  lines.push(
4627
- "\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"
4699
+ "\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos (see below)."
4628
4700
  );
4629
4701
  }
4630
4702
  lines.push("");
4631
4703
  if (result.inSync.length > 0) {
4632
- lines.push(`\u540C\u671F\u6E08\u307F (${result.inSync.length}): ${result.inSync.join(", ")}`);
4704
+ lines.push(`In sync (${result.inSync.length}): ${result.inSync.join(", ")}`);
4633
4705
  lines.push("");
4634
4706
  }
4635
4707
  if (result.failures.length > 0) {
4636
4708
  lines.push(
4637
- `## \u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557 (${result.failures.length}) \u2014 \u4E00\u90E8\u306E canonical \u3092\u66F8\u3051\u307E\u305B\u3093\u3067\u3057\u305F`
4709
+ `## Write failed (${result.failures.length}) \u2014 some canonicals could not be written`
4638
4710
  );
4639
4711
  for (const f of result.failures) lines.push(`- ${f.repo}: ${f.message}`);
4640
4712
  lines.push("");
4641
4713
  }
4642
4714
  if (result.markerConflicts.length > 0) {
4643
4715
  lines.push(
4644
- `## \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`
4716
+ `## Marker conflicts (${result.markerConflicts.length}) \u2014 the canonical's markers are missing/malformed, so it is not overwritten`
4645
4717
  );
4646
4718
  for (const c of result.markerConflicts) {
4647
- const detail = c.reason === "no_markers" ? "\u30DE\u30FC\u30AB\u30FC\u9818\u57DF\u304C\u7121\u3044" : `\u30DE\u30FC\u30AB\u30FC\u4E0D\u6574\u5408(${c.reason})`;
4719
+ const detail = c.reason === "no_markers" ? "no marker region" : `malformed markers (${c.reason})`;
4648
4720
  lines.push(`- ${c.repo}: ${detail}`);
4649
4721
  }
4650
4722
  lines.push(
4651
- ` \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`
4723
+ ` Fix: add these two lines where you want the preset block \u2014 \`${GENERATED_START}\` and \`${GENERATED_END}\` (absent, basou creates a fresh canonical).`
4652
4724
  );
4653
4725
  lines.push("");
4654
4726
  }
4655
4727
  if (result.unreadable.length > 0) {
4656
4728
  lines.push(
4657
- `## 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`
4729
+ `## Canonical unreadable (${result.unreadable.length}) \u2014 could not be read (a directory, permissions, etc.)`
4658
4730
  );
4659
4731
  for (const p of result.unreadable) lines.push(`- ${p}`);
4660
4732
  lines.push("");
4661
4733
  }
4662
4734
  if (result.collisions.length > 0) {
4663
4735
  lines.push(
4664
- `## canonical \u885D\u7A81 (${result.collisions.length}) \u2014 \u5225 repo \u304C\u540C\u540D canonical \u3092\u5171\u6709(\u81EA\u52D5\u751F\u6210\u3057\u307E\u305B\u3093)`
4736
+ `## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-generated)`
4665
4737
  );
4666
4738
  for (const c of result.collisions) {
4667
4739
  lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
@@ -4670,25 +4742,25 @@ function renderProjectPreset(result) {
4670
4742
  }
4671
4743
  if (result.undeclared.length > 0) {
4672
4744
  lines.push(
4673
- `## \u5BA3\u8A00\u306A\u3057 (${result.undeclared.length}) \u2014 visibility / language / publishes \u304C\u672A\u8A2D\u5B9A\u306E\u305F\u3081\u751F\u6210\u3057\u307E\u305B\u3093`
4745
+ `## Undeclared (${result.undeclared.length}) \u2014 visibility / language / publishes unset, so nothing is generated`
4674
4746
  );
4675
4747
  for (const p of result.undeclared) lines.push(`- ${p}`);
4676
4748
  lines.push("");
4677
4749
  }
4678
4750
  if (result.anchors.length > 0) {
4679
4751
  lines.push(
4680
- `## anchor (${result.anchors.length}) \u2014 \u81EA\u8EAB\u306E AGENTS.md \u306F\u624B\u3067\u7DAD\u6301\u3059\u308B\u305F\u3081\u30B9\u30AD\u30C3\u30D7`
4752
+ `## Anchor (${result.anchors.length}) \u2014 its own AGENTS.md is hand-maintained, so it is skipped`
4681
4753
  );
4682
4754
  for (const p of result.anchors) lines.push(`- ${p}`);
4683
4755
  lines.push("");
4684
4756
  }
4685
4757
  if (result.unreachable.length > 0) {
4686
- lines.push(`## \u5230\u9054\u4E0D\u80FD (${result.unreachable.length}) \u2014 \u30D1\u30B9\u672A\u89E3\u6C7A / git repo \u3067\u306A\u3044`);
4758
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4687
4759
  for (const p of result.unreachable) lines.push(`- ${p}`);
4688
4760
  lines.push("");
4689
4761
  }
4690
4762
  lines.push(
4691
- "\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"
4763
+ "Note: only the marker region is generated; the canonical's hand-authored content (outside the markers) is preserved. The generated content is derived from the manifest declaration."
4692
4764
  );
4693
4765
  return lines.join("\n");
4694
4766
  }
@@ -4749,6 +4821,403 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4749
4821
  canonical: canonical2
4750
4822
  };
4751
4823
  }
4824
+ function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
4825
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4826
+ return expectedSymlinkTargets(repoReal, canonicalFile);
4827
+ }
4828
+ function viewLinkPointsAt(viewDir, name, repoReal) {
4829
+ const filePath = join7(viewDir, name);
4830
+ try {
4831
+ if (!lstatSync(filePath).isSymbolicLink()) return false;
4832
+ const target = readlinkSync(filePath);
4833
+ if (isAbsolute3(target)) return false;
4834
+ return realpathSync(resolve7(viewDir, target)) === repoReal;
4835
+ } catch {
4836
+ return false;
4837
+ }
4838
+ }
4839
+ function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
4840
+ const filePath = join7(viewDir, name);
4841
+ try {
4842
+ if (!lstatSync(filePath).isSymbolicLink()) return false;
4843
+ const target = readlinkSync(filePath);
4844
+ if (isAbsolute3(target)) return false;
4845
+ return resolve7(viewDir, target) === expectedRepoPath;
4846
+ } catch {
4847
+ return false;
4848
+ }
4849
+ }
4850
+ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4851
+ const anchorReal = realpathSync(repositoryRoot);
4852
+ let repoReal;
4853
+ try {
4854
+ repoReal = realpathSync(resolve7(repositoryRoot, target));
4855
+ } catch {
4856
+ repoReal = void 0;
4857
+ }
4858
+ const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
4859
+ const targetAbs = resolve7(repositoryRoot, target);
4860
+ const canonicalName = basename4(repoReal ?? targetAbs);
4861
+ const roster = manifest.repos ?? [];
4862
+ const inRoster = roster.some((r) => {
4863
+ try {
4864
+ return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
4865
+ } catch {
4866
+ return resolve7(repositoryRoot, r.path) === targetAbs;
4867
+ }
4868
+ });
4869
+ const cnFold = canonicalName.toLowerCase();
4870
+ const canonicalShared = roster.some((r) => {
4871
+ let rReal = null;
4872
+ try {
4873
+ rReal = realpathSync(resolve7(repositoryRoot, r.path));
4874
+ } catch {
4875
+ rReal = null;
4876
+ }
4877
+ if (rReal !== null) {
4878
+ if (repoReal !== void 0 && rReal === repoReal) return false;
4879
+ return basename4(rReal).toLowerCase() === cnFold;
4880
+ }
4881
+ if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
4882
+ return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
4883
+ });
4884
+ const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
4885
+ const items = [];
4886
+ if (!isAnchor) {
4887
+ if (repoReal !== void 0) {
4888
+ for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
4889
+ const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
4890
+ if (state === "correct")
4891
+ items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
4892
+ else if (state === "mismatch")
4893
+ items.push({
4894
+ kind: "instruction-symlink",
4895
+ label: spec.name,
4896
+ state: "foreign",
4897
+ note: `points at a different target (${actualTarget ?? "?"})`
4898
+ });
4899
+ else if (state === "occupied")
4900
+ items.push({
4901
+ kind: "instruction-symlink",
4902
+ label: spec.name,
4903
+ state: "foreign",
4904
+ note: "a real file, not a symlink"
4905
+ });
4906
+ else if (state === "blocked")
4907
+ items.push({
4908
+ kind: "instruction-symlink",
4909
+ label: spec.name,
4910
+ state: "blocked",
4911
+ note: "could not be inspected"
4912
+ });
4913
+ }
4914
+ let ignored;
4915
+ try {
4916
+ ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
4917
+ for (const p of INSTRUCTION_FILES) {
4918
+ if (ignored.has(p) || ignored.has(`/${p}`)) {
4919
+ items.push({
4920
+ kind: "gitignore",
4921
+ label: p,
4922
+ state: "manual",
4923
+ note: "cannot tell a basou-appended line from a hand-added one (no marker) \u2014 remove manually"
4924
+ });
4925
+ }
4926
+ }
4927
+ } catch {
4928
+ items.push({
4929
+ kind: "gitignore",
4930
+ label: ".gitignore",
4931
+ state: "blocked",
4932
+ note: "could not be read"
4933
+ });
4934
+ }
4935
+ }
4936
+ const viewPath = manifest.workspace.view;
4937
+ if (viewPath !== void 0) {
4938
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
4939
+ const linkPath = join7(viewDir, canonicalName);
4940
+ let isLink = false;
4941
+ try {
4942
+ isLink = lstatSync(linkPath).isSymbolicLink();
4943
+ } catch {
4944
+ isLink = false;
4945
+ }
4946
+ if (isLink) {
4947
+ const owned = repoReal !== void 0 ? viewLinkPointsAt(viewDir, canonicalName, repoReal) : viewLinkPointsAtPath(viewDir, canonicalName, targetAbs);
4948
+ if (!owned)
4949
+ items.push({
4950
+ kind: "view-symlink",
4951
+ label: canonicalName,
4952
+ state: "foreign",
4953
+ note: "a view link that does not point at this repo"
4954
+ });
4955
+ else if (canonicalShared)
4956
+ items.push({
4957
+ kind: "view-symlink",
4958
+ label: canonicalName,
4959
+ state: "blocked",
4960
+ note: collisionNote
4961
+ });
4962
+ else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
4963
+ }
4964
+ }
4965
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4966
+ const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
4967
+ let canonicalIsLink = false;
4968
+ try {
4969
+ canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
4970
+ } catch {
4971
+ canonicalIsLink = false;
4972
+ }
4973
+ if (canonicalIsLink) {
4974
+ items.push({
4975
+ kind: "canonical-block",
4976
+ label: canonicalLabel,
4977
+ state: "foreign",
4978
+ note: "the canonical is a symlink (not generated)"
4979
+ });
4980
+ } else if (existsSync(canonicalFile)) {
4981
+ let content;
4982
+ try {
4983
+ content = readFileSync(canonicalFile, "utf8");
4984
+ } catch {
4985
+ items.push({
4986
+ kind: "canonical-block",
4987
+ label: canonicalLabel,
4988
+ state: "blocked",
4989
+ note: "could not be read"
4990
+ });
4991
+ }
4992
+ if (content !== void 0 && content !== "") {
4993
+ const section = parseMarkers(content);
4994
+ if (section.kind === "ok" && canonicalShared) {
4995
+ items.push({
4996
+ kind: "canonical-block",
4997
+ label: canonicalLabel,
4998
+ state: "blocked",
4999
+ note: collisionNote
5000
+ });
5001
+ } else if (section.kind === "ok" && repoReal === void 0) {
5002
+ items.push({
5003
+ kind: "canonical-block",
5004
+ label: canonicalLabel,
5005
+ state: "manual",
5006
+ note: "repo could not be resolved, so ownership cannot be verified (check manually)"
5007
+ });
5008
+ } else if (section.kind === "ok") {
5009
+ const emptyAfter = removeMarkerSection(content, canonicalLabel).trim().length === 0;
5010
+ items.push({
5011
+ kind: "canonical-block",
5012
+ label: canonicalLabel,
5013
+ state: "removable",
5014
+ ...emptyAfter ? {
5015
+ note: "the file becomes empty after the generated block is removed (a manual-delete candidate)"
5016
+ } : {}
5017
+ });
5018
+ } else if (section.kind === "no_markers") {
5019
+ items.push({
5020
+ kind: "canonical-block",
5021
+ label: canonicalLabel,
5022
+ state: "foreign",
5023
+ note: "no generated block (hand-authored only \u2014 left untouched)"
5024
+ });
5025
+ } else {
5026
+ items.push({
5027
+ kind: "canonical-block",
5028
+ label: canonicalLabel,
5029
+ state: "blocked",
5030
+ note: "malformed markers (fix manually)"
5031
+ });
5032
+ }
5033
+ }
5034
+ }
5035
+ }
5036
+ return {
5037
+ target,
5038
+ resolved: repoReal !== void 0,
5039
+ repoReal: repoReal ?? null,
5040
+ canonicalName,
5041
+ isAnchor,
5042
+ inRoster,
5043
+ items,
5044
+ removableCount: items.filter((i) => i.state === "removable").length
5045
+ };
5046
+ }
5047
+ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5048
+ const removed = [];
5049
+ const failed = [];
5050
+ const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
5051
+ let currentRepoReal = null;
5052
+ try {
5053
+ currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
5054
+ } catch {
5055
+ currentRepoReal = null;
5056
+ }
5057
+ const identityOk = plan.repoReal === null ? true : currentRepoReal === plan.repoReal;
5058
+ const removable = plan.items.filter((i) => i.state === "removable");
5059
+ if (!identityOk) {
5060
+ for (const item of removable) changed(item.label);
5061
+ return { removed, failed };
5062
+ }
5063
+ const anchorReal = realpathSync(repositoryRoot);
5064
+ const { canonicalName, repoReal } = plan;
5065
+ const expectedByName = new Map(
5066
+ repoReal !== null ? teardownExpectedTargets(repoReal, anchorReal, canonicalName).map((s) => [s.name, s.target]) : []
5067
+ );
5068
+ for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5069
+ const expected = expectedByName.get(item.label);
5070
+ if (repoReal === null || expected === void 0 || inspectSymlink(join7(repoReal, item.label), expected).state !== "correct") {
5071
+ changed(item.label);
5072
+ continue;
5073
+ }
5074
+ try {
5075
+ unlinkSync(join7(repoReal, item.label));
5076
+ removed.push(item.label);
5077
+ } catch (error) {
5078
+ failed.push({ label: item.label, message: failureReason(error) });
5079
+ }
5080
+ }
5081
+ const viewPath = manifest.workspace.view;
5082
+ for (const item of removable.filter((i) => i.kind === "view-symlink")) {
5083
+ if (viewPath === void 0) {
5084
+ changed(item.label);
5085
+ continue;
5086
+ }
5087
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
5088
+ const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve7(repositoryRoot, plan.target));
5089
+ if (!owned) {
5090
+ changed(item.label);
5091
+ continue;
5092
+ }
5093
+ try {
5094
+ unlinkSync(join7(viewDir, item.label));
5095
+ removed.push(`view/${item.label}`);
5096
+ } catch (error) {
5097
+ failed.push({ label: `view/${item.label}`, message: failureReason(error) });
5098
+ }
5099
+ }
5100
+ const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5101
+ for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5102
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5103
+ try {
5104
+ if (lstatSync(canonicalFile).isSymbolicLink()) {
5105
+ changed(item.label);
5106
+ continue;
5107
+ }
5108
+ } catch (error) {
5109
+ failed.push({ label: item.label, message: failureReason(error) });
5110
+ continue;
5111
+ }
5112
+ let fd;
5113
+ try {
5114
+ fd = openSync(canonicalFile, fsConstants.O_RDWR | NOFOLLOW);
5115
+ } catch (error) {
5116
+ changed(item.label);
5117
+ void error;
5118
+ continue;
5119
+ }
5120
+ try {
5121
+ const content = readFileSync(fd, "utf8");
5122
+ if (parseMarkers(content).kind !== "ok") {
5123
+ changed(item.label);
5124
+ continue;
5125
+ }
5126
+ const next = Buffer.from(removeMarkerSection(content, item.label), "utf8");
5127
+ ftruncateSync(fd, 0);
5128
+ writeSync(fd, next, 0, next.length, 0);
5129
+ removed.push(item.label);
5130
+ } catch (error) {
5131
+ failed.push({ label: item.label, message: failureReason(error) });
5132
+ } finally {
5133
+ closeSync(fd);
5134
+ }
5135
+ }
5136
+ return { removed, failed };
5137
+ }
5138
+ function renderProjectTeardown(result) {
5139
+ const lines = [];
5140
+ lines.push(`# teardown: ${result.target}`);
5141
+ lines.push("");
5142
+ if (result.isAnchor) {
5143
+ lines.push("The anchor (`.`) cannot be torn down (it is the project's home).");
5144
+ return lines.join("\n");
5145
+ }
5146
+ if (!result.resolved) {
5147
+ lines.push(
5148
+ "Note: the repo path could not be resolved (already deleted?). The in-repo wiring cannot be inspected, so only the view link / canonical are in scope."
5149
+ );
5150
+ lines.push("");
5151
+ }
5152
+ lines.push(
5153
+ result.inRoster ? "Status: still a roster member (the declaration remains in the manifest)." : "Status: not a roster member (already archived)."
5154
+ );
5155
+ lines.push("");
5156
+ const removable = result.items.filter((i) => i.state === "removable");
5157
+ const manual = result.items.filter((i) => i.state === "manual");
5158
+ const foreign = result.items.filter((i) => i.state === "foreign");
5159
+ const blocked = result.items.filter((i) => i.state === "blocked");
5160
+ if (removable.length === 0) {
5161
+ lines.push("No basou-generated artifact to remove.");
5162
+ } else {
5163
+ lines.push(`To remove (${removable.length}):`);
5164
+ for (const i of removable)
5165
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5166
+ }
5167
+ if (manual.length > 0) {
5168
+ lines.push("");
5169
+ lines.push("Check manually (not auto-removed):");
5170
+ for (const i of manual)
5171
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5172
+ }
5173
+ if (foreign.length > 0) {
5174
+ lines.push("");
5175
+ lines.push("Left untouched (not basou-generated):");
5176
+ for (const i of foreign)
5177
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5178
+ }
5179
+ if (blocked.length > 0) {
5180
+ lines.push("");
5181
+ lines.push("Could not be inspected:");
5182
+ for (const i of blocked)
5183
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5184
+ }
5185
+ lines.push("");
5186
+ if (result.applied) {
5187
+ lines.push(`--apply: removed ${result.removed.length}.`);
5188
+ for (const r of result.removed) lines.push(` \u2713 ${r}`);
5189
+ if (result.failed.length > 0) {
5190
+ lines.push("Failed:");
5191
+ for (const f of result.failed) lines.push(` \u2717 ${f.label} \u2014 ${f.message}`);
5192
+ }
5193
+ } else if (removable.length > 0) {
5194
+ lines.push(
5195
+ "This is a dry-run. Pass --apply to remove (this is a destructive, irreversible operation)."
5196
+ );
5197
+ }
5198
+ return lines.join("\n");
5199
+ }
5200
+ async function doRunProjectTeardown(target, options, ctx = {}) {
5201
+ const cwd = ctx.cwd ?? process.cwd();
5202
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project teardown");
5203
+ const paths = basouPaths10(repositoryRoot);
5204
+ const manifest = await readManifest6(paths);
5205
+ const plan = gatherRepoTeardown(repositoryRoot, manifest, target);
5206
+ const willApply = options.apply === true && !plan.isAnchor && plan.removableCount > 0;
5207
+ const { removed, failed } = willApply ? applyRepoTeardown(repositoryRoot, manifest, plan) : { removed: [], failed: [] };
5208
+ const result = { ...plan, applied: willApply, removed, failed };
5209
+ if (options.json === true) console.log(JSON.stringify(result));
5210
+ else console.log(renderProjectTeardown(result));
5211
+ return result;
5212
+ }
5213
+ async function runProjectTeardown(target, options, ctx = {}) {
5214
+ try {
5215
+ await doRunProjectTeardown(target, options, ctx);
5216
+ } catch (error) {
5217
+ renderCliError(error, { verbose: isVerbose(options) });
5218
+ process.exitCode = 1;
5219
+ }
5220
+ }
4752
5221
  function omitKey(obj, key) {
4753
5222
  const clone = { ...obj };
4754
5223
  delete clone[key];
@@ -4818,68 +5287,74 @@ async function doRunProjectArchive(target, options, ctx) {
4818
5287
  }
4819
5288
  function renderProjectArchive(result) {
4820
5289
  const lines = [];
4821
- lines.push("# repo \u306E archive(roster \u304B\u3089\u7573\u3080)");
5290
+ lines.push("# Archive a repo (fold it out of the roster)");
4822
5291
  lines.push("");
4823
5292
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
4824
5293
  if (!result.hasRoster) {
4825
- 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");
5294
+ lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to archive.");
4826
5295
  return lines.join("\n");
4827
5296
  }
4828
5297
  if (result.isAnchor) {
4829
5298
  lines.push(
4830
- `\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`
5299
+ `\u26A0\uFE0F \`${result.target}\` is the anchor (the project root). The anchor cannot be archived (it is the manifest's home).`
4831
5300
  );
4832
5301
  return lines.join("\n");
4833
5302
  }
4834
5303
  if (!result.found) {
4835
- lines.push(`\u2139\uFE0F \`${result.target}\` \u306F roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093(archive \u5BFE\u8C61\u306A\u3057)\u3002`);
5304
+ lines.push(`\u2139\uFE0F \`${result.target}\` is not declared in the roster (nothing to archive).`);
4836
5305
  return lines.join("\n");
4837
5306
  }
4838
5307
  if (result.applied) {
4839
- lines.push(`\u2705 \`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u3057\u307E\u3057\u305F\u3002`);
5308
+ lines.push(`\u2705 Removed \`${result.target}\` from the roster.`);
4840
5309
  } else {
4841
- lines.push(`\`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`);
5310
+ lines.push(`To remove \`${result.target}\` from the roster (dry-run; pass --apply to write):`);
4842
5311
  }
4843
5312
  if (result.sourceRootRemoval !== void 0) {
4844
5313
  lines.push(
4845
- `- 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`
5314
+ `- ${result.applied ? "Pruned" : "Will prune"} ${result.sourceRootRemoval} from source_roots (no longer captured by refresh).`
4846
5315
  );
4847
5316
  } else {
4848
- lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(prune \u4E0D\u8981)\u3002");
5317
+ lines.push("- No matching entry in source_roots (nothing to prune).");
4849
5318
  }
4850
5319
  if (result.reposEmptied) {
4851
5320
  lines.push(
4852
- "- \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"
5321
+ "- This was the last member \u2192 the roster empties and the `repos` declaration is removed (the project is folded up)."
4853
5322
  );
4854
5323
  } else if (result.becomesSolo) {
4855
5324
  lines.push(
4856
- "- \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"
5325
+ "- This leaves 1 repo (solo) \u2192 the workspace view is unnecessary (consider removing the view declaration / directory)."
4857
5326
  );
4858
5327
  }
4859
5328
  lines.push("");
4860
5329
  const t = result.teardown;
4861
5330
  const items = [];
4862
- if (t.viewLink) items.push("workspace view \u306E symlink \u30A8\u30F3\u30C8\u30EA");
4863
- if (t.instructionFiles.length > 0) items.push(`\u6307\u793A\u66F8(${t.instructionFiles.join(", ")})`);
5331
+ if (t.viewLink) items.push("the workspace view's symlink entry");
5332
+ if (t.instructionFiles.length > 0)
5333
+ items.push(`instruction files (${t.instructionFiles.join(", ")})`);
4864
5334
  if (t.gitignorePatterns.length > 0)
4865
- items.push(`.gitignore \u306E\u6307\u793A\u66F8\u30D1\u30BF\u30FC\u30F3(${t.gitignorePatterns.join(", ")})`);
4866
- if (t.canonical) items.push(`anchor \u306E canonical(agents/${basename4(result.target)}/AGENTS.md)`);
5335
+ items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
5336
+ if (t.canonical)
5337
+ items.push(`the anchor's canonical (agents/${basename4(result.target)}/AGENTS.md)`);
4867
5338
  if (!t.inspected) {
4868
- lines.push("## \u624B\u52D5 teardown(repo \u304C\u30C7\u30A3\u30B9\u30AF\u4E0A\u306B\u89E3\u6C7A\u3067\u304D\u306A\u3044\u305F\u3081\u672A\u691C\u67FB)");
4869
5339
  lines.push(
4870
- "- 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"
5340
+ "## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
5341
+ );
5342
+ lines.push(
5343
+ "- The repo may already be deleted. Check manually for a leftover view symlink / instruction symlinks / .gitignore / canonical."
4871
5344
  );
4872
5345
  lines.push("");
4873
5346
  } else if (items.length > 0) {
4874
- 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)");
5347
+ lines.push(
5348
+ "## Manual teardown (--apply does not touch these; remove the leftover wiring by hand)"
5349
+ );
4875
5350
  for (const i of items) lines.push(`- ${i}`);
4876
5351
  lines.push("");
4877
5352
  } else {
4878
- lines.push("repo \u5074\u306E wiring(view/\u6307\u793A\u66F8/.gitignore/canonical)\u306F\u6B8B\u3063\u3066\u3044\u307E\u305B\u3093\u3002");
5353
+ lines.push("No repo-side wiring (view / instruction files / .gitignore / canonical) remains.");
4879
5354
  lines.push("");
4880
5355
  }
4881
5356
  lines.push(
4882
- "\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"
5357
+ "Note: archive only changes the manifest (.basou, git-tracked, reversible). The repo, its captured history, and its on-disk wiring are not removed."
4883
5358
  );
4884
5359
  return lines.join("\n");
4885
5360
  }
@@ -4965,46 +5440,46 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
4965
5440
  }
4966
5441
  function renderProjectRename(result) {
4967
5442
  const lines = [];
4968
- lines.push("# repo \u306E rename(roster \u306E\u30D1\u30B9\u66F4\u65B0)");
5443
+ lines.push("# Rename a repo (update its roster path)");
4969
5444
  lines.push("");
4970
5445
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
4971
5446
  if (!result.hasRoster) {
4972
- 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");
5447
+ lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to rename.");
4973
5448
  return lines.join("\n");
4974
5449
  }
4975
5450
  if (result.noop) {
4976
- lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u3068 \`${result.newTarget}\` \u306F\u540C\u4E00\u3067\u3059(\u5909\u66F4\u306A\u3057)\u3002`);
5451
+ lines.push(`\u2139\uFE0F \`${result.oldTarget}\` and \`${result.newTarget}\` are identical (no change).`);
4977
5452
  return lines.join("\n");
4978
5453
  }
4979
5454
  if (result.isAnchor) {
4980
5455
  lines.push(
4981
- `\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`
5456
+ `\u26A0\uFE0F \`${result.oldTarget}\` is the anchor (the project root). The anchor cannot be renamed.`
4982
5457
  );
4983
5458
  return lines.join("\n");
4984
5459
  }
4985
5460
  if (!result.found) {
4986
- lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u306F roster \u306B\u5BA3\u8A00\u3055\u308C\u3066\u3044\u307E\u305B\u3093(rename \u5BFE\u8C61\u306A\u3057)\u3002`);
5461
+ lines.push(`\u2139\uFE0F \`${result.oldTarget}\` is not declared in the roster (nothing to rename).`);
4987
5462
  return lines.join("\n");
4988
5463
  }
4989
5464
  if (result.collision) {
4990
5465
  lines.push(
4991
- `\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`
5466
+ `\u26A0\uFE0F \`${result.newTarget}\` is already declared in the roster. Not renaming, to avoid a duplicate.`
4992
5467
  );
4993
5468
  return lines.join("\n");
4994
5469
  }
4995
5470
  if (result.applied) {
4996
- lines.push(`\u2705 \`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u3057\u307E\u3057\u305F\u3002`);
5471
+ lines.push(`\u2705 Renamed \`${result.oldTarget}\` to \`${result.newTarget}\`.`);
4997
5472
  } else {
4998
5473
  lines.push(
4999
- `\`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
5474
+ `To rename \`${result.oldTarget}\` to \`${result.newTarget}\` (dry-run; pass --apply to write):`
5000
5475
  );
5001
5476
  }
5002
5477
  if (result.sourceRootRenamed !== void 0) {
5003
5478
  lines.push(
5004
- `- source_roots \u306E ${result.sourceRootRenamed} \u3092 ${result.newTarget} \u306B\u66F4\u65B0${result.applied ? "\u3057\u307E\u3057\u305F" : "\u3057\u307E\u3059"}\u3002`
5479
+ `- ${result.applied ? "Updated" : "Will update"} ${result.sourceRootRenamed} to ${result.newTarget} in source_roots.`
5005
5480
  );
5006
5481
  } else {
5007
- lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(\u66F4\u65B0\u4E0D\u8981)\u3002");
5482
+ lines.push("- No matching entry in source_roots (nothing to update).");
5008
5483
  }
5009
5484
  lines.push("");
5010
5485
  if (result.basenameChanged) {
@@ -5013,28 +5488,447 @@ function renderProjectRename(result) {
5013
5488
  const items = [];
5014
5489
  if (result.wiring.canonicalDirOld)
5015
5490
  items.push(`anchor canonical: agents/${oldName}/ \u2192 agents/${newName}/`);
5016
- if (result.wiring.viewLinkOld) items.push(`workspace view \u306E symlink: ${oldName} \u2192 ${newName}`);
5491
+ if (result.wiring.viewLinkOld) items.push(`workspace view symlink: ${oldName} \u2192 ${newName}`);
5017
5492
  if (items.length > 0) {
5018
5493
  lines.push(
5019
- "## \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)"
5494
+ "## Manual rename (--apply does not touch these; the basename changed, so update them by hand)"
5020
5495
  );
5021
5496
  for (const i of items) lines.push(`- ${i}`);
5022
5497
  } else {
5023
5498
  lines.push(
5024
- `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`
5499
+ `The basename changes ${oldName} \u2192 ${newName}, but no anchor canonical / view symlink was found.`
5025
5500
  );
5026
5501
  }
5027
5502
  lines.push(
5028
- " \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"
5503
+ " After applying, regenerate the instruction symlinks and the view with `basou project symlinks` / `basou project workspace`."
5029
5504
  );
5030
5505
  } else {
5031
5506
  lines.push(
5032
- "\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"
5507
+ "Note: the basename is unchanged. If you moved the repo elsewhere, regenerate the relative targets with `basou project symlinks` / `basou project workspace`."
5033
5508
  );
5034
5509
  }
5035
5510
  lines.push("");
5036
5511
  lines.push(
5037
- "\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"
5512
+ "Note: rename only changes the manifest (.basou, git-tracked, reversible). It does not move the repo or update the on-disk wiring."
5513
+ );
5514
+ return lines.join("\n");
5515
+ }
5516
+ async function runProjectNew(repos, options, ctx = {}) {
5517
+ try {
5518
+ await doRunProjectNew(repos, options, ctx);
5519
+ } catch (error) {
5520
+ renderCliError(error, { verbose: isVerbose(options) });
5521
+ process.exitCode = 1;
5522
+ }
5523
+ }
5524
+ async function resolveRepositoryRootForNew(cwd) {
5525
+ try {
5526
+ return await resolveRepositoryRoot8(cwd);
5527
+ } catch (error) {
5528
+ if (error instanceof Error && error.message === "Not a git repository") {
5529
+ throw new Error(
5530
+ "Not a git repository. Run 'git init' first, then re-run 'basou project new'.",
5531
+ { cause: error }
5532
+ );
5533
+ }
5534
+ throw error;
5535
+ }
5536
+ }
5537
+ async function doRunProjectNew(repos, options, ctx) {
5538
+ const cwd = ctx.cwd ?? process.cwd();
5539
+ const repositoryRoot = await resolveRepositoryRootForNew(cwd);
5540
+ const workspaceName = basename4(repositoryRoot);
5541
+ const declared = repos.map((p) => {
5542
+ const abs = resolve7(cwd, p);
5543
+ let real;
5544
+ try {
5545
+ real = realpathSync(abs);
5546
+ } catch {
5547
+ real = abs;
5548
+ }
5549
+ const rel = relative2(repositoryRoot, real);
5550
+ return rel === "" ? "." : rel;
5551
+ });
5552
+ const invalidRepos = declared.filter(
5553
+ (rel) => classifySourceRoot(repositoryRoot, rel).kind !== "repo"
5554
+ );
5555
+ if (invalidRepos.length > 0) {
5556
+ throw new Error(
5557
+ `These declared repos are not git repositories (create them with 'git init' first): ${invalidRepos.join(", ")}`
5558
+ );
5559
+ }
5560
+ const rosterPaths = ["."];
5561
+ for (const rel of declared) {
5562
+ if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
5563
+ }
5564
+ const roster = rosterPaths.map((path) => ({ path }));
5565
+ const viewPath = options.view === false ? null : options.view ?? `../${workspaceName}-workspace`;
5566
+ const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
5567
+ const paths = basouPaths10(repositoryRoot);
5568
+ const existed = existsSync(paths.files.manifest);
5569
+ const manifest = createManifest2({ workspaceName, sourceRoots });
5570
+ manifest.repos = roster;
5571
+ if (viewPath !== null) manifest.workspace.view = viewPath;
5572
+ let applied = false;
5573
+ if (options.apply === true) {
5574
+ await ensureBasouDirectory2(repositoryRoot);
5575
+ await writeManifest2(paths, manifest, { force: options.force === true });
5576
+ applied = true;
5577
+ try {
5578
+ await appendBasouGitignore2(repositoryRoot, { localOnly: options.localOnly === true });
5579
+ } catch (error) {
5580
+ renderGitignoreWarningForNew(error, isVerbose(options));
5581
+ }
5582
+ }
5583
+ const result = {
5584
+ workspaceName,
5585
+ repos: roster,
5586
+ view: viewPath,
5587
+ sourceRoots,
5588
+ invalidRepos: [],
5589
+ existed,
5590
+ applied
5591
+ };
5592
+ if (options.json === true) {
5593
+ console.log(JSON.stringify(result));
5594
+ } else {
5595
+ console.log(renderProjectNew(result));
5596
+ }
5597
+ return result;
5598
+ }
5599
+ function renderGitignoreWarningForNew(error, verbose) {
5600
+ const baseMessage = error instanceof Error ? error.message : String(error);
5601
+ console.error(
5602
+ `Warning: Could not update .gitignore (${baseMessage}). Add Basou's default .gitignore block manually.`
5603
+ );
5604
+ if (verbose && error instanceof Error) {
5605
+ const label = extractCauseLabel(error);
5606
+ if (label !== void 0) console.error(`Caused by: ${label}`);
5607
+ }
5608
+ }
5609
+ function renderProjectNew(result) {
5610
+ const lines = [];
5611
+ lines.push("# Scaffold a new project (build from a declaration)");
5612
+ lines.push("");
5613
+ if (result.existed && !result.applied) {
5614
+ lines.push(
5615
+ "\u26A0\uFE0F This anchor already has a `.basou/manifest.yaml`. Overwriting it requires --force (nothing is written by default, so an existing declaration is not lost)."
5616
+ );
5617
+ lines.push("");
5618
+ }
5619
+ if (result.applied) {
5620
+ lines.push(`\u2705 Created \`.basou/\` for \`${result.workspaceName}\` and seeded the manifest:`);
5621
+ } else {
5622
+ lines.push(
5623
+ `Will create \`.basou/\` for \`${result.workspaceName}\` and seed the manifest (dry-run; pass --apply to write):`
5624
+ );
5625
+ }
5626
+ lines.push("");
5627
+ lines.push(`repos roster (${result.repos.length}):`);
5628
+ for (const r of result.repos) {
5629
+ lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
5630
+ }
5631
+ lines.push("");
5632
+ lines.push(
5633
+ result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
5634
+ );
5635
+ lines.push("");
5636
+ lines.push(`source_roots (${result.sourceRoots.length}):`);
5637
+ for (const s of result.sourceRoots) lines.push(`- ${s}`);
5638
+ lines.push("");
5639
+ lines.push(
5640
+ "Note: visibility / language are unset. Assign them to each repo manually. Basou does not create git repos; a declared repo must already be `git init`-ed."
5641
+ );
5642
+ if (result.applied) {
5643
+ lines.push(
5644
+ "Next: fill in visibility / language for each repo in `.basou/manifest.yaml`, then run `basou project derive --apply` to generate the wiring in one pass."
5645
+ );
5646
+ } else {
5647
+ lines.push(
5648
+ "After applying, fill in visibility / language in the manifest, then run `basou project derive`."
5649
+ );
5650
+ }
5651
+ return lines.join("\n");
5652
+ }
5653
+ async function runProjectDerive(options, ctx = {}) {
5654
+ try {
5655
+ await doRunProjectDerive(options, ctx);
5656
+ } catch (error) {
5657
+ renderCliError(error, { verbose: isVerbose(options) });
5658
+ process.exitCode = 1;
5659
+ }
5660
+ }
5661
+ async function doRunProjectDerive(options, ctx) {
5662
+ const cwd = ctx.cwd ?? process.cwd();
5663
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
5664
+ const paths = basouPaths10(repositoryRoot);
5665
+ const manifest = await readManifest6(paths);
5666
+ if (manifest.repos === void 0 || manifest.repos.length === 0) {
5667
+ console.log(
5668
+ "# Generate project wiring in one pass (declaration \u2192 wiring)\n\n\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one first with `basou project new` (new project) or `basou project adopt` (bootstrap from existing source_roots)."
5669
+ );
5670
+ return;
5671
+ }
5672
+ const apply = options.apply === true;
5673
+ const stepCtx = {
5674
+ cwd: repositoryRoot,
5675
+ ...ctx.now !== void 0 ? { now: ctx.now } : {}
5676
+ };
5677
+ const stepOpts = { apply };
5678
+ console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
5679
+ console.log("");
5680
+ console.log("## 1/5 sync source_roots (roster \u2192 capture config)");
5681
+ await doRunProjectSync(stepOpts, stepCtx);
5682
+ console.log("");
5683
+ console.log("## 2/5 generate instruction-file A preset (declaration \u2192 canonical)");
5684
+ await doRunProjectPreset(stepOpts, stepCtx);
5685
+ console.log("");
5686
+ console.log("## 3/5 generate instruction-file symlinks (each repo \u2192 canonical)");
5687
+ await doRunProjectSymlinks(stepOpts, stepCtx);
5688
+ console.log("");
5689
+ console.log("## 4/5 generate workspace view (aggregate the roster repos)");
5690
+ await doRunProjectWorkspace(stepOpts, stepCtx);
5691
+ console.log("");
5692
+ console.log("## 5/5 generate .gitignore (exclude public repos' instruction files)");
5693
+ await doRunProjectGitignore(stepOpts, stepCtx);
5694
+ console.log("");
5695
+ console.log(
5696
+ apply ? "\u2705 Ran every step (each is idempotent, so a partial apply recovers on re-run)." : "\u2139\uFE0F Dry-run preview. Pass --apply to write the changes, then re-run."
5697
+ );
5698
+ }
5699
+ async function runProjectRetrofit(repo, options, ctx = {}) {
5700
+ try {
5701
+ await doRunProjectRetrofit(repo, options, ctx);
5702
+ } catch (error) {
5703
+ renderCliError(error, { verbose: isVerbose(options) });
5704
+ process.exitCode = 1;
5705
+ }
5706
+ }
5707
+ function inspectAgentsState(filePath) {
5708
+ let st;
5709
+ try {
5710
+ st = lstatSync(filePath);
5711
+ } catch (error) {
5712
+ if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
5713
+ return "blocked";
5714
+ }
5715
+ if (st.isSymbolicLink()) return "symlink";
5716
+ return st.isFile() ? "regular-file" : "blocked";
5717
+ }
5718
+ function regularFileSpokes(repoReal) {
5719
+ const out = [];
5720
+ for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
5721
+ try {
5722
+ const st = lstatSync(join7(repoReal, spoke));
5723
+ if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
5724
+ } catch {
5725
+ }
5726
+ }
5727
+ return out;
5728
+ }
5729
+ function pathPresent(p) {
5730
+ try {
5731
+ lstatSync(p);
5732
+ return true;
5733
+ } catch {
5734
+ return false;
5735
+ }
5736
+ }
5737
+ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
5738
+ const declared = roster.some((entry) => {
5739
+ const entryAbs = resolve7(repositoryRoot, entry.path);
5740
+ if (argReal !== void 0) {
5741
+ try {
5742
+ if (realpathSync(entryAbs) === argReal) return true;
5743
+ } catch {
5744
+ }
5745
+ }
5746
+ return entryAbs === argAbs;
5747
+ });
5748
+ const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
5749
+ const path = displayRel === "" ? "." : displayRel;
5750
+ const canonicalName = basename4(argReal ?? argAbs);
5751
+ if (argReal === void 0) {
5752
+ return {
5753
+ path,
5754
+ declared,
5755
+ isAnchor: false,
5756
+ reachable: false,
5757
+ canonicalName,
5758
+ agentsState: "absent",
5759
+ canonicalExists: false,
5760
+ regularSpokes: []
5761
+ };
5762
+ }
5763
+ const isAnchor = argReal === anchorReal;
5764
+ const reachable = existsSync(join7(argReal, ".git"));
5765
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5766
+ return {
5767
+ path,
5768
+ declared,
5769
+ isAnchor,
5770
+ reachable,
5771
+ canonicalName,
5772
+ agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
5773
+ canonicalExists: pathPresent(canonicalFile),
5774
+ regularSpokes: regularFileSpokes(argReal)
5775
+ };
5776
+ }
5777
+ function relocateAgentsFile(repoReal, canonicalFile) {
5778
+ const agentsFile = join7(repoReal, CANONICAL_FILE);
5779
+ try {
5780
+ mkdirSync(dirname2(canonicalFile), { recursive: true });
5781
+ } catch (error) {
5782
+ return { ok: false, message: failureReason(error), partial: false };
5783
+ }
5784
+ try {
5785
+ copyFileSync(agentsFile, canonicalFile, fsConstants.COPYFILE_EXCL);
5786
+ } catch (error) {
5787
+ const message = hasErrorCode(error) && error.code === "EEXIST" ? "canonical-exists" : failureReason(error);
5788
+ return { ok: false, message, partial: false };
5789
+ }
5790
+ try {
5791
+ unlinkSync(agentsFile);
5792
+ symlinkSync(relative2(repoReal, canonicalFile), agentsFile);
5793
+ return { ok: true };
5794
+ } catch (error) {
5795
+ return { ok: false, message: failureReason(error), partial: true };
5796
+ }
5797
+ }
5798
+ async function doRunProjectRetrofit(repo, options, ctx) {
5799
+ const cwd = ctx.cwd ?? process.cwd();
5800
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
5801
+ const paths = basouPaths10(repositoryRoot);
5802
+ const manifest = await readManifest6(paths);
5803
+ const roster = manifest.repos ?? [];
5804
+ const anchorReal = realpathSync(repositoryRoot);
5805
+ const argAbs = resolve7(repositoryRoot, repo);
5806
+ let argReal;
5807
+ try {
5808
+ argReal = realpathSync(argAbs);
5809
+ } catch {
5810
+ argReal = void 0;
5811
+ }
5812
+ const facts = gatherRetrofit(repositoryRoot, anchorReal, roster, repo, argAbs, argReal);
5813
+ const plan = classifyRetrofit(facts);
5814
+ let applied = false;
5815
+ let failure;
5816
+ let partial = false;
5817
+ if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
5818
+ const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
5819
+ const res = relocateAgentsFile(argReal, canonicalFile);
5820
+ if (res.ok) {
5821
+ applied = true;
5822
+ } else {
5823
+ failure = res.message;
5824
+ partial = res.partial;
5825
+ }
5826
+ }
5827
+ const result = {
5828
+ ...plan,
5829
+ hasRoster: roster.length > 0,
5830
+ applied,
5831
+ ...failure !== void 0 ? { failure } : {},
5832
+ ...partial ? { partial } : {}
5833
+ };
5834
+ if (options.json === true) {
5835
+ console.log(JSON.stringify(result));
5836
+ } else {
5837
+ console.log(renderProjectRetrofit(result));
5838
+ }
5839
+ return result;
5840
+ }
5841
+ function appendSpokeChecklist(lines, spokes) {
5842
+ if (spokes.length === 0) return;
5843
+ lines.push(
5844
+ `## Spoke files to reconcile (${spokes.length}) \u2014 regular files that would block clean wiring`
5845
+ );
5846
+ for (const s of spokes) {
5847
+ lines.push(
5848
+ `- ${s}: a regular file. If it duplicates AGENTS.md, remove it; if it carries unique content, merge it into AGENTS.md. Then run \`basou project symlinks\`.`
5849
+ );
5850
+ }
5851
+ lines.push("");
5852
+ }
5853
+ function renderProjectRetrofit(result) {
5854
+ const lines = [];
5855
+ lines.push(
5856
+ "# Retrofit an existing AGENTS.md into the project (relocate to the anchor canonical)"
5857
+ );
5858
+ lines.push("");
5859
+ if (!result.hasRoster) {
5860
+ lines.push(
5861
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare the repo first with `basou project new` (or `basou project adopt`), then re-run."
5862
+ );
5863
+ return lines.join("\n");
5864
+ }
5865
+ const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
5866
+ if (result.action === "refuse") {
5867
+ if (result.reason === "not-declared") {
5868
+ lines.push(
5869
+ `\u2139\uFE0F \`${result.path}\` is not declared in the roster (manifest \`repos\`). Add it first with \`basou project new\` / \`basou project adopt\`, then re-run.`
5870
+ );
5871
+ } else if (result.reason === "anchor") {
5872
+ lines.push(
5873
+ `\u26A0\uFE0F \`${result.path}\` is the anchor (the project root). It owns its canonical directly \u2014 there is nothing to relocate.`
5874
+ );
5875
+ } else if (result.reason === "unreachable") {
5876
+ lines.push(
5877
+ `\u26A0\uFE0F \`${result.path}\` does not resolve to a git repository. There is nothing to retrofit.`
5878
+ );
5879
+ } else if (result.reason === "blocked") {
5880
+ lines.push(
5881
+ `\u26A0\uFE0F \`${result.path}/${CANONICAL_FILE}\` could not be inspected (a parent component is not a directory, a permission error, or the path is neither a regular file nor a symlink). Resolve it by hand, then re-run.`
5882
+ );
5883
+ } else if (result.reason === "canonical-exists") {
5884
+ lines.push(
5885
+ `\u26A0\uFE0F The destination canonical \`${canonical2}\` already exists. Not relocating, to avoid clobbering it. If the canonical is the source of truth, the repo's AGENTS.md is redundant (remove it, then run \`basou project symlinks\`); otherwise reconcile the two by hand.`
5886
+ );
5887
+ }
5888
+ return lines.join("\n");
5889
+ }
5890
+ if (result.action === "skip") {
5891
+ if (result.reason === "already-symlink") {
5892
+ lines.push(
5893
+ `\u2705 \`${result.path}/${CANONICAL_FILE}\` is already a symlink (already wired). Nothing to retrofit.`
5894
+ );
5895
+ } else {
5896
+ lines.push(
5897
+ `\u2139\uFE0F \`${result.path}\` has no regular-file \`${CANONICAL_FILE}\` to relocate. If it should have a canonical, run \`basou project derive\` to generate one from the manifest.`
5898
+ );
5899
+ }
5900
+ lines.push("");
5901
+ appendSpokeChecklist(lines, result.regularSpokes);
5902
+ return lines.join("\n").trimEnd();
5903
+ }
5904
+ if (result.failure !== void 0) {
5905
+ lines.push(
5906
+ `Could not relocate \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\`: ${result.failure}.`
5907
+ );
5908
+ if (result.partial === true) {
5909
+ lines.push(
5910
+ `The canonical \`${canonical2}\` was written, but \`${result.path}/${CANONICAL_FILE}\` may be absent. Run \`basou project symlinks --apply\` (or \`basou project derive --apply\`) to recreate the missing link, then verify.`
5911
+ );
5912
+ } else {
5913
+ lines.push("Nothing was changed. Resolve the cause and re-run.");
5914
+ }
5915
+ return lines.join("\n");
5916
+ }
5917
+ if (result.applied) {
5918
+ lines.push(
5919
+ `\u2705 Relocated \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\` and left a symlink in its place.`
5920
+ );
5921
+ } else {
5922
+ lines.push(
5923
+ `To relocate \`${result.path}/${CANONICAL_FILE}\` and replace it with a symlink (dry-run; pass --apply to write):`
5924
+ );
5925
+ lines.push(` move ${result.path}/${CANONICAL_FILE} -> ${canonical2}`);
5926
+ lines.push(` symlink ${result.path}/${CANONICAL_FILE} -> the canonical`);
5927
+ }
5928
+ lines.push("");
5929
+ appendSpokeChecklist(lines, result.regularSpokes);
5930
+ lines.push(
5931
+ result.applied ? "Next: run `basou project derive --apply` to add the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore." : "After applying, run `basou project derive --apply` to finish the wiring (preset block, CLAUDE.md / Copilot spokes, .gitignore)."
5038
5932
  );
5039
5933
  return lines.join("\n");
5040
5934
  }
@@ -5046,7 +5940,7 @@ import {
5046
5940
  PROTOCOL_START,
5047
5941
  parseMarkers as parseMarkers2,
5048
5942
  readMarkdownFile as readMarkdownFile5,
5049
- removeMarkerSection
5943
+ removeMarkerSection as removeMarkerSection2
5050
5944
  } from "@basou/core";
5051
5945
 
5052
5946
  // src/lib/durable-write.ts
@@ -5338,7 +6232,7 @@ async function doRunProtocolUnsync(options) {
5338
6232
  console.log("No target file; nothing to remove.");
5339
6233
  return;
5340
6234
  }
5341
- const newBody = removeMarkerSection(existing, "CLAUDE.md", PROTOCOL_MARKERS);
6235
+ const newBody = removeMarkerSection2(existing, "CLAUDE.md", PROTOCOL_MARKERS);
5342
6236
  if (newBody === existing) {
5343
6237
  console.log("No basou:protocols block found; nothing removed.");
5344
6238
  return;
@@ -5711,7 +6605,7 @@ import {
5711
6605
  basouPaths as basouPaths12,
5712
6606
  findErrorCode as findErrorCode10,
5713
6607
  renderReport,
5714
- resolveRepositoryRoot as resolveRepositoryRoot8,
6608
+ resolveRepositoryRoot as resolveRepositoryRoot9,
5715
6609
  writeMarkdownFile as writeMarkdownFile6
5716
6610
  } from "@basou/core";
5717
6611
  function registerReportCommand(program2) {
@@ -5760,7 +6654,7 @@ async function doRunReportGenerate(options, ctx) {
5760
6654
  }
5761
6655
  async function resolveRepositoryRootForReport(cwd) {
5762
6656
  try {
5763
- return await resolveRepositoryRoot8(cwd);
6657
+ return await resolveRepositoryRoot9(cwd);
5764
6658
  } catch (error) {
5765
6659
  if (error instanceof Error && error.message === "Not a git repository") {
5766
6660
  throw new Error(
@@ -5843,45 +6737,45 @@ async function doRunReviewGaps(options, ctx) {
5843
6737
  return summary;
5844
6738
  }
5845
6739
  function relAge(iso, now) {
5846
- if (iso === null) return "(\u4E0D\u660E)";
6740
+ if (iso === null) return "(unknown)";
5847
6741
  const ms = now.getTime() - Date.parse(iso);
5848
- if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
6742
+ if (!Number.isFinite(ms) || ms < 0) return "just now";
5849
6743
  const days = Math.floor(ms / 864e5);
5850
- if (days >= 1) return `${days}\u65E5\u524D`;
6744
+ if (days >= 1) return `${days}d ago`;
5851
6745
  const hours = Math.floor(ms / 36e5);
5852
- if (hours >= 1) return `${hours}\u6642\u9593\u524D`;
5853
- return `${Math.max(1, Math.floor(ms / 6e4))}\u5206\u524D`;
6746
+ if (hours >= 1) return `${hours}h ago`;
6747
+ return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
5854
6748
  }
5855
6749
  function unitLine(u, now) {
5856
6750
  const when = relAge(u.lastCommitAt, now);
5857
6751
  const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
5858
6752
  if (u.verdict === "near_unbound") {
5859
6753
  const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
5860
- 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}]`;
6754
+ return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]`;
5861
6755
  }
5862
- return `${head} \u2014 \u7D10\u3065\u304F\u30AF\u30ED\u30B9\u30E2\u30C7\u30EB\u30EC\u30D3\u30E5\u30FC\u306A\u3057`;
6756
+ return `${head} \u2014 no bound cross-model review`;
5863
6757
  }
5864
6758
  function candidateLine(u, now) {
5865
6759
  const when = relAge(u.lastCommitAt, now);
5866
6760
  const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
5867
- return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 \u30EC\u30D3\u30E5\u30FC\u5F62\u8DE1: ${cite}`;
6761
+ return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}`;
5868
6762
  }
5869
6763
  function renderReviewGaps(summary) {
5870
6764
  const now = new Date(summary.generatedAt);
5871
6765
  const lines = [];
5872
- const scope = summary.scope ? summary.scope.join(", ") : "\u5168\u30EA\u30DD\u30B8\u30C8\u30EA";
5873
- lines.push(`# \u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306E\u30AE\u30E3\u30C3\u30D7 (${scope})`);
6766
+ const scope = summary.scope ? summary.scope.join(", ") : "all repositories";
6767
+ lines.push(`# Review-trail gaps (${scope})`);
5874
6768
  lines.push("");
5875
6769
  if (summary.gaps.length === 0) {
5876
- 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");
6770
+ lines.push("\u2705 Within the captured range, no unit of work landed without a review trail.");
5877
6771
  } else {
5878
- lines.push(`\u26A0\uFE0F \u30EC\u30D3\u30E5\u30FC\u8A3C\u8DE1\u306A\u3057\u3067\u7740\u5730\u3057\u305F\u4F5C\u696D\u5358\u4F4D: ${summary.gaps.length}`);
6772
+ lines.push(`\u26A0\uFE0F Units of work that landed without a review trail: ${summary.gaps.length}`);
5879
6773
  for (const u of summary.gaps) lines.push(unitLine(u, now));
5880
6774
  }
5881
6775
  lines.push("");
5882
6776
  if (summary.candidates.length > 0) {
5883
6777
  lines.push(
5884
- `## \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`
6778
+ `## To confirm (${summary.candidates.length}) \u2014 a cross-model review trace exists; confirm it actually examined this change`
5885
6779
  );
5886
6780
  for (const u of summary.candidates) lines.push(candidateLine(u, now));
5887
6781
  lines.push("");
@@ -5889,19 +6783,19 @@ function renderReviewGaps(summary) {
5889
6783
  if (summary.unknowns.length > 0) {
5890
6784
  const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
5891
6785
  lines.push(
5892
- `## \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)`
6786
+ `## Undeterminable (${summary.unknowns.length} unit${summary.unknowns.length === 1 ? "" : "s"} / ${n} commit${n === 1 ? "" : "s"}) \u2014 repo or timestamp could not be derived from capture; verdict withheld (not a clear)`
5893
6787
  );
5894
6788
  lines.push("");
5895
6789
  }
5896
- lines.push("## \u30EA\u30DD\u30B8\u30C8\u30EA\u5225");
6790
+ lines.push("## By repository");
5897
6791
  for (const r of summary.repos) {
5898
6792
  lines.push(
5899
- `- ${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}` : ""})`
6793
+ `- ${r.repo}: ${r.units} unit${r.units === 1 ? "" : "s"} (no trail ${r.omissionUnits} / nearby only ${r.nearUnboundUnits} / to confirm ${r.candidateUnits}${r.unknownUnits > 0 ? ` / unknown ${r.unknownUnits}` : ""})`
5900
6794
  );
5901
6795
  }
5902
6796
  lines.push("");
5903
6797
  lines.push(
5904
- `\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`
6798
+ `Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" : relAge(summary.newestCommitAt, now)}). It never auto-judges that a review "happened", and temporal proximity alone is not a pass. It does not enforce.`
5905
6799
  );
5906
6800
  return lines.join("\n");
5907
6801
  }
@@ -5925,7 +6819,7 @@ import {
5925
6819
  readManifest as readManifest7,
5926
6820
  readYamlFile as readYamlFile6,
5927
6821
  resolveClaudeCodeCommand,
5928
- resolveRepositoryRoot as resolveRepositoryRoot9,
6822
+ resolveRepositoryRoot as resolveRepositoryRoot10,
5929
6823
  SessionSchema as SessionSchema2,
5930
6824
  sanitizeRelatedFiles,
5931
6825
  sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
@@ -6298,7 +7192,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
6298
7192
  }
6299
7193
  async function resolveRepositoryRootForRun(cwd) {
6300
7194
  try {
6301
- return await resolveRepositoryRoot9(cwd);
7195
+ return await resolveRepositoryRoot10(cwd);
6302
7196
  } catch (error) {
6303
7197
  if (error instanceof Error && error.message === "Not a git repository") {
6304
7198
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -6980,7 +7874,7 @@ import {
6980
7874
  basouPaths as basouPaths16,
6981
7875
  computeWorkStats,
6982
7876
  findErrorCode as findErrorCode12,
6983
- resolveRepositoryRoot as resolveRepositoryRoot10
7877
+ resolveRepositoryRoot as resolveRepositoryRoot11
6984
7878
  } from "@basou/core";
6985
7879
  function registerStatsCommand(program2) {
6986
7880
  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) => {
@@ -7083,7 +7977,7 @@ function formatInt(n) {
7083
7977
  }
7084
7978
  async function resolveRepositoryRootForStats(cwd) {
7085
7979
  try {
7086
- return await resolveRepositoryRoot10(cwd);
7980
+ return await resolveRepositoryRoot11(cwd);
7087
7981
  } catch (error) {
7088
7982
  if (error instanceof Error && error.message === "Not a git repository") {
7089
7983
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -7111,7 +8005,7 @@ import {
7111
8005
  buildStatusSnapshot,
7112
8006
  findErrorCode as findErrorCode13,
7113
8007
  readManifest as readManifest9,
7114
- resolveRepositoryRoot as resolveRepositoryRoot11,
8008
+ resolveRepositoryRoot as resolveRepositoryRoot12,
7115
8009
  writeStatus
7116
8010
  } from "@basou/core";
7117
8011
  function registerStatusCommand(program2) {
@@ -7167,7 +8061,7 @@ function renderTextStatus(s) {
7167
8061
  }
7168
8062
  async function resolveRepositoryRootForStatus(cwd) {
7169
8063
  try {
7170
- return await resolveRepositoryRoot11(cwd);
8064
+ return await resolveRepositoryRoot12(cwd);
7171
8065
  } catch (error) {
7172
8066
  if (error instanceof Error && error.message === "Not a git repository") {
7173
8067
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -8304,7 +9198,7 @@ import {
8304
9198
  basouPaths as basouPaths19,
8305
9199
  enumerateSessionDirs as enumerateSessionDirs3,
8306
9200
  findErrorCode as findErrorCode15,
8307
- resolveRepositoryRoot as resolveRepositoryRoot12,
9201
+ resolveRepositoryRoot as resolveRepositoryRoot13,
8308
9202
  resolveSessionId as resolveSessionId5,
8309
9203
  verifyEventsChain
8310
9204
  } from "@basou/core";
@@ -8375,7 +9269,7 @@ function renderVerdict(row) {
8375
9269
  }
8376
9270
  async function resolveRepositoryRootForVerify(cwd) {
8377
9271
  try {
8378
- return await resolveRepositoryRoot12(cwd);
9272
+ return await resolveRepositoryRoot13(cwd);
8379
9273
  } catch (error) {
8380
9274
  if (error instanceof Error && error.message === "Not a git repository") {
8381
9275
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -8405,7 +9299,7 @@ import {
8405
9299
  basouPaths as basouPaths20,
8406
9300
  findErrorCode as findErrorCode17,
8407
9301
  readManifest as readManifest13,
8408
- resolveRepositoryRoot as resolveRepositoryRoot13
9302
+ resolveRepositoryRoot as resolveRepositoryRoot14
8409
9303
  } from "@basou/core";
8410
9304
  import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
8411
9305
 
@@ -9821,7 +10715,7 @@ function waitForShutdown(signal) {
9821
10715
  }
9822
10716
  async function resolveRepositoryRootForView(cwd) {
9823
10717
  try {
9824
- return await resolveRepositoryRoot13(cwd);
10718
+ return await resolveRepositoryRoot14(cwd);
9825
10719
  } catch (error) {
9826
10720
  if (error instanceof Error && error.message === "Not a git repository") {
9827
10721
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {