@basou/cli 0.25.0 → 0.26.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,13 @@ async function assertWorkspaceInitialized7(basouRoot) {
3364
3364
 
3365
3365
  // src/commands/project.ts
3366
3366
  import {
3367
+ closeSync,
3367
3368
  existsSync,
3369
+ constants as fsConstants,
3370
+ ftruncateSync,
3368
3371
  lstatSync,
3369
3372
  mkdirSync,
3373
+ openSync,
3370
3374
  readdirSync,
3371
3375
  readFileSync,
3372
3376
  readlinkSync,
@@ -3374,11 +3378,15 @@ import {
3374
3378
  statSync,
3375
3379
  symlinkSync,
3376
3380
  unlinkSync,
3377
- writeFileSync
3381
+ writeFileSync,
3382
+ writeSync
3378
3383
  } from "fs";
3379
3384
  import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
3380
3385
  import {
3386
+ appendBasouGitignore as appendBasouGitignore2,
3381
3387
  basouPaths as basouPaths10,
3388
+ createManifest as createManifest2,
3389
+ ensureBasouDirectory as ensureBasouDirectory2,
3382
3390
  GENERATED_END,
3383
3391
  GENERATED_START,
3384
3392
  isGitNotFound,
@@ -3392,7 +3400,9 @@ import {
3392
3400
  readManifest as readManifest6,
3393
3401
  readMarkdownFile as readMarkdownFile4,
3394
3402
  reconcileSourceRoots,
3403
+ removeMarkerSection,
3395
3404
  renderWithMarkers as renderWithMarkers4,
3405
+ resolveRepositoryRoot as resolveRepositoryRoot8,
3396
3406
  safeSimpleGit,
3397
3407
  summarizePresetPlan,
3398
3408
  summarizeRosterDrift,
@@ -3474,6 +3484,27 @@ function registerProjectCommand(program2) {
3474
3484
  ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (oldPath, newPath, opts) => {
3475
3485
  await runProjectRename(oldPath, newPath, opts);
3476
3486
  });
3487
+ project.command("teardown").argument("<repo>", "The repo path whose basou-generated wiring to tear down (e.g. ../takuhon)").description(
3488
+ "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"
3489
+ ).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) => {
3490
+ await runProjectTeardown(repo, opts);
3491
+ });
3492
+ project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
3493
+ "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"
3494
+ ).option("--apply", "Create `.basou/` and write the seeded manifest (default: dry-run preview)").option(
3495
+ "--view <path>",
3496
+ "Override the workspace view path (default: a <name>-workspace sibling)"
3497
+ ).option("--no-view", "Solo project: declare no workspace view").option(
3498
+ "--local-only",
3499
+ "Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
3500
+ ).option("-f, --force", "Overwrite an existing manifest").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repos, opts) => {
3501
+ await runProjectNew(repos, opts);
3502
+ });
3503
+ project.command("derive").description(
3504
+ "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"
3505
+ ).option("--apply", "Run every step in apply mode (default: dry-run preview)").option("-v, --verbose", "Show error causes").action(async (opts) => {
3506
+ await runProjectDerive(opts);
3507
+ });
3477
3508
  }
3478
3509
  async function runProjectCheck(options, ctx = {}) {
3479
3510
  try {
@@ -3489,7 +3520,7 @@ function effectiveSourceRoots(manifest) {
3489
3520
  function preservedUnknownLines(fields) {
3490
3521
  if (fields.length === 0) return [];
3491
3522
  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(", ")}`,
3523
+ `\u2139\uFE0F Preserving ${fields.length} unrecognized top-level manifest field${fields.length === 1 ? "" : "s"} (kept on write, never dropped): ${fields.join(", ")}`,
3493
3524
  ""
3494
3525
  ];
3495
3526
  }
@@ -3511,39 +3542,41 @@ async function doRunProjectCheck(options, ctx) {
3511
3542
  }
3512
3543
  function renderProjectCheck(summary) {
3513
3544
  const lines = [];
3514
- lines.push("# \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210\u30C1\u30A7\u30C3\u30AF(\u5BA3\u8A00 vs \u6355\u6349)");
3545
+ lines.push("# Project composition check (declared vs captured)");
3515
3546
  lines.push("");
3516
3547
  if (summary.declaredCount === 0) {
3517
3548
  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"
3549
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Running on `source_roots` alone, so there is nothing to compare the declaration against."
3519
3550
  );
3520
3551
  if (summary.extra.length > 0) {
3521
3552
  lines.push("");
3522
- lines.push(`\u6355\u6349\u4E2D\u306E source_roots (${summary.extra.length}):`);
3553
+ lines.push(`Captured source_roots (${summary.extra.length}):`);
3523
3554
  for (const p of summary.extra) lines.push(`- ${p}`);
3524
3555
  }
3525
3556
  return lines.join("\n");
3526
3557
  }
3527
3558
  if (summary.gaps.length === 0) {
3528
3559
  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`
3560
+ `\u2705 All ${summary.declaredCount} declared repo${summary.declaredCount === 1 ? " is" : "s are"} covered by the capture config (source_roots).`
3530
3561
  );
3531
3562
  } 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)`);
3563
+ lines.push(
3564
+ `\u26A0\uFE0F Declared but not captured: ${summary.gaps.length} repo${summary.gaps.length === 1 ? "" : "s"}`
3565
+ );
3533
3566
  for (const g of summary.gaps) {
3534
- lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 source_roots \u306B\u672A\u767B\u9332`);
3567
+ lines.push(`- ${g.path}${g.visibility ? ` [${g.visibility}]` : ""} \u2014 not in source_roots`);
3535
3568
  }
3536
3569
  }
3537
3570
  lines.push("");
3538
3571
  if (summary.extra.length > 0) {
3539
3572
  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`
3573
+ `## Captured but undeclared (${summary.extra.length}) \u2014 the workspace view, or a missing declaration`
3541
3574
  );
3542
3575
  for (const p of summary.extra) lines.push(`- ${p}`);
3543
3576
  lines.push("");
3544
3577
  }
3545
3578
  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"
3579
+ "Note: read-only advisory. It only shows the difference between the declaration (repos) and the capture config (source_roots); it does not enforce."
3547
3580
  );
3548
3581
  return lines.join("\n");
3549
3582
  }
@@ -3593,29 +3626,33 @@ async function doRunProjectSync(options, ctx) {
3593
3626
  }
3594
3627
  function renderProjectSync(result) {
3595
3628
  const lines = [];
3596
- lines.push("# source_roots \u540C\u671F(\u5BA3\u8A00\u30ED\u30FC\u30B9\u30BF\u30FC \u2192 \u6355\u6349\u8A2D\u5B9A)");
3629
+ lines.push("# source_roots sync (declared roster \u2192 capture config)");
3597
3630
  lines.push("");
3598
3631
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
3599
3632
  if (!result.hasRoster) {
3600
3633
  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"
3634
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). There is no declaration to sync from, so nothing changes."
3602
3635
  );
3603
3636
  return lines.join("\n");
3604
3637
  }
3605
3638
  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");
3639
+ lines.push("\u2705 source_roots already covers the entire declared roster (nothing to sync).");
3607
3640
  return lines.join("\n");
3608
3641
  }
3609
3642
  if (result.applied) {
3610
- lines.push(`\u2705 source_roots \u306B ${result.added.length} \u4EF6\u8FFD\u52A0\u3057\u307E\u3057\u305F:`);
3643
+ lines.push(
3644
+ `\u2705 Added ${result.added.length} entr${result.added.length === 1 ? "y" : "ies"} to source_roots:`
3645
+ );
3611
3646
  for (const p of result.added) lines.push(`- ${p}`);
3612
3647
  } else {
3613
3648
  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):`
3649
+ `${result.added.length} repo${result.added.length === 1 ? " is" : "s are"} not in source_roots. To add (dry-run; pass --apply to write):`
3615
3650
  );
3616
3651
  for (const p of result.added) lines.push(`- ${p}`);
3617
3652
  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");
3653
+ lines.push(
3654
+ "Note: existing source_roots are kept; only the missing entries are appended (nothing is removed)."
3655
+ );
3619
3656
  }
3620
3657
  return lines.join("\n");
3621
3658
  }
@@ -3675,37 +3712,41 @@ async function doRunProjectAdopt(options, ctx) {
3675
3712
  }
3676
3713
  function renderProjectAdopt(result) {
3677
3714
  const lines = [];
3678
- lines.push("# repo \u30ED\u30FC\u30B9\u30BF\u30FC\u306E bootstrap(source_roots \u2192 repos)");
3715
+ lines.push("# Bootstrap repo roster (source_roots \u2192 repos)");
3679
3716
  lines.push("");
3680
3717
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
3681
3718
  if (result.alreadyDeclared) {
3682
3719
  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"
3720
+ "\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
3721
  );
3685
3722
  return lines.join("\n");
3686
3723
  }
3687
3724
  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");
3725
+ lines.push("\u2139\uFE0F No git repo found in source_roots (nothing to bootstrap).");
3689
3726
  } 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:`);
3727
+ lines.push(
3728
+ `\u2705 Wrote ${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to the repos roster:`
3729
+ );
3691
3730
  for (const r of result.repos) lines.push(`- ${r.path}`);
3692
3731
  lines.push("");
3693
3732
  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"
3733
+ "Note: visibility is unset. Assign public / private / future-public to each repo manually."
3695
3734
  );
3696
3735
  } else {
3697
3736
  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):`
3737
+ `${result.repos.length} repo${result.repos.length === 1 ? "" : "s"} to declare in the repos roster (dry-run; pass --apply to write):`
3699
3738
  );
3700
3739
  for (const r of result.repos) lines.push(`- ${r.path}`);
3701
3740
  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");
3741
+ lines.push("Note: visibility is proposed unset; assign it manually after applying.");
3703
3742
  }
3704
3743
  if (result.excluded.length > 0) {
3705
3744
  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`);
3745
+ lines.push(
3746
+ `## Excluded (${result.excluded.length}) \u2014 not a git repo, so not included in repos`
3747
+ );
3707
3748
  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)";
3749
+ const reason = e.kind === "non-repo" ? "not a repo (workspace view / tmp, etc.)" : "unresolvable (path does not exist)";
3709
3750
  lines.push(`- ${e.path} \u2014 ${reason}`);
3710
3751
  }
3711
3752
  }
@@ -3773,50 +3814,56 @@ async function doRunProjectWiring(options, ctx) {
3773
3814
  }
3774
3815
  function renderProjectWiring(result) {
3775
3816
  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)");
3817
+ lines.push(
3818
+ "# Instruction-file wiring check (declared roster \xD7 instruction-file presence / git tracking)"
3819
+ );
3777
3820
  lines.push("");
3778
3821
  if (!result.hasRoster) {
3779
3822
  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"
3823
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
3781
3824
  );
3782
3825
  return lines.join("\n");
3783
3826
  }
3784
3827
  if (result.risks.length > 0) {
3785
3828
  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)`
3829
+ `\u26A0\uFE0F Instruction files tracked by git in public-facing repos: ${result.risks.length} (canonical leak risk)`
3787
3830
  );
3788
3831
  for (const r of result.risks) {
3789
3832
  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)`
3833
+ `- ${r.repo} [${r.visibility}] \u2014 ${r.file} is tracked (it should be a gitignored symlink)`
3791
3834
  );
3792
3835
  }
3793
3836
  } 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");
3837
+ lines.push(
3838
+ "\u2705 No instruction file is tracked by git in a public-facing repo (no privacy risk)."
3839
+ );
3795
3840
  } else {
3796
3841
  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"
3842
+ "\u2139\uFE0F No confirmed privacy risk, but some repos are unjudgeable / unreachable (see below)."
3798
3843
  );
3799
3844
  }
3800
3845
  lines.push("");
3801
3846
  if (result.unknown.length > 0) {
3802
3847
  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`
3848
+ `## Visibility unset (${result.unknown.length}) \u2014 privacy cannot be judged. Assign visibility in the manifest repos`
3804
3849
  );
3805
3850
  for (const p of result.unknown) lines.push(`- ${p}`);
3806
3851
  lines.push("");
3807
3852
  }
3808
3853
  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`);
3854
+ lines.push(
3855
+ `## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
3856
+ );
3810
3857
  for (const i of result.incomplete) lines.push(`- ${i.repo} \u2014 ${i.missing.join(", ")}`);
3811
3858
  lines.push("");
3812
3859
  }
3813
3860
  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`);
3861
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
3815
3862
  for (const p of result.unreachable) lines.push(`- ${p}`);
3816
3863
  lines.push("");
3817
3864
  }
3818
3865
  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"
3866
+ "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
3867
  );
3821
3868
  return lines.join("\n");
3822
3869
  }
@@ -3895,45 +3942,47 @@ async function doRunProjectGitignore(options, ctx) {
3895
3942
  }
3896
3943
  function renderProjectGitignore(result) {
3897
3944
  const lines = [];
3898
- lines.push("# .gitignore \u751F\u6210(\u516C\u958B\u7CFB repo \u306E\u6307\u793A\u66F8\u3092\u9664\u5916)");
3945
+ lines.push("# .gitignore generation (exclude instruction files in public-facing repos)");
3899
3946
  lines.push("");
3900
3947
  if (!result.hasRoster) {
3901
3948
  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"
3949
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
3903
3950
  );
3904
3951
  return lines.join("\n");
3905
3952
  }
3906
3953
  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)";
3954
+ const verb = result.applied ? "Added to" : "To add to (dry-run; pass --apply to write)";
3908
3955
  lines.push(
3909
- `${result.applied ? "\u2705 " : ""}${result.plans.length} repo \u306E .gitignore \u306B${verb}:`
3956
+ `${result.applied ? "\u2705 " : ""}${verb} the .gitignore of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"}:`
3910
3957
  );
3911
3958
  for (const p of result.plans) lines.push(`- ${p.path} \u2014 ${p.toAdd.join(", ")}`);
3912
3959
  } 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");
3960
+ lines.push(
3961
+ "\u2705 Public-facing repos already exclude every instruction file in .gitignore (nothing to add)."
3962
+ );
3914
3963
  } else {
3915
3964
  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"
3965
+ "\u2139\uFE0F No public-facing repo needs an addition, but some repos are unjudgeable / unreachable (see below)."
3917
3966
  );
3918
3967
  }
3919
3968
  lines.push("");
3920
3969
  if (result.unknown.length > 0) {
3921
3970
  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`
3971
+ `## Visibility unset (${result.unknown.length}) \u2014 skipped. Assign visibility in the manifest repos`
3923
3972
  );
3924
3973
  for (const p of result.unknown) lines.push(`- ${p}`);
3925
3974
  lines.push("");
3926
3975
  }
3927
3976
  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`);
3977
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
3929
3978
  for (const p of result.unreachable) lines.push(`- ${p}`);
3930
3979
  lines.push("");
3931
3980
  }
3932
3981
  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"
3982
+ "Note: existing .gitignore lines are kept; only the missing patterns are appended (nothing is removed). private / visibility-unset repos are skipped."
3934
3983
  );
3935
3984
  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"
3985
+ "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
3986
  );
3938
3987
  return lines.join("\n");
3939
3988
  }
@@ -4061,11 +4110,11 @@ async function doRunProjectSymlinks(options, ctx) {
4061
4110
  }
4062
4111
  function renderProjectSymlinks(result) {
4063
4112
  const lines = [];
4064
- lines.push("# \u6307\u793A\u66F8 symlink \u751F\u6210(\u5404 repo \u2192 anchor \u306E canonical)");
4113
+ lines.push("# Instruction-file symlink generation (each repo \u2192 the anchor's canonical)");
4065
4114
  lines.push("");
4066
4115
  if (!result.hasRoster) {
4067
4116
  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"
4117
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
4069
4118
  );
4070
4119
  return lines.join("\n");
4071
4120
  }
@@ -4073,14 +4122,14 @@ function renderProjectSymlinks(result) {
4073
4122
  const attempted = result.applied || result.failures.length > 0;
4074
4123
  if (!attempted) {
4075
4124
  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):`
4125
+ `Instruction-file symlinks to create in ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
4077
4126
  );
4078
4127
  for (const p of result.plans) {
4079
4128
  lines.push(`- ${p.path}`);
4080
4129
  for (const c of p.toCreate) lines.push(` ${c.name} -> ${c.target}`);
4081
4130
  }
4082
4131
  } 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):";
4132
+ 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
4133
  lines.push(header);
4085
4134
  for (const p of result.plans) {
4086
4135
  const failedFiles = new Set(
@@ -4093,31 +4142,35 @@ function renderProjectSymlinks(result) {
4093
4142
  }
4094
4143
  }
4095
4144
  } 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");
4145
+ lines.push(
4146
+ "\u2705 Every declared repo's instruction-file symlinks are correctly wired (nothing to generate)."
4147
+ );
4097
4148
  } else {
4098
4149
  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"
4150
+ "\u2139\uFE0F No symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos (see below)."
4100
4151
  );
4101
4152
  }
4102
4153
  lines.push("");
4103
4154
  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`);
4155
+ lines.push(
4156
+ `## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
4157
+ );
4105
4158
  for (const f of result.failures) lines.push(`- ${f.repo} \u2014 ${f.file}: ${f.message}`);
4106
4159
  lines.push("");
4107
4160
  }
4108
4161
  if (result.conflicts.length > 0) {
4109
4162
  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`
4163
+ `## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
4111
4164
  );
4112
4165
  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)";
4166
+ 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
4167
  lines.push(`- ${c.repo} \u2014 ${c.file}: ${detail}`);
4115
4168
  }
4116
4169
  lines.push("");
4117
4170
  }
4118
4171
  if (result.collisions.length > 0) {
4119
4172
  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)`
4173
+ `## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-wired)`
4121
4174
  );
4122
4175
  for (const c of result.collisions) {
4123
4176
  lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
@@ -4126,18 +4179,18 @@ function renderProjectSymlinks(result) {
4126
4179
  }
4127
4180
  if (result.missingCanonical.length > 0) {
4128
4181
  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`
4182
+ `## Canonical missing (${result.missingCanonical.length}) \u2014 the anchor has no agents/<repo>/AGENTS.md, so nothing can be generated`
4130
4183
  );
4131
4184
  for (const p of result.missingCanonical) lines.push(`- ${p}`);
4132
4185
  lines.push("");
4133
4186
  }
4134
4187
  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`);
4188
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4136
4189
  for (const p of result.unreachable) lines.push(`- ${p}`);
4137
4190
  lines.push("");
4138
4191
  }
4139
4192
  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"
4193
+ "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
4194
  );
4142
4195
  return lines.join("\n");
4143
4196
  }
@@ -4239,7 +4292,7 @@ function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4239
4292
  names = readdirSync(viewDir);
4240
4293
  } catch (error) {
4241
4294
  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)", {
4295
+ throw new Error("Cannot scan the workspace view (check the path / its type)", {
4243
4296
  cause: error
4244
4297
  });
4245
4298
  }
@@ -4261,7 +4314,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4261
4314
  if (c === null || c.kind !== "repo") {
4262
4315
  failed.push({
4263
4316
  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)"
4317
+ message: "the target changed since the scan (no longer a basou-generated stray repo link; re-run)"
4265
4318
  });
4266
4319
  continue;
4267
4320
  }
@@ -4350,11 +4403,11 @@ async function doRunProjectWorkspace(options, ctx) {
4350
4403
  }
4351
4404
  function renderProjectWorkspace(result) {
4352
4405
  const lines = [];
4353
- lines.push("# workspace view \u751F\u6210(roster repo \u3092\u96C6\u7D04)");
4406
+ lines.push("# workspace view generation (aggregate the roster repos)");
4354
4407
  lines.push("");
4355
4408
  if (!result.hasView) {
4356
4409
  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"
4410
+ "\u2139\uFE0F No view declared (manifest `workspace.view`). Declare the aggregation directory, then re-run."
4358
4411
  );
4359
4412
  return lines.join("\n");
4360
4413
  }
@@ -4362,12 +4415,12 @@ function renderProjectWorkspace(result) {
4362
4415
  const attempted = result.applied || result.failures.length > 0;
4363
4416
  if (!attempted) {
4364
4417
  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):`
4418
+ `Repo symlinks to create in the view: ${result.toCreate.length} (dry-run; pass --apply to write):`
4366
4419
  );
4367
4420
  for (const c of result.toCreate) lines.push(` ${c.name} -> ${c.target}`);
4368
4421
  } else {
4369
4422
  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):";
4423
+ 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
4424
  lines.push(header);
4372
4425
  for (const c of result.toCreate) {
4373
4426
  if (failed.has(c.name)) continue;
@@ -4376,16 +4429,18 @@ function renderProjectWorkspace(result) {
4376
4429
  }
4377
4430
  } else if (result.ok) {
4378
4431
  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`
4432
+ `\u2705 The view aggregates the entire declared roster (${result.correctCount} links; nothing to generate).`
4380
4433
  );
4381
4434
  } else {
4382
4435
  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"
4436
+ "\u2139\uFE0F No symlink needs creating, but there are items needing attention (stray / conflict / collision / unreachable repo, see below)."
4384
4437
  );
4385
4438
  }
4386
4439
  lines.push("");
4387
4440
  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`);
4441
+ lines.push(
4442
+ `## Creation failed (${result.failures.length}) \u2014 some symlinks could not be created`
4443
+ );
4389
4444
  for (const f of result.failures) lines.push(`- ${f.name}: ${f.message}`);
4390
4445
  lines.push("");
4391
4446
  }
@@ -4393,17 +4448,17 @@ function renderProjectWorkspace(result) {
4393
4448
  const attempted = result.pruned || result.pruneFailures.length > 0;
4394
4449
  if (result.pruneWithheld) {
4395
4450
  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):`
4451
+ `${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
4452
  );
4398
4453
  for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
4399
4454
  } else if (!attempted) {
4400
4455
  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):`
4456
+ `Stray repo symlinks to prune: ${result.toPrune.length} (dry-run; pass --prune to remove):`
4402
4457
  );
4403
4458
  for (const p of result.toPrune) lines.push(` ${p.name} -> ${p.target}`);
4404
4459
  } else {
4405
4460
  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):";
4461
+ 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
4462
  lines.push(header);
4408
4463
  for (const p of result.toPrune) {
4409
4464
  if (failed.has(p.name)) continue;
@@ -4414,47 +4469,47 @@ function renderProjectWorkspace(result) {
4414
4469
  }
4415
4470
  if (result.pruneFailures.length > 0) {
4416
4471
  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`
4472
+ `## Pruning failed (${result.pruneFailures.length}) \u2014 some stray symlinks could not be pruned`
4418
4473
  );
4419
4474
  for (const f of result.pruneFailures) lines.push(`- ${f.name}: ${f.message}`);
4420
4475
  lines.push("");
4421
4476
  }
4422
4477
  if (result.conflicts.length > 0) {
4423
4478
  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`
4479
+ `## Conflicts (${result.conflicts.length}) \u2014 existing entries are not overwritten. Check them manually`
4425
4480
  );
4426
4481
  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)";
4482
+ 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
4483
  lines.push(`- ${c.name}: ${detail}`);
4429
4484
  }
4430
4485
  lines.push("");
4431
4486
  }
4432
4487
  if (result.collisions.length > 0) {
4433
4488
  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)`
4489
+ `## Basename collisions (${result.collisions.length}) \u2014 another repo claims the same view name (not auto-wired)`
4435
4490
  );
4436
4491
  for (const c of result.collisions) lines.push(`- ${c.linkName} \u2190 ${c.repos.join(", ")}`);
4437
4492
  lines.push("");
4438
4493
  }
4439
4494
  if (result.unreachable.length > 0) {
4440
4495
  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`
4496
+ `## Unreachable (${result.unreachable.length}) \u2014 path unresolved, or it resolves to the view itself, so it cannot be aggregated`
4442
4497
  );
4443
4498
  for (const p of result.unreachable) lines.push(`- ${p}`);
4444
4499
  lines.push("");
4445
4500
  }
4446
4501
  if (result.strayUnknown.length > 0) {
4447
4502
  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`
4503
+ `## Strays left in place (${result.strayUnknown.length}) \u2014 not confirmed to be a basou-generated repo link, so not pruned. Check them manually`
4449
4504
  );
4450
4505
  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)";
4506
+ 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
4507
  lines.push(`- ${s.name} -> ${s.target}: ${detail}`);
4453
4508
  }
4454
4509
  lines.push("");
4455
4510
  }
4456
4511
  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"
4512
+ "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
4513
  );
4459
4514
  return lines.join("\n");
4460
4515
  }
@@ -4585,15 +4640,17 @@ async function doRunProjectPreset(options, ctx) {
4585
4640
  return result;
4586
4641
  }
4587
4642
  function presetActionLabel(action) {
4588
- return action === "create" ? "\u65B0\u898F\u4F5C\u6210" : "\u66F4\u65B0";
4643
+ return action === "create" ? "create" : "update";
4589
4644
  }
4590
4645
  function renderProjectPreset(result) {
4591
4646
  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)");
4647
+ lines.push(
4648
+ "# Instruction-file preset generation (declaration \u2192 the canonical's generated region)"
4649
+ );
4593
4650
  lines.push("");
4594
4651
  if (!result.hasRoster) {
4595
4652
  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"
4653
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare one with `basou project adopt`, then re-run."
4597
4654
  );
4598
4655
  return lines.join("\n");
4599
4656
  }
@@ -4601,7 +4658,7 @@ function renderProjectPreset(result) {
4601
4658
  const attempted = result.applied || result.failures.length > 0;
4602
4659
  if (!attempted) {
4603
4660
  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):`
4661
+ `Preset blocks to generate in the canonical of ${result.plans.length} repo${result.plans.length === 1 ? "" : "s"} (dry-run; pass --apply to write):`
4605
4662
  );
4606
4663
  for (const p of result.plans) {
4607
4664
  lines.push(
@@ -4611,7 +4668,7 @@ function renderProjectPreset(result) {
4611
4668
  }
4612
4669
  } else {
4613
4670
  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):";
4671
+ 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
4672
  lines.push(header);
4616
4673
  for (const p of result.plans) {
4617
4674
  if (failed.has(p.path)) continue;
@@ -4621,47 +4678,49 @@ function renderProjectPreset(result) {
4621
4678
  }
4622
4679
  }
4623
4680
  } 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");
4681
+ lines.push(
4682
+ "\u2705 Every declared repo's preset block is in sync with its canonical (nothing to generate)."
4683
+ );
4625
4684
  } else {
4626
4685
  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"
4686
+ "\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos (see below)."
4628
4687
  );
4629
4688
  }
4630
4689
  lines.push("");
4631
4690
  if (result.inSync.length > 0) {
4632
- lines.push(`\u540C\u671F\u6E08\u307F (${result.inSync.length}): ${result.inSync.join(", ")}`);
4691
+ lines.push(`In sync (${result.inSync.length}): ${result.inSync.join(", ")}`);
4633
4692
  lines.push("");
4634
4693
  }
4635
4694
  if (result.failures.length > 0) {
4636
4695
  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`
4696
+ `## Write failed (${result.failures.length}) \u2014 some canonicals could not be written`
4638
4697
  );
4639
4698
  for (const f of result.failures) lines.push(`- ${f.repo}: ${f.message}`);
4640
4699
  lines.push("");
4641
4700
  }
4642
4701
  if (result.markerConflicts.length > 0) {
4643
4702
  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`
4703
+ `## Marker conflicts (${result.markerConflicts.length}) \u2014 the canonical's markers are missing/malformed, so it is not overwritten`
4645
4704
  );
4646
4705
  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})`;
4706
+ const detail = c.reason === "no_markers" ? "no marker region" : `malformed markers (${c.reason})`;
4648
4707
  lines.push(`- ${c.repo}: ${detail}`);
4649
4708
  }
4650
4709
  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`
4710
+ ` Fix: add these two lines where you want the preset block \u2014 \`${GENERATED_START}\` and \`${GENERATED_END}\` (absent, basou creates a fresh canonical).`
4652
4711
  );
4653
4712
  lines.push("");
4654
4713
  }
4655
4714
  if (result.unreadable.length > 0) {
4656
4715
  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`
4716
+ `## Canonical unreadable (${result.unreadable.length}) \u2014 could not be read (a directory, permissions, etc.)`
4658
4717
  );
4659
4718
  for (const p of result.unreadable) lines.push(`- ${p}`);
4660
4719
  lines.push("");
4661
4720
  }
4662
4721
  if (result.collisions.length > 0) {
4663
4722
  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)`
4723
+ `## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-generated)`
4665
4724
  );
4666
4725
  for (const c of result.collisions) {
4667
4726
  lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
@@ -4670,25 +4729,25 @@ function renderProjectPreset(result) {
4670
4729
  }
4671
4730
  if (result.undeclared.length > 0) {
4672
4731
  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`
4732
+ `## Undeclared (${result.undeclared.length}) \u2014 visibility / language / publishes unset, so nothing is generated`
4674
4733
  );
4675
4734
  for (const p of result.undeclared) lines.push(`- ${p}`);
4676
4735
  lines.push("");
4677
4736
  }
4678
4737
  if (result.anchors.length > 0) {
4679
4738
  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`
4739
+ `## Anchor (${result.anchors.length}) \u2014 its own AGENTS.md is hand-maintained, so it is skipped`
4681
4740
  );
4682
4741
  for (const p of result.anchors) lines.push(`- ${p}`);
4683
4742
  lines.push("");
4684
4743
  }
4685
4744
  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`);
4745
+ lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4687
4746
  for (const p of result.unreachable) lines.push(`- ${p}`);
4688
4747
  lines.push("");
4689
4748
  }
4690
4749
  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"
4750
+ "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
4751
  );
4693
4752
  return lines.join("\n");
4694
4753
  }
@@ -4749,6 +4808,403 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4749
4808
  canonical: canonical2
4750
4809
  };
4751
4810
  }
4811
+ function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
4812
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4813
+ return expectedSymlinkTargets(repoReal, canonicalFile);
4814
+ }
4815
+ function viewLinkPointsAt(viewDir, name, repoReal) {
4816
+ const filePath = join7(viewDir, name);
4817
+ try {
4818
+ if (!lstatSync(filePath).isSymbolicLink()) return false;
4819
+ const target = readlinkSync(filePath);
4820
+ if (isAbsolute3(target)) return false;
4821
+ return realpathSync(resolve7(viewDir, target)) === repoReal;
4822
+ } catch {
4823
+ return false;
4824
+ }
4825
+ }
4826
+ function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
4827
+ const filePath = join7(viewDir, name);
4828
+ try {
4829
+ if (!lstatSync(filePath).isSymbolicLink()) return false;
4830
+ const target = readlinkSync(filePath);
4831
+ if (isAbsolute3(target)) return false;
4832
+ return resolve7(viewDir, target) === expectedRepoPath;
4833
+ } catch {
4834
+ return false;
4835
+ }
4836
+ }
4837
+ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4838
+ const anchorReal = realpathSync(repositoryRoot);
4839
+ let repoReal;
4840
+ try {
4841
+ repoReal = realpathSync(resolve7(repositoryRoot, target));
4842
+ } catch {
4843
+ repoReal = void 0;
4844
+ }
4845
+ const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
4846
+ const targetAbs = resolve7(repositoryRoot, target);
4847
+ const canonicalName = basename4(repoReal ?? targetAbs);
4848
+ const roster = manifest.repos ?? [];
4849
+ const inRoster = roster.some((r) => {
4850
+ try {
4851
+ return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
4852
+ } catch {
4853
+ return resolve7(repositoryRoot, r.path) === targetAbs;
4854
+ }
4855
+ });
4856
+ const cnFold = canonicalName.toLowerCase();
4857
+ const canonicalShared = roster.some((r) => {
4858
+ let rReal = null;
4859
+ try {
4860
+ rReal = realpathSync(resolve7(repositoryRoot, r.path));
4861
+ } catch {
4862
+ rReal = null;
4863
+ }
4864
+ if (rReal !== null) {
4865
+ if (repoReal !== void 0 && rReal === repoReal) return false;
4866
+ return basename4(rReal).toLowerCase() === cnFold;
4867
+ }
4868
+ if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
4869
+ return basename4(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
4870
+ });
4871
+ const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
4872
+ const items = [];
4873
+ if (!isAnchor) {
4874
+ if (repoReal !== void 0) {
4875
+ for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
4876
+ const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
4877
+ if (state === "correct")
4878
+ items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
4879
+ else if (state === "mismatch")
4880
+ items.push({
4881
+ kind: "instruction-symlink",
4882
+ label: spec.name,
4883
+ state: "foreign",
4884
+ note: `points at a different target (${actualTarget ?? "?"})`
4885
+ });
4886
+ else if (state === "occupied")
4887
+ items.push({
4888
+ kind: "instruction-symlink",
4889
+ label: spec.name,
4890
+ state: "foreign",
4891
+ note: "a real file, not a symlink"
4892
+ });
4893
+ else if (state === "blocked")
4894
+ items.push({
4895
+ kind: "instruction-symlink",
4896
+ label: spec.name,
4897
+ state: "blocked",
4898
+ note: "could not be inspected"
4899
+ });
4900
+ }
4901
+ let ignored;
4902
+ try {
4903
+ ignored = new Set(readGitignoreLines(join7(repoReal, ".gitignore")).map((l) => l.trim()));
4904
+ for (const p of INSTRUCTION_FILES) {
4905
+ if (ignored.has(p) || ignored.has(`/${p}`)) {
4906
+ items.push({
4907
+ kind: "gitignore",
4908
+ label: p,
4909
+ state: "manual",
4910
+ note: "cannot tell a basou-appended line from a hand-added one (no marker) \u2014 remove manually"
4911
+ });
4912
+ }
4913
+ }
4914
+ } catch {
4915
+ items.push({
4916
+ kind: "gitignore",
4917
+ label: ".gitignore",
4918
+ state: "blocked",
4919
+ note: "could not be read"
4920
+ });
4921
+ }
4922
+ }
4923
+ const viewPath = manifest.workspace.view;
4924
+ if (viewPath !== void 0) {
4925
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
4926
+ const linkPath = join7(viewDir, canonicalName);
4927
+ let isLink = false;
4928
+ try {
4929
+ isLink = lstatSync(linkPath).isSymbolicLink();
4930
+ } catch {
4931
+ isLink = false;
4932
+ }
4933
+ if (isLink) {
4934
+ const owned = repoReal !== void 0 ? viewLinkPointsAt(viewDir, canonicalName, repoReal) : viewLinkPointsAtPath(viewDir, canonicalName, targetAbs);
4935
+ if (!owned)
4936
+ items.push({
4937
+ kind: "view-symlink",
4938
+ label: canonicalName,
4939
+ state: "foreign",
4940
+ note: "a view link that does not point at this repo"
4941
+ });
4942
+ else if (canonicalShared)
4943
+ items.push({
4944
+ kind: "view-symlink",
4945
+ label: canonicalName,
4946
+ state: "blocked",
4947
+ note: collisionNote
4948
+ });
4949
+ else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
4950
+ }
4951
+ }
4952
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4953
+ const canonicalLabel = join7("agents", canonicalName, CANONICAL_FILE);
4954
+ let canonicalIsLink = false;
4955
+ try {
4956
+ canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
4957
+ } catch {
4958
+ canonicalIsLink = false;
4959
+ }
4960
+ if (canonicalIsLink) {
4961
+ items.push({
4962
+ kind: "canonical-block",
4963
+ label: canonicalLabel,
4964
+ state: "foreign",
4965
+ note: "the canonical is a symlink (not generated)"
4966
+ });
4967
+ } else if (existsSync(canonicalFile)) {
4968
+ let content;
4969
+ try {
4970
+ content = readFileSync(canonicalFile, "utf8");
4971
+ } catch {
4972
+ items.push({
4973
+ kind: "canonical-block",
4974
+ label: canonicalLabel,
4975
+ state: "blocked",
4976
+ note: "could not be read"
4977
+ });
4978
+ }
4979
+ if (content !== void 0 && content !== "") {
4980
+ const section = parseMarkers(content);
4981
+ if (section.kind === "ok" && canonicalShared) {
4982
+ items.push({
4983
+ kind: "canonical-block",
4984
+ label: canonicalLabel,
4985
+ state: "blocked",
4986
+ note: collisionNote
4987
+ });
4988
+ } else if (section.kind === "ok" && repoReal === void 0) {
4989
+ items.push({
4990
+ kind: "canonical-block",
4991
+ label: canonicalLabel,
4992
+ state: "manual",
4993
+ note: "repo could not be resolved, so ownership cannot be verified (check manually)"
4994
+ });
4995
+ } else if (section.kind === "ok") {
4996
+ const emptyAfter = removeMarkerSection(content, canonicalLabel).trim().length === 0;
4997
+ items.push({
4998
+ kind: "canonical-block",
4999
+ label: canonicalLabel,
5000
+ state: "removable",
5001
+ ...emptyAfter ? {
5002
+ note: "the file becomes empty after the generated block is removed (a manual-delete candidate)"
5003
+ } : {}
5004
+ });
5005
+ } else if (section.kind === "no_markers") {
5006
+ items.push({
5007
+ kind: "canonical-block",
5008
+ label: canonicalLabel,
5009
+ state: "foreign",
5010
+ note: "no generated block (hand-authored only \u2014 left untouched)"
5011
+ });
5012
+ } else {
5013
+ items.push({
5014
+ kind: "canonical-block",
5015
+ label: canonicalLabel,
5016
+ state: "blocked",
5017
+ note: "malformed markers (fix manually)"
5018
+ });
5019
+ }
5020
+ }
5021
+ }
5022
+ }
5023
+ return {
5024
+ target,
5025
+ resolved: repoReal !== void 0,
5026
+ repoReal: repoReal ?? null,
5027
+ canonicalName,
5028
+ isAnchor,
5029
+ inRoster,
5030
+ items,
5031
+ removableCount: items.filter((i) => i.state === "removable").length
5032
+ };
5033
+ }
5034
+ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5035
+ const removed = [];
5036
+ const failed = [];
5037
+ const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
5038
+ let currentRepoReal = null;
5039
+ try {
5040
+ currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
5041
+ } catch {
5042
+ currentRepoReal = null;
5043
+ }
5044
+ const identityOk = plan.repoReal === null ? true : currentRepoReal === plan.repoReal;
5045
+ const removable = plan.items.filter((i) => i.state === "removable");
5046
+ if (!identityOk) {
5047
+ for (const item of removable) changed(item.label);
5048
+ return { removed, failed };
5049
+ }
5050
+ const anchorReal = realpathSync(repositoryRoot);
5051
+ const { canonicalName, repoReal } = plan;
5052
+ const expectedByName = new Map(
5053
+ repoReal !== null ? teardownExpectedTargets(repoReal, anchorReal, canonicalName).map((s) => [s.name, s.target]) : []
5054
+ );
5055
+ for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5056
+ const expected = expectedByName.get(item.label);
5057
+ if (repoReal === null || expected === void 0 || inspectSymlink(join7(repoReal, item.label), expected).state !== "correct") {
5058
+ changed(item.label);
5059
+ continue;
5060
+ }
5061
+ try {
5062
+ unlinkSync(join7(repoReal, item.label));
5063
+ removed.push(item.label);
5064
+ } catch (error) {
5065
+ failed.push({ label: item.label, message: failureReason(error) });
5066
+ }
5067
+ }
5068
+ const viewPath = manifest.workspace.view;
5069
+ for (const item of removable.filter((i) => i.kind === "view-symlink")) {
5070
+ if (viewPath === void 0) {
5071
+ changed(item.label);
5072
+ continue;
5073
+ }
5074
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
5075
+ const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve7(repositoryRoot, plan.target));
5076
+ if (!owned) {
5077
+ changed(item.label);
5078
+ continue;
5079
+ }
5080
+ try {
5081
+ unlinkSync(join7(viewDir, item.label));
5082
+ removed.push(`view/${item.label}`);
5083
+ } catch (error) {
5084
+ failed.push({ label: `view/${item.label}`, message: failureReason(error) });
5085
+ }
5086
+ }
5087
+ const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5088
+ for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5089
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5090
+ try {
5091
+ if (lstatSync(canonicalFile).isSymbolicLink()) {
5092
+ changed(item.label);
5093
+ continue;
5094
+ }
5095
+ } catch (error) {
5096
+ failed.push({ label: item.label, message: failureReason(error) });
5097
+ continue;
5098
+ }
5099
+ let fd;
5100
+ try {
5101
+ fd = openSync(canonicalFile, fsConstants.O_RDWR | NOFOLLOW);
5102
+ } catch (error) {
5103
+ changed(item.label);
5104
+ void error;
5105
+ continue;
5106
+ }
5107
+ try {
5108
+ const content = readFileSync(fd, "utf8");
5109
+ if (parseMarkers(content).kind !== "ok") {
5110
+ changed(item.label);
5111
+ continue;
5112
+ }
5113
+ const next = Buffer.from(removeMarkerSection(content, item.label), "utf8");
5114
+ ftruncateSync(fd, 0);
5115
+ writeSync(fd, next, 0, next.length, 0);
5116
+ removed.push(item.label);
5117
+ } catch (error) {
5118
+ failed.push({ label: item.label, message: failureReason(error) });
5119
+ } finally {
5120
+ closeSync(fd);
5121
+ }
5122
+ }
5123
+ return { removed, failed };
5124
+ }
5125
+ function renderProjectTeardown(result) {
5126
+ const lines = [];
5127
+ lines.push(`# teardown: ${result.target}`);
5128
+ lines.push("");
5129
+ if (result.isAnchor) {
5130
+ lines.push("The anchor (`.`) cannot be torn down (it is the project's home).");
5131
+ return lines.join("\n");
5132
+ }
5133
+ if (!result.resolved) {
5134
+ lines.push(
5135
+ "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."
5136
+ );
5137
+ lines.push("");
5138
+ }
5139
+ lines.push(
5140
+ result.inRoster ? "Status: still a roster member (the declaration remains in the manifest)." : "Status: not a roster member (already archived)."
5141
+ );
5142
+ lines.push("");
5143
+ const removable = result.items.filter((i) => i.state === "removable");
5144
+ const manual = result.items.filter((i) => i.state === "manual");
5145
+ const foreign = result.items.filter((i) => i.state === "foreign");
5146
+ const blocked = result.items.filter((i) => i.state === "blocked");
5147
+ if (removable.length === 0) {
5148
+ lines.push("No basou-generated artifact to remove.");
5149
+ } else {
5150
+ lines.push(`To remove (${removable.length}):`);
5151
+ for (const i of removable)
5152
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5153
+ }
5154
+ if (manual.length > 0) {
5155
+ lines.push("");
5156
+ lines.push("Check manually (not auto-removed):");
5157
+ for (const i of manual)
5158
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5159
+ }
5160
+ if (foreign.length > 0) {
5161
+ lines.push("");
5162
+ lines.push("Left untouched (not basou-generated):");
5163
+ for (const i of foreign)
5164
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5165
+ }
5166
+ if (blocked.length > 0) {
5167
+ lines.push("");
5168
+ lines.push("Could not be inspected:");
5169
+ for (const i of blocked)
5170
+ lines.push(` - [${i.kind}] ${i.label}${i.note !== void 0 ? ` \u2014 ${i.note}` : ""}`);
5171
+ }
5172
+ lines.push("");
5173
+ if (result.applied) {
5174
+ lines.push(`--apply: removed ${result.removed.length}.`);
5175
+ for (const r of result.removed) lines.push(` \u2713 ${r}`);
5176
+ if (result.failed.length > 0) {
5177
+ lines.push("Failed:");
5178
+ for (const f of result.failed) lines.push(` \u2717 ${f.label} \u2014 ${f.message}`);
5179
+ }
5180
+ } else if (removable.length > 0) {
5181
+ lines.push(
5182
+ "This is a dry-run. Pass --apply to remove (this is a destructive, irreversible operation)."
5183
+ );
5184
+ }
5185
+ return lines.join("\n");
5186
+ }
5187
+ async function doRunProjectTeardown(target, options, ctx = {}) {
5188
+ const cwd = ctx.cwd ?? process.cwd();
5189
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project teardown");
5190
+ const paths = basouPaths10(repositoryRoot);
5191
+ const manifest = await readManifest6(paths);
5192
+ const plan = gatherRepoTeardown(repositoryRoot, manifest, target);
5193
+ const willApply = options.apply === true && !plan.isAnchor && plan.removableCount > 0;
5194
+ const { removed, failed } = willApply ? applyRepoTeardown(repositoryRoot, manifest, plan) : { removed: [], failed: [] };
5195
+ const result = { ...plan, applied: willApply, removed, failed };
5196
+ if (options.json === true) console.log(JSON.stringify(result));
5197
+ else console.log(renderProjectTeardown(result));
5198
+ return result;
5199
+ }
5200
+ async function runProjectTeardown(target, options, ctx = {}) {
5201
+ try {
5202
+ await doRunProjectTeardown(target, options, ctx);
5203
+ } catch (error) {
5204
+ renderCliError(error, { verbose: isVerbose(options) });
5205
+ process.exitCode = 1;
5206
+ }
5207
+ }
4752
5208
  function omitKey(obj, key) {
4753
5209
  const clone = { ...obj };
4754
5210
  delete clone[key];
@@ -4818,68 +5274,74 @@ async function doRunProjectArchive(target, options, ctx) {
4818
5274
  }
4819
5275
  function renderProjectArchive(result) {
4820
5276
  const lines = [];
4821
- lines.push("# repo \u306E archive(roster \u304B\u3089\u7573\u3080)");
5277
+ lines.push("# Archive a repo (fold it out of the roster)");
4822
5278
  lines.push("");
4823
5279
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
4824
5280
  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");
5281
+ lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to archive.");
4826
5282
  return lines.join("\n");
4827
5283
  }
4828
5284
  if (result.isAnchor) {
4829
5285
  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`
5286
+ `\u26A0\uFE0F \`${result.target}\` is the anchor (the project root). The anchor cannot be archived (it is the manifest's home).`
4831
5287
  );
4832
5288
  return lines.join("\n");
4833
5289
  }
4834
5290
  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`);
5291
+ lines.push(`\u2139\uFE0F \`${result.target}\` is not declared in the roster (nothing to archive).`);
4836
5292
  return lines.join("\n");
4837
5293
  }
4838
5294
  if (result.applied) {
4839
- lines.push(`\u2705 \`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u3057\u307E\u3057\u305F\u3002`);
5295
+ lines.push(`\u2705 Removed \`${result.target}\` from the roster.`);
4840
5296
  } else {
4841
- lines.push(`\`${result.target}\` \u3092 roster \u304B\u3089\u524A\u9664\u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`);
5297
+ lines.push(`To remove \`${result.target}\` from the roster (dry-run; pass --apply to write):`);
4842
5298
  }
4843
5299
  if (result.sourceRootRemoval !== void 0) {
4844
5300
  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`
5301
+ `- ${result.applied ? "Pruned" : "Will prune"} ${result.sourceRootRemoval} from source_roots (no longer captured by refresh).`
4846
5302
  );
4847
5303
  } else {
4848
- lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(prune \u4E0D\u8981)\u3002");
5304
+ lines.push("- No matching entry in source_roots (nothing to prune).");
4849
5305
  }
4850
5306
  if (result.reposEmptied) {
4851
5307
  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"
5308
+ "- This was the last member \u2192 the roster empties and the `repos` declaration is removed (the project is folded up)."
4853
5309
  );
4854
5310
  } else if (result.becomesSolo) {
4855
5311
  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"
5312
+ "- This leaves 1 repo (solo) \u2192 the workspace view is unnecessary (consider removing the view declaration / directory)."
4857
5313
  );
4858
5314
  }
4859
5315
  lines.push("");
4860
5316
  const t = result.teardown;
4861
5317
  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(", ")})`);
5318
+ if (t.viewLink) items.push("the workspace view's symlink entry");
5319
+ if (t.instructionFiles.length > 0)
5320
+ items.push(`instruction files (${t.instructionFiles.join(", ")})`);
4864
5321
  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)`);
5322
+ items.push(`.gitignore instruction patterns (${t.gitignorePatterns.join(", ")})`);
5323
+ if (t.canonical)
5324
+ items.push(`the anchor's canonical (agents/${basename4(result.target)}/AGENTS.md)`);
4867
5325
  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
5326
  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"
5327
+ "## Manual teardown (the repo could not be resolved on disk, so it was not inspected)"
5328
+ );
5329
+ lines.push(
5330
+ "- The repo may already be deleted. Check manually for a leftover view symlink / instruction symlinks / .gitignore / canonical."
4871
5331
  );
4872
5332
  lines.push("");
4873
5333
  } 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)");
5334
+ lines.push(
5335
+ "## Manual teardown (--apply does not touch these; remove the leftover wiring by hand)"
5336
+ );
4875
5337
  for (const i of items) lines.push(`- ${i}`);
4876
5338
  lines.push("");
4877
5339
  } else {
4878
- lines.push("repo \u5074\u306E wiring(view/\u6307\u793A\u66F8/.gitignore/canonical)\u306F\u6B8B\u3063\u3066\u3044\u307E\u305B\u3093\u3002");
5340
+ lines.push("No repo-side wiring (view / instruction files / .gitignore / canonical) remains.");
4879
5341
  lines.push("");
4880
5342
  }
4881
5343
  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"
5344
+ "Note: archive only changes the manifest (.basou, git-tracked, reversible). The repo, its captured history, and its on-disk wiring are not removed."
4883
5345
  );
4884
5346
  return lines.join("\n");
4885
5347
  }
@@ -4965,46 +5427,46 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
4965
5427
  }
4966
5428
  function renderProjectRename(result) {
4967
5429
  const lines = [];
4968
- lines.push("# repo \u306E rename(roster \u306E\u30D1\u30B9\u66F4\u65B0)");
5430
+ lines.push("# Rename a repo (update its roster path)");
4969
5431
  lines.push("");
4970
5432
  lines.push(...preservedUnknownLines(result.preservedUnknownFields));
4971
5433
  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");
5434
+ lines.push("\u2139\uFE0F No repo roster declared (manifest `repos`). There is nothing to rename.");
4973
5435
  return lines.join("\n");
4974
5436
  }
4975
5437
  if (result.noop) {
4976
- lines.push(`\u2139\uFE0F \`${result.oldTarget}\` \u3068 \`${result.newTarget}\` \u306F\u540C\u4E00\u3067\u3059(\u5909\u66F4\u306A\u3057)\u3002`);
5438
+ lines.push(`\u2139\uFE0F \`${result.oldTarget}\` and \`${result.newTarget}\` are identical (no change).`);
4977
5439
  return lines.join("\n");
4978
5440
  }
4979
5441
  if (result.isAnchor) {
4980
5442
  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`
5443
+ `\u26A0\uFE0F \`${result.oldTarget}\` is the anchor (the project root). The anchor cannot be renamed.`
4982
5444
  );
4983
5445
  return lines.join("\n");
4984
5446
  }
4985
5447
  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`);
5448
+ lines.push(`\u2139\uFE0F \`${result.oldTarget}\` is not declared in the roster (nothing to rename).`);
4987
5449
  return lines.join("\n");
4988
5450
  }
4989
5451
  if (result.collision) {
4990
5452
  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`
5453
+ `\u26A0\uFE0F \`${result.newTarget}\` is already declared in the roster. Not renaming, to avoid a duplicate.`
4992
5454
  );
4993
5455
  return lines.join("\n");
4994
5456
  }
4995
5457
  if (result.applied) {
4996
- lines.push(`\u2705 \`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u3057\u307E\u3057\u305F\u3002`);
5458
+ lines.push(`\u2705 Renamed \`${result.oldTarget}\` to \`${result.newTarget}\`.`);
4997
5459
  } else {
4998
5460
  lines.push(
4999
- `\`${result.oldTarget}\` \u3092 \`${result.newTarget}\` \u306B rename \u4E88\u5B9A(dry-run\u3001\u53CD\u6620\u3059\u308B\u306B\u306F --apply):`
5461
+ `To rename \`${result.oldTarget}\` to \`${result.newTarget}\` (dry-run; pass --apply to write):`
5000
5462
  );
5001
5463
  }
5002
5464
  if (result.sourceRootRenamed !== void 0) {
5003
5465
  lines.push(
5004
- `- source_roots \u306E ${result.sourceRootRenamed} \u3092 ${result.newTarget} \u306B\u66F4\u65B0${result.applied ? "\u3057\u307E\u3057\u305F" : "\u3057\u307E\u3059"}\u3002`
5466
+ `- ${result.applied ? "Updated" : "Will update"} ${result.sourceRootRenamed} to ${result.newTarget} in source_roots.`
5005
5467
  );
5006
5468
  } else {
5007
- lines.push("- source_roots \u306B\u8A72\u5F53\u30A8\u30F3\u30C8\u30EA\u306F\u3042\u308A\u307E\u305B\u3093(\u66F4\u65B0\u4E0D\u8981)\u3002");
5469
+ lines.push("- No matching entry in source_roots (nothing to update).");
5008
5470
  }
5009
5471
  lines.push("");
5010
5472
  if (result.basenameChanged) {
@@ -5013,31 +5475,214 @@ function renderProjectRename(result) {
5013
5475
  const items = [];
5014
5476
  if (result.wiring.canonicalDirOld)
5015
5477
  items.push(`anchor canonical: agents/${oldName}/ \u2192 agents/${newName}/`);
5016
- if (result.wiring.viewLinkOld) items.push(`workspace view \u306E symlink: ${oldName} \u2192 ${newName}`);
5478
+ if (result.wiring.viewLinkOld) items.push(`workspace view symlink: ${oldName} \u2192 ${newName}`);
5017
5479
  if (items.length > 0) {
5018
5480
  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)"
5481
+ "## Manual rename (--apply does not touch these; the basename changed, so update them by hand)"
5020
5482
  );
5021
5483
  for (const i of items) lines.push(`- ${i}`);
5022
5484
  } else {
5023
5485
  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`
5486
+ `The basename changes ${oldName} \u2192 ${newName}, but no anchor canonical / view symlink was found.`
5025
5487
  );
5026
5488
  }
5027
5489
  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"
5490
+ " After applying, regenerate the instruction symlinks and the view with `basou project symlinks` / `basou project workspace`."
5029
5491
  );
5030
5492
  } else {
5031
5493
  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"
5494
+ "Note: the basename is unchanged. If you moved the repo elsewhere, regenerate the relative targets with `basou project symlinks` / `basou project workspace`."
5033
5495
  );
5034
5496
  }
5035
5497
  lines.push("");
5036
5498
  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"
5499
+ "Note: rename only changes the manifest (.basou, git-tracked, reversible). It does not move the repo or update the on-disk wiring."
5038
5500
  );
5039
5501
  return lines.join("\n");
5040
5502
  }
5503
+ async function runProjectNew(repos, options, ctx = {}) {
5504
+ try {
5505
+ await doRunProjectNew(repos, options, ctx);
5506
+ } catch (error) {
5507
+ renderCliError(error, { verbose: isVerbose(options) });
5508
+ process.exitCode = 1;
5509
+ }
5510
+ }
5511
+ async function resolveRepositoryRootForNew(cwd) {
5512
+ try {
5513
+ return await resolveRepositoryRoot8(cwd);
5514
+ } catch (error) {
5515
+ if (error instanceof Error && error.message === "Not a git repository") {
5516
+ throw new Error(
5517
+ "Not a git repository. Run 'git init' first, then re-run 'basou project new'.",
5518
+ { cause: error }
5519
+ );
5520
+ }
5521
+ throw error;
5522
+ }
5523
+ }
5524
+ async function doRunProjectNew(repos, options, ctx) {
5525
+ const cwd = ctx.cwd ?? process.cwd();
5526
+ const repositoryRoot = await resolveRepositoryRootForNew(cwd);
5527
+ const workspaceName = basename4(repositoryRoot);
5528
+ const declared = repos.map((p) => {
5529
+ const abs = resolve7(cwd, p);
5530
+ let real;
5531
+ try {
5532
+ real = realpathSync(abs);
5533
+ } catch {
5534
+ real = abs;
5535
+ }
5536
+ const rel = relative2(repositoryRoot, real);
5537
+ return rel === "" ? "." : rel;
5538
+ });
5539
+ const invalidRepos = declared.filter(
5540
+ (rel) => classifySourceRoot(repositoryRoot, rel).kind !== "repo"
5541
+ );
5542
+ if (invalidRepos.length > 0) {
5543
+ throw new Error(
5544
+ `These declared repos are not git repositories (create them with 'git init' first): ${invalidRepos.join(", ")}`
5545
+ );
5546
+ }
5547
+ const rosterPaths = ["."];
5548
+ for (const rel of declared) {
5549
+ if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
5550
+ }
5551
+ const roster = rosterPaths.map((path) => ({ path }));
5552
+ const viewPath = options.view === false ? null : options.view ?? `../${workspaceName}-workspace`;
5553
+ const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
5554
+ const paths = basouPaths10(repositoryRoot);
5555
+ const existed = existsSync(paths.files.manifest);
5556
+ const manifest = createManifest2({ workspaceName, sourceRoots });
5557
+ manifest.repos = roster;
5558
+ if (viewPath !== null) manifest.workspace.view = viewPath;
5559
+ let applied = false;
5560
+ if (options.apply === true) {
5561
+ await ensureBasouDirectory2(repositoryRoot);
5562
+ await writeManifest2(paths, manifest, { force: options.force === true });
5563
+ applied = true;
5564
+ try {
5565
+ await appendBasouGitignore2(repositoryRoot, { localOnly: options.localOnly === true });
5566
+ } catch (error) {
5567
+ renderGitignoreWarningForNew(error, isVerbose(options));
5568
+ }
5569
+ }
5570
+ const result = {
5571
+ workspaceName,
5572
+ repos: roster,
5573
+ view: viewPath,
5574
+ sourceRoots,
5575
+ invalidRepos: [],
5576
+ existed,
5577
+ applied
5578
+ };
5579
+ if (options.json === true) {
5580
+ console.log(JSON.stringify(result));
5581
+ } else {
5582
+ console.log(renderProjectNew(result));
5583
+ }
5584
+ return result;
5585
+ }
5586
+ function renderGitignoreWarningForNew(error, verbose) {
5587
+ const baseMessage = error instanceof Error ? error.message : String(error);
5588
+ console.error(
5589
+ `Warning: Could not update .gitignore (${baseMessage}). Add Basou's default .gitignore block manually.`
5590
+ );
5591
+ if (verbose && error instanceof Error) {
5592
+ const label = extractCauseLabel(error);
5593
+ if (label !== void 0) console.error(`Caused by: ${label}`);
5594
+ }
5595
+ }
5596
+ function renderProjectNew(result) {
5597
+ const lines = [];
5598
+ lines.push("# Scaffold a new project (build from a declaration)");
5599
+ lines.push("");
5600
+ if (result.existed && !result.applied) {
5601
+ lines.push(
5602
+ "\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)."
5603
+ );
5604
+ lines.push("");
5605
+ }
5606
+ if (result.applied) {
5607
+ lines.push(`\u2705 Created \`.basou/\` for \`${result.workspaceName}\` and seeded the manifest:`);
5608
+ } else {
5609
+ lines.push(
5610
+ `Will create \`.basou/\` for \`${result.workspaceName}\` and seed the manifest (dry-run; pass --apply to write):`
5611
+ );
5612
+ }
5613
+ lines.push("");
5614
+ lines.push(`repos roster (${result.repos.length}):`);
5615
+ for (const r of result.repos) {
5616
+ lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
5617
+ }
5618
+ lines.push("");
5619
+ lines.push(
5620
+ result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
5621
+ );
5622
+ lines.push("");
5623
+ lines.push(`source_roots (${result.sourceRoots.length}):`);
5624
+ for (const s of result.sourceRoots) lines.push(`- ${s}`);
5625
+ lines.push("");
5626
+ lines.push(
5627
+ "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."
5628
+ );
5629
+ if (result.applied) {
5630
+ lines.push(
5631
+ "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."
5632
+ );
5633
+ } else {
5634
+ lines.push(
5635
+ "After applying, fill in visibility / language in the manifest, then run `basou project derive`."
5636
+ );
5637
+ }
5638
+ return lines.join("\n");
5639
+ }
5640
+ async function runProjectDerive(options, ctx = {}) {
5641
+ try {
5642
+ await doRunProjectDerive(options, ctx);
5643
+ } catch (error) {
5644
+ renderCliError(error, { verbose: isVerbose(options) });
5645
+ process.exitCode = 1;
5646
+ }
5647
+ }
5648
+ async function doRunProjectDerive(options, ctx) {
5649
+ const cwd = ctx.cwd ?? process.cwd();
5650
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
5651
+ const paths = basouPaths10(repositoryRoot);
5652
+ const manifest = await readManifest6(paths);
5653
+ if (manifest.repos === void 0 || manifest.repos.length === 0) {
5654
+ console.log(
5655
+ "# 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)."
5656
+ );
5657
+ return;
5658
+ }
5659
+ const apply = options.apply === true;
5660
+ const stepCtx = {
5661
+ cwd: repositoryRoot,
5662
+ ...ctx.now !== void 0 ? { now: ctx.now } : {}
5663
+ };
5664
+ const stepOpts = { apply };
5665
+ console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
5666
+ console.log("");
5667
+ console.log("## 1/5 sync source_roots (roster \u2192 capture config)");
5668
+ await doRunProjectSync(stepOpts, stepCtx);
5669
+ console.log("");
5670
+ console.log("## 2/5 generate instruction-file A preset (declaration \u2192 canonical)");
5671
+ await doRunProjectPreset(stepOpts, stepCtx);
5672
+ console.log("");
5673
+ console.log("## 3/5 generate instruction-file symlinks (each repo \u2192 canonical)");
5674
+ await doRunProjectSymlinks(stepOpts, stepCtx);
5675
+ console.log("");
5676
+ console.log("## 4/5 generate workspace view (aggregate the roster repos)");
5677
+ await doRunProjectWorkspace(stepOpts, stepCtx);
5678
+ console.log("");
5679
+ console.log("## 5/5 generate .gitignore (exclude public repos' instruction files)");
5680
+ await doRunProjectGitignore(stepOpts, stepCtx);
5681
+ console.log("");
5682
+ console.log(
5683
+ 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."
5684
+ );
5685
+ }
5041
5686
 
5042
5687
  // src/commands/protocol.ts
5043
5688
  import { readFile as readFile4 } from "fs/promises";
@@ -5046,7 +5691,7 @@ import {
5046
5691
  PROTOCOL_START,
5047
5692
  parseMarkers as parseMarkers2,
5048
5693
  readMarkdownFile as readMarkdownFile5,
5049
- removeMarkerSection
5694
+ removeMarkerSection as removeMarkerSection2
5050
5695
  } from "@basou/core";
5051
5696
 
5052
5697
  // src/lib/durable-write.ts
@@ -5338,7 +5983,7 @@ async function doRunProtocolUnsync(options) {
5338
5983
  console.log("No target file; nothing to remove.");
5339
5984
  return;
5340
5985
  }
5341
- const newBody = removeMarkerSection(existing, "CLAUDE.md", PROTOCOL_MARKERS);
5986
+ const newBody = removeMarkerSection2(existing, "CLAUDE.md", PROTOCOL_MARKERS);
5342
5987
  if (newBody === existing) {
5343
5988
  console.log("No basou:protocols block found; nothing removed.");
5344
5989
  return;
@@ -5711,7 +6356,7 @@ import {
5711
6356
  basouPaths as basouPaths12,
5712
6357
  findErrorCode as findErrorCode10,
5713
6358
  renderReport,
5714
- resolveRepositoryRoot as resolveRepositoryRoot8,
6359
+ resolveRepositoryRoot as resolveRepositoryRoot9,
5715
6360
  writeMarkdownFile as writeMarkdownFile6
5716
6361
  } from "@basou/core";
5717
6362
  function registerReportCommand(program2) {
@@ -5760,7 +6405,7 @@ async function doRunReportGenerate(options, ctx) {
5760
6405
  }
5761
6406
  async function resolveRepositoryRootForReport(cwd) {
5762
6407
  try {
5763
- return await resolveRepositoryRoot8(cwd);
6408
+ return await resolveRepositoryRoot9(cwd);
5764
6409
  } catch (error) {
5765
6410
  if (error instanceof Error && error.message === "Not a git repository") {
5766
6411
  throw new Error(
@@ -5843,45 +6488,45 @@ async function doRunReviewGaps(options, ctx) {
5843
6488
  return summary;
5844
6489
  }
5845
6490
  function relAge(iso, now) {
5846
- if (iso === null) return "(\u4E0D\u660E)";
6491
+ if (iso === null) return "(unknown)";
5847
6492
  const ms = now.getTime() - Date.parse(iso);
5848
- if (!Number.isFinite(ms) || ms < 0) return "\u305F\u3063\u305F\u4ECA";
6493
+ if (!Number.isFinite(ms) || ms < 0) return "just now";
5849
6494
  const days = Math.floor(ms / 864e5);
5850
- if (days >= 1) return `${days}\u65E5\u524D`;
6495
+ if (days >= 1) return `${days}d ago`;
5851
6496
  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`;
6497
+ if (hours >= 1) return `${hours}h ago`;
6498
+ return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
5854
6499
  }
5855
6500
  function unitLine(u, now) {
5856
6501
  const when = relAge(u.lastCommitAt, now);
5857
6502
  const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
5858
6503
  if (u.verdict === "near_unbound") {
5859
6504
  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}]`;
6505
+ return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]`;
5861
6506
  }
5862
- return `${head} \u2014 \u7D10\u3065\u304F\u30AF\u30ED\u30B9\u30E2\u30C7\u30EB\u30EC\u30D3\u30E5\u30FC\u306A\u3057`;
6507
+ return `${head} \u2014 no bound cross-model review`;
5863
6508
  }
5864
6509
  function candidateLine(u, now) {
5865
6510
  const when = relAge(u.lastCommitAt, now);
5866
6511
  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}`;
6512
+ return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}`;
5868
6513
  }
5869
6514
  function renderReviewGaps(summary) {
5870
6515
  const now = new Date(summary.generatedAt);
5871
6516
  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})`);
6517
+ const scope = summary.scope ? summary.scope.join(", ") : "all repositories";
6518
+ lines.push(`# Review-trail gaps (${scope})`);
5874
6519
  lines.push("");
5875
6520
  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");
6521
+ lines.push("\u2705 Within the captured range, no unit of work landed without a review trail.");
5877
6522
  } 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}`);
6523
+ lines.push(`\u26A0\uFE0F Units of work that landed without a review trail: ${summary.gaps.length}`);
5879
6524
  for (const u of summary.gaps) lines.push(unitLine(u, now));
5880
6525
  }
5881
6526
  lines.push("");
5882
6527
  if (summary.candidates.length > 0) {
5883
6528
  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`
6529
+ `## To confirm (${summary.candidates.length}) \u2014 a cross-model review trace exists; confirm it actually examined this change`
5885
6530
  );
5886
6531
  for (const u of summary.candidates) lines.push(candidateLine(u, now));
5887
6532
  lines.push("");
@@ -5889,19 +6534,19 @@ function renderReviewGaps(summary) {
5889
6534
  if (summary.unknowns.length > 0) {
5890
6535
  const n = summary.unknowns.reduce((sum, u) => sum + u.commitCount, 0);
5891
6536
  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)`
6537
+ `## 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
6538
  );
5894
6539
  lines.push("");
5895
6540
  }
5896
- lines.push("## \u30EA\u30DD\u30B8\u30C8\u30EA\u5225");
6541
+ lines.push("## By repository");
5897
6542
  for (const r of summary.repos) {
5898
6543
  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}` : ""})`
6544
+ `- ${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
6545
  );
5901
6546
  }
5902
6547
  lines.push("");
5903
6548
  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`
6549
+ `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
6550
  );
5906
6551
  return lines.join("\n");
5907
6552
  }
@@ -5925,7 +6570,7 @@ import {
5925
6570
  readManifest as readManifest7,
5926
6571
  readYamlFile as readYamlFile6,
5927
6572
  resolveClaudeCodeCommand,
5928
- resolveRepositoryRoot as resolveRepositoryRoot9,
6573
+ resolveRepositoryRoot as resolveRepositoryRoot10,
5929
6574
  SessionSchema as SessionSchema2,
5930
6575
  sanitizeRelatedFiles,
5931
6576
  sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
@@ -6298,7 +6943,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
6298
6943
  }
6299
6944
  async function resolveRepositoryRootForRun(cwd) {
6300
6945
  try {
6301
- return await resolveRepositoryRoot9(cwd);
6946
+ return await resolveRepositoryRoot10(cwd);
6302
6947
  } catch (error) {
6303
6948
  if (error instanceof Error && error.message === "Not a git repository") {
6304
6949
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -6980,7 +7625,7 @@ import {
6980
7625
  basouPaths as basouPaths16,
6981
7626
  computeWorkStats,
6982
7627
  findErrorCode as findErrorCode12,
6983
- resolveRepositoryRoot as resolveRepositoryRoot10
7628
+ resolveRepositoryRoot as resolveRepositoryRoot11
6984
7629
  } from "@basou/core";
6985
7630
  function registerStatsCommand(program2) {
6986
7631
  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 +7728,7 @@ function formatInt(n) {
7083
7728
  }
7084
7729
  async function resolveRepositoryRootForStats(cwd) {
7085
7730
  try {
7086
- return await resolveRepositoryRoot10(cwd);
7731
+ return await resolveRepositoryRoot11(cwd);
7087
7732
  } catch (error) {
7088
7733
  if (error instanceof Error && error.message === "Not a git repository") {
7089
7734
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -7111,7 +7756,7 @@ import {
7111
7756
  buildStatusSnapshot,
7112
7757
  findErrorCode as findErrorCode13,
7113
7758
  readManifest as readManifest9,
7114
- resolveRepositoryRoot as resolveRepositoryRoot11,
7759
+ resolveRepositoryRoot as resolveRepositoryRoot12,
7115
7760
  writeStatus
7116
7761
  } from "@basou/core";
7117
7762
  function registerStatusCommand(program2) {
@@ -7167,7 +7812,7 @@ function renderTextStatus(s) {
7167
7812
  }
7168
7813
  async function resolveRepositoryRootForStatus(cwd) {
7169
7814
  try {
7170
- return await resolveRepositoryRoot11(cwd);
7815
+ return await resolveRepositoryRoot12(cwd);
7171
7816
  } catch (error) {
7172
7817
  if (error instanceof Error && error.message === "Not a git repository") {
7173
7818
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -8304,7 +8949,7 @@ import {
8304
8949
  basouPaths as basouPaths19,
8305
8950
  enumerateSessionDirs as enumerateSessionDirs3,
8306
8951
  findErrorCode as findErrorCode15,
8307
- resolveRepositoryRoot as resolveRepositoryRoot12,
8952
+ resolveRepositoryRoot as resolveRepositoryRoot13,
8308
8953
  resolveSessionId as resolveSessionId5,
8309
8954
  verifyEventsChain
8310
8955
  } from "@basou/core";
@@ -8375,7 +9020,7 @@ function renderVerdict(row) {
8375
9020
  }
8376
9021
  async function resolveRepositoryRootForVerify(cwd) {
8377
9022
  try {
8378
- return await resolveRepositoryRoot12(cwd);
9023
+ return await resolveRepositoryRoot13(cwd);
8379
9024
  } catch (error) {
8380
9025
  if (error instanceof Error && error.message === "Not a git repository") {
8381
9026
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -8405,7 +9050,7 @@ import {
8405
9050
  basouPaths as basouPaths20,
8406
9051
  findErrorCode as findErrorCode17,
8407
9052
  readManifest as readManifest13,
8408
- resolveRepositoryRoot as resolveRepositoryRoot13
9053
+ resolveRepositoryRoot as resolveRepositoryRoot14
8409
9054
  } from "@basou/core";
8410
9055
  import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
8411
9056
 
@@ -9821,7 +10466,7 @@ function waitForShutdown(signal) {
9821
10466
  }
9822
10467
  async function resolveRepositoryRootForView(cwd) {
9823
10468
  try {
9824
- return await resolveRepositoryRoot13(cwd);
10469
+ return await resolveRepositoryRoot14(cwd);
9825
10470
  } catch (error) {
9826
10471
  if (error instanceof Error && error.message === "Not a git repository") {
9827
10472
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {