@basou/cli 0.30.0 → 0.32.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
@@ -3049,7 +3049,6 @@ import {
3049
3049
  createManifest,
3050
3050
  ensureBasouDirectory,
3051
3051
  resolveRepositoryRoot as resolveRepositoryRoot7,
3052
- tryRemoteUrl,
3053
3052
  writeManifest
3054
3053
  } from "@basou/core";
3055
3054
  function collectValue(value, previous) {
@@ -3058,7 +3057,7 @@ function collectValue(value, previous) {
3058
3057
  function registerInitCommand(program2) {
3059
3058
  program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
3060
3059
  "--repo-url <url>",
3061
- "Repository URL (defaults to git remote.origin.url; pass empty string for null)"
3060
+ "Deprecated and ignored (project.repository_url was removed); accepted for 0.x CLI stability, removed at 1.0"
3062
3061
  ).option(
3063
3062
  "--source-root <path>",
3064
3063
  "Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
@@ -3083,11 +3082,10 @@ async function doRunInit(options, ctx) {
3083
3082
  const cwd = ctx.cwd ?? process.cwd();
3084
3083
  const repositoryRoot = await resolveRepositoryRootForInit(cwd);
3085
3084
  const workspaceName = options.name ?? basename4(repositoryRoot);
3086
- let repositoryUrl;
3087
3085
  if (options.repoUrl !== void 0) {
3088
- repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
3089
- } else {
3090
- repositoryUrl = await tryRemoteUrl(repositoryRoot);
3086
+ console.error(
3087
+ "Warning: --repo-url is deprecated and ignored (project.repository_url was removed); the flag will be removed at 1.0."
3088
+ );
3091
3089
  }
3092
3090
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
3093
3091
  const rel = relative(repositoryRoot, resolve5(cwd, p));
@@ -3098,7 +3096,6 @@ async function doRunInit(options, ctx) {
3098
3096
  workspaceName,
3099
3097
  ...options.projectName !== void 0 ? { projectName: options.projectName } : {},
3100
3098
  ...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
3101
- ...repositoryUrl !== void 0 ? { repositoryUrl } : {},
3102
3099
  ...sourceRoots.length > 0 ? { sourceRoots } : {}
3103
3100
  });
3104
3101
  await writeManifest(paths, manifest, { force: options.force === true });
@@ -3661,13 +3658,17 @@ import {
3661
3658
  readMarkdownFile as readMarkdownFile4,
3662
3659
  reconcileSourceRoots,
3663
3660
  removeMarkerSection,
3661
+ renderAnchorStarter,
3662
+ renderViewPresetBlock,
3664
3663
  renderWithMarkers as renderWithMarkers4,
3665
3664
  resolveRepositoryRoot as resolveRepositoryRoot8,
3666
3665
  safeSimpleGit,
3666
+ seedMarkers,
3667
3667
  summarizePresetPlan,
3668
3668
  summarizeRosterDrift,
3669
3669
  summarizeSymlinkPlan,
3670
3670
  summarizeWiring,
3671
+ summarizeWiringDrift,
3671
3672
  unknownManifestKeys,
3672
3673
  writeManifest as writeManifest2,
3673
3674
  writeMarkdownFile as writeMarkdownFile5
@@ -3677,7 +3678,7 @@ var CANONICAL_FILE = "AGENTS.md";
3677
3678
  function registerProjectCommand(program2) {
3678
3679
  const project = program2.command("project").description("Inspect a project's declared repo roster (read-only)");
3679
3680
  project.command("check").description(
3680
- "Compare the declared repo roster (manifest `repos`) against the capture config (`source_roots`) and surface drift (read-only, advisory)"
3681
+ "Surface project drift (read-only, advisory): the declared repo roster (manifest `repos`) vs the capture config (`source_roots`), AND the instruction-file wiring vs basou's native topology \u2014 a missing AGENTS.md canonical (repo or workspace view), incomplete spokes, conflicts, or collisions"
3681
3682
  ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
3682
3683
  await runProjectCheck(opts);
3683
3684
  });
@@ -3752,8 +3753,11 @@ function registerProjectCommand(program2) {
3752
3753
  project.command("new").argument("[repos...]", "Extra repo paths (besides the anchor) to seed into the roster").description(
3753
3754
  "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"
3754
3755
  ).option("--apply", "Create `.basou/` and write the seeded manifest (default: dry-run preview)").option(
3756
+ "--project-name <name>",
3757
+ "The project's product name (a simple name \u2014 letters, digits, '.', '-', '_'): sets `project.name` and the default view path (a `<name>-workspace` sibling). Omit to fall back to the anchor directory name; use --view for a custom view path"
3758
+ ).option(
3755
3759
  "--view <path>",
3756
- "Override the workspace view path (default: a <name>-workspace sibling)"
3760
+ "Override the workspace view path (default: a `<name>-workspace` sibling, where <name> is --project-name or the anchor directory name)"
3757
3761
  ).option("--no-view", "Solo project: declare no workspace view").option(
3758
3762
  "--local-only",
3759
3763
  "Write a .basou/ full-exclude .gitignore block (keep the trail out of version control) instead of the default ignore+commit block"
@@ -3766,10 +3770,10 @@ function registerProjectCommand(program2) {
3766
3770
  await runProjectDerive(opts);
3767
3771
  });
3768
3772
  project.command("retrofit").argument(
3769
- "<repo>",
3770
- "The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo)"
3773
+ "[repo]",
3774
+ "The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo). Omit to run only the workspace view's canonical auto-migration"
3771
3775
  ).description(
3772
- "Fold an existing repo's hand-authored AGENTS.md into the project topology: move the repo's regular-file `AGENTS.md` to the anchor canonical (`agents/<repo>/AGENTS.md`) and replace it with a symlink, so the prose lives at the single source of truth. Dry-run by default; pass --apply to relocate. The onboarding counterpart to `new` for a repo that already carries its own AGENTS.md \u2014 run it before `basou project derive`, which then adds the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore. Non-destructive: it refuses when the destination canonical already exists (it never clobbers it), and skips a repo whose AGENTS.md is already a symlink or absent. The anchor (`.`) is refused"
3776
+ "Fold an existing repo's hand-authored AGENTS.md into the project topology: move the repo's regular-file `AGENTS.md` to the anchor canonical (`agents/<repo>/AGENTS.md`) and replace it with a symlink, so the prose lives at the single source of truth. Dry-run by default; pass --apply to relocate. The onboarding counterpart to `new` for a repo that already carries its own AGENTS.md \u2014 run it before `basou project derive`, which then adds the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore. Non-destructive: it refuses when the destination canonical already exists (it never clobbers it), and skips a repo whose AGENTS.md is already a symlink or absent. The anchor (`.`) is refused. The workspace view's own canonical is auto-migrated when it is markerless prose (the generated block is prepended, the prose kept): omit the repo argument to perform that migration \u2014 a repo-argument run only reports it"
3773
3777
  ).option(
3774
3778
  "--apply",
3775
3779
  "Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
@@ -3800,18 +3804,39 @@ async function doRunProjectCheck(options, ctx) {
3800
3804
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
3801
3805
  const paths = basouPaths10(repositoryRoot);
3802
3806
  const manifest = await readManifest6(paths);
3803
- const summary = summarizeRosterDrift({
3807
+ const roster = summarizeRosterDrift({
3804
3808
  ...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
3805
3809
  sourceRoots: effectiveSourceRoots(manifest)
3806
3810
  });
3811
+ let wiring;
3812
+ const rosterRepos2 = manifest.repos ?? [];
3813
+ if (rosterRepos2.length > 0) {
3814
+ const anchorReal = realpathSync(repositoryRoot);
3815
+ const repoFacts = rosterRepos2.map(
3816
+ (entry) => gatherRepoSymlinks(repositoryRoot, anchorReal, entry)
3817
+ );
3818
+ let view = { kind: "no-view" };
3819
+ const viewPath = manifest.workspace.view;
3820
+ if (viewPath !== void 0) {
3821
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
3822
+ view = gatherViewSymlinks(repositoryRoot, anchorReal, rosterRepos2, viewDir);
3823
+ }
3824
+ wiring = summarizeWiringDrift({ repos: repoFacts, view });
3825
+ }
3826
+ const result = {
3827
+ roster,
3828
+ ...wiring !== void 0 ? { wiring } : {},
3829
+ ok: roster.ok && (wiring?.ok ?? true)
3830
+ };
3807
3831
  if (options.json === true) {
3808
- console.log(JSON.stringify(summary));
3832
+ console.log(JSON.stringify(result));
3809
3833
  } else {
3810
- console.log(renderProjectCheck(summary));
3834
+ console.log(renderProjectCheck(result));
3811
3835
  }
3812
- return summary;
3836
+ return result;
3813
3837
  }
3814
- function renderProjectCheck(summary) {
3838
+ function renderProjectCheck(result) {
3839
+ const summary = result.roster;
3815
3840
  const lines = [];
3816
3841
  lines.push("# Project composition check (declared vs captured)");
3817
3842
  lines.push("");
@@ -3846,8 +3871,58 @@ function renderProjectCheck(summary) {
3846
3871
  for (const p of summary.extra) lines.push(`- ${p}`);
3847
3872
  lines.push("");
3848
3873
  }
3874
+ const w = result.wiring;
3875
+ if (w !== void 0) {
3876
+ lines.push("## Instruction-file wiring drift (declared topology vs on-disk)");
3877
+ if (w.ok) {
3878
+ lines.push(
3879
+ "\u2705 Every present repo's and the view's instruction files (AGENTS.md + spokes) are wired as declared."
3880
+ );
3881
+ } else {
3882
+ if (w.missingCanonicals.length > 0) {
3883
+ lines.push(`\u26A0\uFE0F Missing instruction canonical (AGENTS.md): ${w.missingCanonicals.length}`);
3884
+ for (const m of w.missingCanonicals) {
3885
+ const where = m.target === "view" ? `view "${m.name}"` : m.target === "repo-self" ? `${m.name} (self)` : m.name;
3886
+ lines.push(`- ${where} \u2014 canonical AGENTS.md absent`);
3887
+ }
3888
+ }
3889
+ if (w.incompleteWiring.length > 0) {
3890
+ lines.push(
3891
+ `\u26A0\uFE0F Incomplete wiring (canonical present, spokes missing): ${w.incompleteWiring.length}`
3892
+ );
3893
+ for (const i of w.incompleteWiring) {
3894
+ const where = i.target === "view" ? `view "${i.path}"` : i.path;
3895
+ lines.push(`- ${where}: ${i.files.join(", ")}`);
3896
+ }
3897
+ }
3898
+ if (w.conflicts.length > 0) {
3899
+ lines.push(
3900
+ `\u26A0\uFE0F Wiring conflicts (existing file/link, left untouched): ${w.conflicts.length}`
3901
+ );
3902
+ for (const c of w.conflicts) {
3903
+ const where = c.target === "view" ? `view "${c.path}"` : c.path;
3904
+ const detail = c.reason === "mismatch" && c.actualTarget !== void 0 ? `mismatch -> ${c.actualTarget}` : c.reason;
3905
+ lines.push(`- ${where} ${c.file}: ${detail}`);
3906
+ }
3907
+ }
3908
+ if (w.collisions.length > 0) {
3909
+ lines.push(`\u26A0\uFE0F Canonical-name collisions (ambiguous, not wired): ${w.collisions.length}`);
3910
+ for (const c of w.collisions) {
3911
+ const who = c.view === true ? `${c.repos.join(", ")} + view` : c.repos.join(", ");
3912
+ lines.push(`- agents/${c.canonicalName}/AGENTS.md <- ${who}`);
3913
+ }
3914
+ }
3915
+ }
3916
+ if (w.unreachable.length > 0) {
3917
+ lines.push(
3918
+ `\u2139\uFE0F Declared repos not present on this machine (advisory \u2014 not counted as drift): ${w.unreachable.length}`
3919
+ );
3920
+ for (const u of w.unreachable) lines.push(`- ${u}`);
3921
+ }
3922
+ lines.push("");
3923
+ }
3849
3924
  lines.push(
3850
- "Note: read-only advisory. It only shows the difference between the declaration (repos) and the capture config (source_roots); it does not enforce."
3925
+ "Note: read-only advisory. It shows the difference between the declaration (repos) and the capture config (source_roots), and the instruction-file wiring vs basou's native topology; it does not enforce or generate."
3851
3926
  );
3852
3927
  return lines.join("\n");
3853
3928
  }
@@ -4301,6 +4376,17 @@ function inspectSymlink(filePath, expectedTarget) {
4301
4376
  const actual = readlinkSync(filePath);
4302
4377
  return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
4303
4378
  }
4379
+ function anchorCanonicalState(filePath) {
4380
+ let st;
4381
+ try {
4382
+ st = lstatSync(filePath);
4383
+ } catch (error) {
4384
+ if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
4385
+ return "broken";
4386
+ }
4387
+ if (st.isSymbolicLink()) return existsSync(filePath) ? "usable" : "broken";
4388
+ return st.isFile() ? "usable" : "broken";
4389
+ }
4304
4390
  function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4305
4391
  const mode = instructionMode(entry);
4306
4392
  const isSelf = mode === "self";
@@ -4312,7 +4398,42 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4312
4398
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4313
4399
  }
4314
4400
  if (real === anchorReal) {
4315
- return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
4401
+ const anchorCanonical = join9(real, CANONICAL_FILE);
4402
+ const anchorState = anchorCanonicalState(anchorCanonical);
4403
+ if (anchorState === "absent") {
4404
+ return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
4405
+ }
4406
+ if (anchorState === "broken") {
4407
+ return {
4408
+ ...base,
4409
+ isAnchor: true,
4410
+ reachable: true,
4411
+ canonicalPresent: true,
4412
+ canonicalName: basename5(real),
4413
+ files: [{ name: CANONICAL_FILE, expectedTarget: CANONICAL_FILE, state: "blocked" }]
4414
+ };
4415
+ }
4416
+ const anchorFiles = expectedSymlinkTargets(
4417
+ real,
4418
+ anchorCanonical,
4419
+ "self"
4420
+ ).map((spec) => {
4421
+ const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
4422
+ return {
4423
+ name: spec.name,
4424
+ expectedTarget: spec.target,
4425
+ state,
4426
+ ...actualTarget !== void 0 ? { actualTarget } : {}
4427
+ };
4428
+ });
4429
+ return {
4430
+ ...base,
4431
+ isAnchor: true,
4432
+ reachable: true,
4433
+ canonicalPresent: true,
4434
+ canonicalName: basename5(real),
4435
+ files: anchorFiles
4436
+ };
4316
4437
  }
4317
4438
  if (!existsSync(join9(real, ".git"))) {
4318
4439
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
@@ -4366,6 +4487,53 @@ function applySymlinkPlan(repositoryRoot, plan) {
4366
4487
  function failureReason(error) {
4367
4488
  return hasErrorCode(error) ? error.code : "unknown error";
4368
4489
  }
4490
+ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
4491
+ for (const entry of roster) {
4492
+ let name;
4493
+ try {
4494
+ name = basename5(realpathSync(resolve7(repositoryRoot, entry.path)));
4495
+ } catch {
4496
+ name = basename5(resolve7(repositoryRoot, entry.path));
4497
+ }
4498
+ if (name === viewName) return entry.path;
4499
+ }
4500
+ return void 0;
4501
+ }
4502
+ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
4503
+ const viewName = basename5(viewDir);
4504
+ const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
4505
+ if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
4506
+ const canonicalFile = canonicalFileFor(anchorReal, viewName);
4507
+ if (!existsSync(canonicalFile)) return { kind: "missing-canonical", viewName };
4508
+ const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
4509
+ (spec) => {
4510
+ const { state, actualTarget } = inspectSymlink(join9(viewDir, spec.name), spec.target);
4511
+ return {
4512
+ name: spec.name,
4513
+ expectedTarget: spec.target,
4514
+ state,
4515
+ ...actualTarget !== void 0 ? { actualTarget } : {}
4516
+ };
4517
+ }
4518
+ );
4519
+ return { kind: "gathered", viewName, files };
4520
+ }
4521
+ function applyViewSymlinks(viewDir, files) {
4522
+ const created = [];
4523
+ const failed = [];
4524
+ for (const f of files) {
4525
+ if (f.state !== "missing") continue;
4526
+ const filePath = join9(viewDir, f.name);
4527
+ try {
4528
+ mkdirSync(dirname3(filePath), { recursive: true });
4529
+ symlinkSync(f.expectedTarget, filePath);
4530
+ created.push(f.name);
4531
+ } catch (error) {
4532
+ failed.push({ file: f.name, message: failureReason(error) });
4533
+ }
4534
+ }
4535
+ return { created, failed };
4536
+ }
4369
4537
  async function doRunProjectSymlinks(options, ctx) {
4370
4538
  const cwd = ctx.cwd ?? process.cwd();
4371
4539
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
@@ -4385,11 +4553,33 @@ async function doRunProjectSymlinks(options, ctx) {
4385
4553
  for (const f of failed) failures.push({ repo: plan.path, file: f.file, message: f.message });
4386
4554
  }
4387
4555
  }
4556
+ let view;
4557
+ let viewCreated = [];
4558
+ const viewFailures = [];
4559
+ if (roster.length > 0) {
4560
+ const viewPath = manifest.workspace.view;
4561
+ if (viewPath === void 0) {
4562
+ view = { kind: "no-view" };
4563
+ } else {
4564
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
4565
+ view = gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir);
4566
+ if (options.apply === true && view.kind === "gathered") {
4567
+ const { created, failed } = applyViewSymlinks(viewDir, view.files);
4568
+ viewCreated = created;
4569
+ for (const f of failed) viewFailures.push(f);
4570
+ }
4571
+ }
4572
+ }
4573
+ const viewClean = view === void 0 || view.kind === "no-view" || view.kind === "gathered" && view.files.every((f) => f.state === "correct");
4388
4574
  const result = {
4389
4575
  ...summary,
4576
+ ok: summary.ok && viewClean,
4390
4577
  hasRoster: roster.length > 0,
4391
4578
  applied: createdCount > 0,
4392
- failures
4579
+ failures,
4580
+ ...view !== void 0 ? { view } : {},
4581
+ viewCreated,
4582
+ viewFailures
4393
4583
  };
4394
4584
  if (options.json === true) {
4395
4585
  console.log(JSON.stringify(result));
@@ -4437,7 +4627,7 @@ function renderProjectSymlinks(result) {
4437
4627
  );
4438
4628
  } else {
4439
4629
  lines.push(
4440
- "\u2139\uFE0F No symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos (see below)."
4630
+ "\u2139\uFE0F No repo symlink needs generating, but there are conflicts / collisions / a missing canonical / unreachable repos, or the workspace view's spokes need attention (see below)."
4441
4631
  );
4442
4632
  }
4443
4633
  lines.push("");
@@ -4486,11 +4676,61 @@ function renderProjectSymlinks(result) {
4486
4676
  for (const p of result.unreachable) lines.push(`- ${p}`);
4487
4677
  lines.push("");
4488
4678
  }
4679
+ appendViewSymlinksSection(lines, result);
4489
4680
  lines.push(
4490
4681
  "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)."
4491
4682
  );
4492
4683
  return lines.join("\n");
4493
4684
  }
4685
+ function appendViewSymlinksSection(lines, result) {
4686
+ const view = result.view;
4687
+ if (view === void 0 || view.kind === "no-view") return;
4688
+ lines.push("## Workspace view spokes (the view's own instruction files)");
4689
+ if (view.kind === "collision") {
4690
+ lines.push(
4691
+ `\u26A0\uFE0F ${view.viewName}: the view shares its canonical name with the roster repo \`${view.repoPath}\` \u2014 both would own \`agents/${view.viewName}/${CANONICAL_FILE}\`, so no view spoke is wired. Rename the view directory or the repo to disambiguate, then re-run.`
4692
+ );
4693
+ lines.push("");
4694
+ return;
4695
+ }
4696
+ if (view.kind === "missing-canonical") {
4697
+ lines.push(
4698
+ `\u2139\uFE0F ${view.viewName}: the view canonical \`agents/${view.viewName}/${CANONICAL_FILE}\` does not exist yet \u2014 run \`basou project preset --apply\` first (or \`basou project derive --apply\`), then re-run.`
4699
+ );
4700
+ lines.push("");
4701
+ return;
4702
+ }
4703
+ const failedFiles = new Set(result.viewFailures.map((f) => f.file));
4704
+ const missing = view.files.filter((f) => f.state === "missing");
4705
+ const attempted = result.viewCreated.length > 0 || result.viewFailures.length > 0;
4706
+ if (missing.length === 0) {
4707
+ lines.push(`\u2705 ${view.viewName}: the view's instruction spokes are correctly wired.`);
4708
+ } else if (!attempted) {
4709
+ lines.push(`${view.viewName}: view spokes to create (dry-run; pass --apply to write):`);
4710
+ for (const f of missing) lines.push(` ${f.name} -> ${f.expectedTarget}`);
4711
+ } else {
4712
+ lines.push(`${view.viewName}: view spokes created:`);
4713
+ for (const f of missing) {
4714
+ if (failedFiles.has(f.name)) continue;
4715
+ lines.push(` ${f.name} -> ${f.expectedTarget}`);
4716
+ }
4717
+ }
4718
+ if (result.viewFailures.length > 0) {
4719
+ lines.push(` Failed:`);
4720
+ for (const f of result.viewFailures) lines.push(` ${f.file}: ${f.message}`);
4721
+ }
4722
+ const conflicts = view.files.filter(
4723
+ (f) => f.state === "mismatch" || f.state === "occupied" || f.state === "blocked"
4724
+ );
4725
+ if (conflicts.length > 0) {
4726
+ lines.push(` Conflicts (left untouched):`);
4727
+ for (const f of conflicts) {
4728
+ const detail = f.state === "mismatch" ? `points elsewhere (currently: ${f.actualTarget ?? "?"})` : f.state === "occupied" ? "a real file/directory" : "an uninspectable path";
4729
+ lines.push(` ${f.name}: ${detail}`);
4730
+ }
4731
+ }
4732
+ lines.push("");
4733
+ }
4494
4734
  async function runProjectWorkspace(options, ctx = {}) {
4495
4735
  try {
4496
4736
  await doRunProjectWorkspace(options, ctx);
@@ -4881,6 +5121,86 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4881
5121
  ...section.kind === "ok" ? { currentBlock: section.generated } : {}
4882
5122
  };
4883
5123
  }
5124
+ function normalizeViewBlock(s) {
5125
+ return s.replace(/\r\n/g, "\n").replace(/\n+$/, "");
5126
+ }
5127
+ function viewPresetReposFor(repositoryRoot, roster) {
5128
+ let anchorReal;
5129
+ try {
5130
+ anchorReal = realpathSync(repositoryRoot);
5131
+ } catch {
5132
+ anchorReal = void 0;
5133
+ }
5134
+ return roster.map((entry) => {
5135
+ const abs = resolve7(repositoryRoot, entry.path);
5136
+ let real;
5137
+ try {
5138
+ real = realpathSync(abs);
5139
+ } catch {
5140
+ real = void 0;
5141
+ }
5142
+ const name = basename5(real ?? abs);
5143
+ const isAnchor = real !== void 0 && anchorReal !== void 0 ? real === anchorReal : abs === repositoryRoot;
5144
+ return {
5145
+ name,
5146
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
5147
+ ...entry.language !== void 0 ? { language: entry.language } : {},
5148
+ ...isAnchor ? { anchor: true } : instructionMode(entry) === "self" ? { self: true } : {}
5149
+ };
5150
+ });
5151
+ }
5152
+ function gatherViewPreset(repositoryRoot, anchorReal, viewName, roster) {
5153
+ const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
5154
+ if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
5155
+ const desiredBlock = renderViewPresetBlock({
5156
+ viewName,
5157
+ repos: viewPresetReposFor(repositoryRoot, roster)
5158
+ });
5159
+ const canonicalFile = canonicalFileFor(anchorReal, viewName);
5160
+ let content;
5161
+ try {
5162
+ content = readFileSync(canonicalFile, "utf8");
5163
+ } catch (error) {
5164
+ if (hasErrorCode(error) && error.code === "ENOENT") {
5165
+ return {
5166
+ kind: "plan",
5167
+ action: "create",
5168
+ canonicalName: viewName,
5169
+ viewName,
5170
+ block: desiredBlock
5171
+ };
5172
+ }
5173
+ return { kind: "unreadable", canonicalName: viewName, viewName };
5174
+ }
5175
+ const section = parseMarkers(content);
5176
+ if (section.kind === "ok") {
5177
+ if (normalizeViewBlock(section.generated) === normalizeViewBlock(desiredBlock)) {
5178
+ return { kind: "in-sync", canonicalName: viewName, viewName };
5179
+ }
5180
+ return {
5181
+ kind: "plan",
5182
+ action: "update",
5183
+ canonicalName: viewName,
5184
+ viewName,
5185
+ block: desiredBlock
5186
+ };
5187
+ }
5188
+ return { kind: "conflict", canonicalName: viewName, viewName, reason: section.kind };
5189
+ }
5190
+ async function applyViewPreset(anchorReal, outcome) {
5191
+ const file = canonicalFileFor(anchorReal, outcome.canonicalName);
5192
+ const label = canonicalLabelFor(outcome.canonicalName);
5193
+ let isLink = false;
5194
+ try {
5195
+ isLink = lstatSync(file).isSymbolicLink();
5196
+ } catch {
5197
+ isLink = false;
5198
+ }
5199
+ if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
5200
+ if (outcome.action === "create") mkdirSync(dirname3(file), { recursive: true });
5201
+ const existing = await readMarkdownFile4(file);
5202
+ await writeMarkdownFile5(file, renderWithMarkers4(existing, outcome.block, label));
5203
+ }
4884
5204
  async function applyPresetPlan(anchorReal, plan) {
4885
5205
  const file = canonicalFileFor(anchorReal, plan.canonicalName);
4886
5206
  const label = canonicalLabelFor(plan.canonicalName);
@@ -4913,7 +5233,12 @@ async function doRunProjectPreset(options, ctx) {
4913
5233
  const anchorReal = realpathSync(repositoryRoot);
4914
5234
  const facts = [];
4915
5235
  for (const entry of roster) facts.push(await gatherRepoPreset(repositoryRoot, anchorReal, entry));
4916
- const summary = summarizePresetPlan(facts);
5236
+ const viewPath = manifest.workspace.view;
5237
+ const viewCanonicalName = roster.length > 0 && viewPath !== void 0 ? basename5(resolveViewDir(repositoryRoot, viewPath)) : void 0;
5238
+ const summary = summarizePresetPlan(
5239
+ facts,
5240
+ viewCanonicalName !== void 0 ? { viewCanonicalName } : void 0
5241
+ );
4917
5242
  const failures = [];
4918
5243
  let writtenCount = 0;
4919
5244
  if (options.apply === true && summary.plans.length > 0) {
@@ -4926,11 +5251,30 @@ async function doRunProjectPreset(options, ctx) {
4926
5251
  }
4927
5252
  }
4928
5253
  }
5254
+ let view;
5255
+ let viewApplied = false;
5256
+ let viewFailure;
5257
+ if (roster.length > 0) {
5258
+ view = viewCanonicalName === void 0 ? { kind: "no-view" } : gatherViewPreset(repositoryRoot, anchorReal, viewCanonicalName, roster);
5259
+ if (options.apply === true && view.kind === "plan") {
5260
+ try {
5261
+ await applyViewPreset(anchorReal, view);
5262
+ viewApplied = true;
5263
+ } catch (error) {
5264
+ viewFailure = presetFailureReason(error);
5265
+ }
5266
+ }
5267
+ }
5268
+ const viewClean = view === void 0 || view.kind === "no-view" || view.kind === "in-sync";
4929
5269
  const result = {
4930
5270
  ...summary,
5271
+ ok: summary.ok && viewClean,
4931
5272
  hasRoster: roster.length > 0,
4932
5273
  applied: writtenCount > 0,
4933
- failures
5274
+ failures,
5275
+ ...view !== void 0 ? { view } : {},
5276
+ viewApplied,
5277
+ ...viewFailure !== void 0 ? { viewFailure } : {}
4934
5278
  };
4935
5279
  if (options.json === true) {
4936
5280
  console.log(JSON.stringify(result));
@@ -4983,7 +5327,7 @@ function renderProjectPreset(result) {
4983
5327
  );
4984
5328
  } else {
4985
5329
  lines.push(
4986
- "\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos (see below)."
5330
+ "\u2139\uFE0F No repo needs generating, but there are marker conflicts / collisions / undeclared / unreachable repos, or the workspace view canonical needs attention (see below)."
4987
5331
  );
4988
5332
  }
4989
5333
  lines.push("");
@@ -5020,10 +5364,11 @@ function renderProjectPreset(result) {
5020
5364
  }
5021
5365
  if (result.collisions.length > 0) {
5022
5366
  lines.push(
5023
- `## Canonical collisions (${result.collisions.length}) \u2014 another repo shares the same-named canonical (not auto-generated)`
5367
+ `## Canonical collisions (${result.collisions.length}) \u2014 another repo (or the workspace view) shares the same-named canonical (not auto-generated)`
5024
5368
  );
5025
5369
  for (const c of result.collisions) {
5026
- lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}`);
5370
+ const suffix = c.view === true ? " + the workspace view (rename the view directory or the repo to disambiguate)" : "";
5371
+ lines.push(`- agents/${c.canonicalName}/AGENTS.md \u2190 ${c.repos.join(", ")}${suffix}`);
5027
5372
  }
5028
5373
  lines.push("");
5029
5374
  }
@@ -5053,11 +5398,46 @@ function renderProjectPreset(result) {
5053
5398
  for (const p of result.unreachable) lines.push(`- ${p}`);
5054
5399
  lines.push("");
5055
5400
  }
5401
+ appendViewPresetSection(lines, result);
5056
5402
  lines.push(
5057
5403
  "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."
5058
5404
  );
5059
5405
  return lines.join("\n");
5060
5406
  }
5407
+ function appendViewPresetSection(lines, result) {
5408
+ const view = result.view;
5409
+ if (view === void 0 || view.kind === "no-view") return;
5410
+ const canonical2 = canonicalLabelFor(view.viewName);
5411
+ lines.push("## Workspace view canonical (the view's own AGENTS.md)");
5412
+ if (view.kind === "collision") {
5413
+ lines.push(
5414
+ `\u26A0\uFE0F ${view.viewName}: shares its canonical name with the roster repo \`${view.repoPath}\` \u2014 both would own ${canonical2}, so neither side is generated. Rename the view directory or the repo to disambiguate, then re-run.`
5415
+ );
5416
+ } else if (view.kind === "in-sync") {
5417
+ lines.push(`\u2705 ${view.viewName}: in sync with ${canonical2} (nothing to generate).`);
5418
+ } else if (view.kind === "plan") {
5419
+ if (result.viewApplied) {
5420
+ lines.push(`\u2705 ${view.viewName} [${view.action}] \u2192 ${canonical2}`);
5421
+ } else if (result.viewFailure !== void 0) {
5422
+ lines.push(`- ${view.viewName} [${view.action}] \u2192 ${canonical2}: ${result.viewFailure}`);
5423
+ } else {
5424
+ lines.push(
5425
+ `- ${view.viewName} [${view.action}] \u2192 ${canonical2} (dry-run; pass --apply to write):`
5426
+ );
5427
+ for (const bl of view.block.split("\n")) lines.push(` ${bl}`);
5428
+ }
5429
+ } else if (view.kind === "conflict") {
5430
+ const detail = view.reason === "no_markers" ? "no marker region" : `malformed markers (${view.reason})`;
5431
+ lines.push(
5432
+ `\u26A0\uFE0F ${view.viewName}: ${canonical2} has ${detail}, so it is not overwritten. Add \`${GENERATED_START}\` / \`${GENERATED_END}\` where the block should go (or run \`basou project retrofit\` to prepend it, preserving your prose).`
5433
+ );
5434
+ } else {
5435
+ lines.push(
5436
+ `\u26A0\uFE0F ${view.viewName}: ${canonical2} could not be read (a directory, permissions, etc.). Resolve it by hand, then re-run.`
5437
+ );
5438
+ }
5439
+ lines.push("");
5440
+ }
5061
5441
  async function runProjectArchive(target, options, ctx = {}) {
5062
5442
  try {
5063
5443
  await doRunProjectArchive(target, options, ctx);
@@ -5840,10 +6220,21 @@ async function resolveRepositoryRootForNew(cwd) {
5840
6220
  throw error;
5841
6221
  }
5842
6222
  }
6223
+ function validateProjectName(name) {
6224
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
6225
+ throw new Error(
6226
+ `Invalid --project-name "${name}": use a simple name (letters, digits, '.', '-', '_'; starting with a letter or digit; no spaces or path separators). For a custom view path, use --view.`
6227
+ );
6228
+ }
6229
+ return name;
6230
+ }
5843
6231
  async function doRunProjectNew(repos, options, ctx) {
5844
6232
  const cwd = ctx.cwd ?? process.cwd();
5845
6233
  const repositoryRoot = await resolveRepositoryRootForNew(cwd);
5846
6234
  const workspaceName = basename5(repositoryRoot);
6235
+ const productName = options.projectName !== void 0 ? validateProjectName(options.projectName) : void 0;
6236
+ const viewStem = productName ?? workspaceName;
6237
+ const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
5847
6238
  const declared = repos.map((p) => {
5848
6239
  const abs = resolve7(cwd, p);
5849
6240
  let real;
@@ -5868,11 +6259,15 @@ async function doRunProjectNew(repos, options, ctx) {
5868
6259
  if (rel !== "." && !rosterPaths.includes(rel)) rosterPaths.push(rel);
5869
6260
  }
5870
6261
  const roster = rosterPaths.map((path) => ({ path }));
5871
- const viewPath = options.view === false ? null : options.view ?? `../${workspaceName}-workspace`;
6262
+ const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
5872
6263
  const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
5873
6264
  const paths = basouPaths10(repositoryRoot);
5874
6265
  const existed = existsSync(paths.files.manifest);
5875
- const manifest = createManifest2({ workspaceName, sourceRoots });
6266
+ const manifest = createManifest2({
6267
+ workspaceName,
6268
+ sourceRoots,
6269
+ ...productName !== void 0 ? { projectName: productName } : {}
6270
+ });
5876
6271
  manifest.repos = roster;
5877
6272
  if (viewPath !== null) manifest.workspace.view = viewPath;
5878
6273
  let applied = false;
@@ -5888,8 +6283,10 @@ async function doRunProjectNew(repos, options, ctx) {
5888
6283
  }
5889
6284
  const result = {
5890
6285
  workspaceName,
6286
+ projectName: productName ?? null,
5891
6287
  repos: roster,
5892
6288
  view: viewPath,
6289
+ viewOverridesProjectName,
5893
6290
  sourceRoots,
5894
6291
  invalidRepos: [],
5895
6292
  existed,
@@ -5922,11 +6319,12 @@ function renderProjectNew(result) {
5922
6319
  );
5923
6320
  lines.push("");
5924
6321
  }
6322
+ const identity = result.projectName ?? result.workspaceName;
5925
6323
  if (result.applied) {
5926
- lines.push(`\u2705 Created \`.basou/\` for \`${result.workspaceName}\` and seeded the manifest:`);
6324
+ lines.push(`\u2705 Created \`.basou/\` for \`${identity}\` and seeded the manifest:`);
5927
6325
  } else {
5928
6326
  lines.push(
5929
- `Will create \`.basou/\` for \`${result.workspaceName}\` and seed the manifest (dry-run; pass --apply to write):`
6327
+ `Will create \`.basou/\` for \`${identity}\` and seed the manifest (dry-run; pass --apply to write):`
5930
6328
  );
5931
6329
  }
5932
6330
  lines.push("");
@@ -5935,9 +6333,16 @@ function renderProjectNew(result) {
5935
6333
  lines.push(`- ${r.path}${r.path === "." ? " (anchor)" : ""}`);
5936
6334
  }
5937
6335
  lines.push("");
6336
+ if (result.projectName !== null) {
6337
+ lines.push(`project name: ${result.projectName}`);
6338
+ lines.push("");
6339
+ }
5938
6340
  lines.push(
5939
6341
  result.view !== null ? `workspace view: ${result.view}` : "workspace view: none (solo project)"
5940
6342
  );
6343
+ if (result.viewOverridesProjectName) {
6344
+ lines.push(" (note: --view set this path, so --project-name did not drive the view name)");
6345
+ }
5941
6346
  lines.push("");
5942
6347
  lines.push(`source_roots (${result.sourceRoots.length}):`);
5943
6348
  for (const s of result.sourceRoots) lines.push(`- ${s}`);
@@ -5983,25 +6388,79 @@ async function doRunProjectDerive(options, ctx) {
5983
6388
  const stepOpts = { apply };
5984
6389
  console.log("# Generate project wiring in one pass (declaration \u2192 wiring)");
5985
6390
  console.log("");
5986
- console.log("## 1/5 sync source_roots (roster \u2192 capture config)");
6391
+ console.log("## 1/6 sync source_roots (roster \u2192 capture config)");
5987
6392
  await doRunProjectSync(stepOpts, stepCtx);
5988
6393
  console.log("");
5989
- console.log("## 2/5 generate instruction-file A preset (declaration \u2192 canonical)");
6394
+ console.log("## 2/6 generate instruction-file A preset (declaration \u2192 canonical)");
5990
6395
  await doRunProjectPreset(stepOpts, stepCtx);
5991
6396
  console.log("");
5992
- console.log("## 3/5 generate instruction-file symlinks (each repo \u2192 canonical)");
6397
+ console.log("## 3/6 seed the anchor's own AGENTS.md (greenfield only; create-only)");
6398
+ await doRunProjectSeedAnchor(stepOpts, stepCtx);
6399
+ console.log("");
6400
+ console.log("## 4/6 generate instruction-file symlinks (each repo \u2192 canonical)");
5993
6401
  await doRunProjectSymlinks(stepOpts, stepCtx);
5994
6402
  console.log("");
5995
- console.log("## 4/5 generate workspace view (aggregate the roster repos)");
6403
+ console.log("## 5/6 generate workspace view (aggregate the roster repos)");
5996
6404
  await doRunProjectWorkspace(stepOpts, stepCtx);
5997
6405
  console.log("");
5998
- console.log("## 5/5 generate .gitignore (exclude public repos' instruction files)");
6406
+ console.log("## 6/6 generate .gitignore (exclude public repos' instruction files)");
5999
6407
  await doRunProjectGitignore(stepOpts, stepCtx);
6000
6408
  console.log("");
6001
6409
  console.log(
6002
6410
  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."
6003
6411
  );
6004
6412
  }
6413
+ async function doRunProjectSeedAnchor(options, ctx) {
6414
+ const cwd = ctx.cwd ?? process.cwd();
6415
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
6416
+ const paths = basouPaths10(repositoryRoot);
6417
+ const manifest = await readManifest6(paths);
6418
+ const roster = manifest.repos ?? [];
6419
+ console.log("# Anchor instruction-file seed (the planning master's own AGENTS.md)");
6420
+ console.log("");
6421
+ if (roster.length === 0) {
6422
+ console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
6423
+ return;
6424
+ }
6425
+ const anchorDoc = join9(repositoryRoot, CANONICAL_FILE);
6426
+ if (pathPresent(anchorDoc)) {
6427
+ console.log(
6428
+ `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
6429
+ );
6430
+ return;
6431
+ }
6432
+ const viewPath = manifest.workspace.view;
6433
+ const viewName = viewPath !== void 0 ? basename5(resolveViewDir(repositoryRoot, viewPath)) : void 0;
6434
+ const repos = viewPresetReposFor(repositoryRoot, roster);
6435
+ const content = renderAnchorStarter({
6436
+ anchorName: basename5(repositoryRoot),
6437
+ ...manifest.project?.name !== void 0 ? { projectName: manifest.project.name } : {},
6438
+ ...viewName !== void 0 ? { viewName } : {},
6439
+ repos
6440
+ });
6441
+ if (options.apply !== true) {
6442
+ console.log(
6443
+ `- ${CANONICAL_FILE} [create] \u2192 the anchor's own AGENTS.md (dry-run; pass --apply to write). A create-only starter; hand-maintain it afterward \u2014 basou never rewrites it.`
6444
+ );
6445
+ return;
6446
+ }
6447
+ try {
6448
+ writeFileSync(anchorDoc, content, { flag: "wx" });
6449
+ console.log(
6450
+ `\u2705 Seeded the anchor's own \`${CANONICAL_FILE}\` (create-only starter \u2014 hand-maintain it from here; basou never rewrites it).`
6451
+ );
6452
+ } catch (error) {
6453
+ if (hasErrorCode(error) && error.code === "EEXIST") {
6454
+ console.log(
6455
+ `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
6456
+ );
6457
+ return;
6458
+ }
6459
+ console.log(
6460
+ `\u26A0\uFE0F Could not seed the anchor's \`${CANONICAL_FILE}\`: ${presetFailureReason(error)}`
6461
+ );
6462
+ }
6463
+ }
6005
6464
  async function runProjectRetrofit(repo, options, ctx = {}) {
6006
6465
  try {
6007
6466
  await doRunProjectRetrofit(repo, options, ctx);
@@ -6040,7 +6499,7 @@ function pathPresent(p) {
6040
6499
  return false;
6041
6500
  }
6042
6501
  }
6043
- function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
6502
+ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal, viewCanonicalName) {
6044
6503
  const declaredEntry = roster.find((entry) => {
6045
6504
  const entryAbs = resolve7(repositoryRoot, entry.path);
6046
6505
  if (argReal !== void 0) {
@@ -6064,6 +6523,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
6064
6523
  isAnchor: false,
6065
6524
  reachable: false,
6066
6525
  canonicalName,
6526
+ ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
6067
6527
  agentsState: "absent",
6068
6528
  canonicalExists: false,
6069
6529
  regularSpokes: []
@@ -6079,6 +6539,7 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
6079
6539
  isAnchor,
6080
6540
  reachable,
6081
6541
  canonicalName,
6542
+ ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
6082
6543
  agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
6083
6544
  canonicalExists: pathPresent(canonicalFile),
6084
6545
  regularSpokes: regularFileSpokes(argReal)
@@ -6105,6 +6566,44 @@ function relocateAgentsFile(repoReal, canonicalFile) {
6105
6566
  return { ok: false, message: failureReason(error), partial: true };
6106
6567
  }
6107
6568
  }
6569
+ function gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster) {
6570
+ if (viewPath === void 0) return { kind: "no-view" };
6571
+ const viewDir = resolveViewDir(repositoryRoot, viewPath);
6572
+ const viewName = basename5(viewDir);
6573
+ const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
6574
+ if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
6575
+ const canonicalFile = canonicalFileFor(anchorReal, viewName);
6576
+ let content;
6577
+ try {
6578
+ content = readFileSync(canonicalFile, "utf8");
6579
+ } catch (error) {
6580
+ if (hasErrorCode(error) && error.code === "ENOENT") return { kind: "absent", viewName };
6581
+ return { kind: "unreadable", viewName };
6582
+ }
6583
+ const section = parseMarkers(content);
6584
+ if (section.kind === "ok") return { kind: "already-marked", viewName };
6585
+ if (section.kind === "no_markers") {
6586
+ const block = renderViewPresetBlock({
6587
+ viewName,
6588
+ repos: viewPresetReposFor(repositoryRoot, roster)
6589
+ });
6590
+ return { kind: "seed", viewName, block };
6591
+ }
6592
+ return { kind: "malformed", viewName, reason: section.kind };
6593
+ }
6594
+ async function applyViewRetrofit(anchorReal, outcome) {
6595
+ const file = canonicalFileFor(anchorReal, outcome.viewName);
6596
+ const label = canonicalLabelFor(outcome.viewName);
6597
+ let isLink = false;
6598
+ try {
6599
+ isLink = lstatSync(file).isSymbolicLink();
6600
+ } catch {
6601
+ isLink = false;
6602
+ }
6603
+ if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
6604
+ const existing = await readMarkdownFile4(file);
6605
+ await writeMarkdownFile5(file, seedMarkers(existing, outcome.block, label));
6606
+ }
6108
6607
  async function doRunProjectRetrofit(repo, options, ctx) {
6109
6608
  const cwd = ctx.cwd ?? process.cwd();
6110
6609
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
@@ -6112,6 +6611,36 @@ async function doRunProjectRetrofit(repo, options, ctx) {
6112
6611
  const manifest = await readManifest6(paths);
6113
6612
  const roster = manifest.repos ?? [];
6114
6613
  const anchorReal = realpathSync(repositoryRoot);
6614
+ const viewPath = manifest.workspace.view;
6615
+ if (repo === void 0) {
6616
+ let view2;
6617
+ let viewApplied = false;
6618
+ let viewFailure;
6619
+ if (roster.length > 0) {
6620
+ view2 = gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster);
6621
+ if (options.apply === true && view2.kind === "seed") {
6622
+ try {
6623
+ await applyViewRetrofit(anchorReal, view2);
6624
+ viewApplied = true;
6625
+ } catch (error) {
6626
+ viewFailure = presetFailureReason(error);
6627
+ }
6628
+ }
6629
+ }
6630
+ const result2 = {
6631
+ kind: "view-only",
6632
+ hasRoster: roster.length > 0,
6633
+ ...view2 !== void 0 ? { view: view2 } : {},
6634
+ viewApplied,
6635
+ ...viewFailure !== void 0 ? { viewFailure } : {}
6636
+ };
6637
+ if (options.json === true) {
6638
+ console.log(JSON.stringify(result2));
6639
+ } else {
6640
+ console.log(renderProjectRetrofit(result2));
6641
+ }
6642
+ return result2;
6643
+ }
6115
6644
  const argAbs = resolve7(repositoryRoot, repo);
6116
6645
  let argReal;
6117
6646
  try {
@@ -6119,7 +6648,16 @@ async function doRunProjectRetrofit(repo, options, ctx) {
6119
6648
  } catch {
6120
6649
  argReal = void 0;
6121
6650
  }
6122
- const facts = gatherRetrofit(repositoryRoot, anchorReal, roster, repo, argAbs, argReal);
6651
+ const viewCanonicalName = viewPath !== void 0 ? basename5(resolveViewDir(repositoryRoot, viewPath)) : void 0;
6652
+ const facts = gatherRetrofit(
6653
+ repositoryRoot,
6654
+ anchorReal,
6655
+ roster,
6656
+ repo,
6657
+ argAbs,
6658
+ argReal,
6659
+ viewCanonicalName
6660
+ );
6123
6661
  const plan = classifyRetrofit(facts);
6124
6662
  let applied = false;
6125
6663
  let failure;
@@ -6134,12 +6672,19 @@ async function doRunProjectRetrofit(repo, options, ctx) {
6134
6672
  partial = res.partial;
6135
6673
  }
6136
6674
  }
6675
+ let view;
6676
+ if (roster.length > 0) {
6677
+ view = gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster);
6678
+ }
6137
6679
  const result = {
6680
+ kind: "repo",
6138
6681
  ...plan,
6139
6682
  hasRoster: roster.length > 0,
6140
6683
  applied,
6141
6684
  ...failure !== void 0 ? { failure } : {},
6142
- ...partial ? { partial } : {}
6685
+ ...partial ? { partial } : {},
6686
+ ...view !== void 0 ? { view } : {},
6687
+ viewApplied: false
6143
6688
  };
6144
6689
  if (options.json === true) {
6145
6690
  console.log(JSON.stringify(result));
@@ -6148,6 +6693,47 @@ async function doRunProjectRetrofit(repo, options, ctx) {
6148
6693
  }
6149
6694
  return result;
6150
6695
  }
6696
+ var MARKER_PORTABILITY_NOTE = "The marker pair can later be moved anywhere in the file by hand \u2014 `basou project preset` rewrites only the region between the markers.";
6697
+ function appendViewRetrofitSection(lines, result) {
6698
+ const view = result.view;
6699
+ if (view === void 0 || view.kind === "no-view" || view.kind === "absent" || view.kind === "already-marked")
6700
+ return;
6701
+ const canonical2 = `agents/${view.viewName}/${CANONICAL_FILE}`;
6702
+ lines.push("## Workspace view canonical (auto-migrate the view's own AGENTS.md)");
6703
+ if (view.kind === "seed") {
6704
+ if (result.kind === "repo") {
6705
+ lines.push(
6706
+ `\`${canonical2}\` is markerless prose. Run \`basou project retrofit\` (no repo argument) to prepend the generated block, preserving the prose.`
6707
+ );
6708
+ lines.push(MARKER_PORTABILITY_NOTE);
6709
+ } else if (result.viewApplied) {
6710
+ lines.push(
6711
+ `\u2705 Prepended the generated block into \`${canonical2}\` (your hand-written prose is preserved below it).`
6712
+ );
6713
+ lines.push(MARKER_PORTABILITY_NOTE);
6714
+ } else if (result.viewFailure !== void 0) {
6715
+ lines.push(`\u26A0\uFE0F Could not seed \`${canonical2}\`: ${result.viewFailure}. Nothing was changed.`);
6716
+ } else {
6717
+ lines.push(
6718
+ `\`${canonical2}\` is markerless prose. Retrofit will prepend the generated block, preserving the prose (dry-run; pass --apply to write).`
6719
+ );
6720
+ lines.push(MARKER_PORTABILITY_NOTE);
6721
+ }
6722
+ } else if (view.kind === "collision") {
6723
+ lines.push(
6724
+ `\u26A0\uFE0F The view shares its canonical name with the roster repo \`${view.repoPath}\` \u2014 both would own \`${canonical2}\`, so nothing is migrated. Rename the view directory or the repo to disambiguate, then re-run.`
6725
+ );
6726
+ } else if (view.kind === "malformed") {
6727
+ lines.push(
6728
+ `\u26A0\uFE0F \`${canonical2}\` has malformed markers (${view.reason}), so it is not rewritten. Fix the \`${GENERATED_START}\` / \`${GENERATED_END}\` pair by hand, then re-run.`
6729
+ );
6730
+ } else {
6731
+ lines.push(
6732
+ `\u26A0\uFE0F \`${canonical2}\` could not be read (a directory, permissions, etc.). Resolve it by hand, then re-run.`
6733
+ );
6734
+ }
6735
+ lines.push("");
6736
+ }
6151
6737
  function appendSpokeChecklist(lines, spokes) {
6152
6738
  if (spokes.length === 0) return;
6153
6739
  lines.push(
@@ -6172,6 +6758,25 @@ function renderProjectRetrofit(result) {
6172
6758
  );
6173
6759
  return lines.join("\n");
6174
6760
  }
6761
+ if (result.kind === "view-only") {
6762
+ const view = result.view;
6763
+ if (view === void 0 || view.kind === "no-view") {
6764
+ lines.push(
6765
+ "\u2139\uFE0F No `workspace.view` declared \u2014 there is no view canonical to migrate. Pass a repo argument to relocate a repo's AGENTS.md instead."
6766
+ );
6767
+ } else if (view.kind === "absent") {
6768
+ lines.push(
6769
+ `\u2139\uFE0F The view canonical \`agents/${view.viewName}/${CANONICAL_FILE}\` does not exist yet \u2014 run \`basou project preset --apply\` (or \`basou project derive --apply\`) to create it; there is nothing to migrate.`
6770
+ );
6771
+ } else if (view.kind === "already-marked") {
6772
+ lines.push(
6773
+ `\u2705 \`agents/${view.viewName}/${CANONICAL_FILE}\` already has a BASOU:GENERATED region. Nothing to migrate.`
6774
+ );
6775
+ } else {
6776
+ appendViewRetrofitSection(lines, result);
6777
+ }
6778
+ return lines.join("\n").trimEnd();
6779
+ }
6175
6780
  const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
6176
6781
  if (result.action === "refuse") {
6177
6782
  if (result.reason === "not-declared") {
@@ -6194,12 +6799,18 @@ function renderProjectRetrofit(result) {
6194
6799
  lines.push(
6195
6800
  `\u26A0\uFE0F \`${result.path}/${CANONICAL_FILE}\` could not be inspected (a parent component is not a directory, a permission error, or the path is neither a regular file nor a symlink). Resolve it by hand, then re-run.`
6196
6801
  );
6802
+ } else if (result.reason === "view-collision") {
6803
+ lines.push(
6804
+ `\u26A0\uFE0F \`${result.path}\` shares its canonical name with the workspace view \u2014 \`${canonical2}\` would be owned by both, so relocating would corrupt one with the other. Rename the view directory or the repo to disambiguate, then re-run.`
6805
+ );
6197
6806
  } else if (result.reason === "canonical-exists") {
6198
6807
  lines.push(
6199
6808
  `\u26A0\uFE0F The destination canonical \`${canonical2}\` already exists. Not relocating, to avoid clobbering it. If the canonical is the source of truth, the repo's AGENTS.md is redundant (remove it, then run \`basou project symlinks\`); otherwise reconcile the two by hand.`
6200
6809
  );
6201
6810
  }
6202
- return lines.join("\n");
6811
+ lines.push("");
6812
+ appendViewRetrofitSection(lines, result);
6813
+ return lines.join("\n").trimEnd();
6203
6814
  }
6204
6815
  if (result.action === "skip") {
6205
6816
  if (result.reason === "already-symlink") {
@@ -6213,6 +6824,7 @@ function renderProjectRetrofit(result) {
6213
6824
  }
6214
6825
  lines.push("");
6215
6826
  appendSpokeChecklist(lines, result.regularSpokes);
6827
+ appendViewRetrofitSection(lines, result);
6216
6828
  return lines.join("\n").trimEnd();
6217
6829
  }
6218
6830
  if (result.failure !== void 0) {
@@ -6226,7 +6838,9 @@ function renderProjectRetrofit(result) {
6226
6838
  } else {
6227
6839
  lines.push("Nothing was changed. Resolve the cause and re-run.");
6228
6840
  }
6229
- return lines.join("\n");
6841
+ lines.push("");
6842
+ appendViewRetrofitSection(lines, result);
6843
+ return lines.join("\n").trimEnd();
6230
6844
  }
6231
6845
  if (result.applied) {
6232
6846
  lines.push(
@@ -6241,6 +6855,7 @@ function renderProjectRetrofit(result) {
6241
6855
  }
6242
6856
  lines.push("");
6243
6857
  appendSpokeChecklist(lines, result.regularSpokes);
6858
+ appendViewRetrofitSection(lines, result);
6244
6859
  lines.push(
6245
6860
  result.applied ? "Next: run `basou project derive --apply` to add the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore." : "After applying, run `basou project derive --apply` to finish the wiring (preset block, CLAUDE.md / Copilot spokes, .gitignore)."
6246
6861
  );
@@ -6707,19 +7322,19 @@ function parseInterval(value) {
6707
7322
  return seconds;
6708
7323
  }
6709
7324
  function abortableSleep(ms, signal) {
6710
- return new Promise((resolve13) => {
7325
+ return new Promise((resolve14) => {
6711
7326
  if (signal.aborted) {
6712
- resolve13();
7327
+ resolve14();
6713
7328
  return;
6714
7329
  }
6715
7330
  let timer;
6716
7331
  const onAbort = () => {
6717
7332
  clearTimeout(timer);
6718
- resolve13();
7333
+ resolve14();
6719
7334
  };
6720
7335
  timer = setTimeout(() => {
6721
7336
  signal.removeEventListener("abort", onAbort);
6722
- resolve13();
7337
+ resolve14();
6723
7338
  }, ms);
6724
7339
  signal.addEventListener("abort", onAbort, { once: true });
6725
7340
  });
@@ -8606,6 +9221,8 @@ async function doRunStatus(options, ctx) {
8606
9221
  if (findErrorCode14(error, "ENOENT")) {
8607
9222
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8608
9223
  }
9224
+ const gateMessage = formatVersionGateMessage(error);
9225
+ if (gateMessage !== void 0) throw new Error(gateMessage, { cause: error });
8609
9226
  throw new Error("Failed to read workspace manifest", { cause: error });
8610
9227
  }
8611
9228
  const snapshot = await buildStatusSnapshot({ manifest, paths });
@@ -8637,6 +9254,18 @@ async function resolveRepositoryRootForStatus(cwd) {
8637
9254
  throw error;
8638
9255
  }
8639
9256
  }
9257
+ function formatVersionGateMessage(error) {
9258
+ const issues = error.issues;
9259
+ if (!Array.isArray(issues)) return void 0;
9260
+ for (const issue of issues) {
9261
+ const path = issue.path;
9262
+ const message = issue.message;
9263
+ if (Array.isArray(path) && (path.includes("schema_version") || path.includes("basou_version")) && typeof message === "string" && message.startsWith("unsupported .basou format version")) {
9264
+ return message;
9265
+ }
9266
+ }
9267
+ return void 0;
9268
+ }
8640
9269
 
8641
9270
  // src/commands/task.ts
8642
9271
  import { readFile as readFile7 } from "fs/promises";
@@ -9859,7 +10488,7 @@ async function assertWorkspaceInitialized14(basouRoot) {
9859
10488
  // src/commands/view.ts
9860
10489
  import { spawn } from "child_process";
9861
10490
  import { createHash } from "crypto";
9862
- import { basename as basename7, resolve as resolve12 } from "path";
10491
+ import { basename as basename8, resolve as resolve13 } from "path";
9863
10492
  import {
9864
10493
  assertBasouRootSafe as assertBasouRootSafe18,
9865
10494
  basouPaths as basouPaths21,
@@ -9997,7 +10626,7 @@ function formatSafetyReport(result) {
9997
10626
 
9998
10627
  // src/lib/view-server.ts
9999
10628
  import { createServer } from "http";
10000
- import { join as join17 } from "path";
10629
+ import { basename as basename7, join as join17, resolve as resolve12 } from "path";
10001
10630
  import {
10002
10631
  computeWorkStats as computeWorkStats2,
10003
10632
  enumerateApprovals as enumerateApprovals2,
@@ -10013,9 +10642,42 @@ import {
10013
10642
  readTaskFile as readTaskFile2,
10014
10643
  renderDecisions as renderDecisions3,
10015
10644
  renderHandoff as renderHandoff3,
10016
- summarizeOrientation
10645
+ summarizeOrientation,
10646
+ tryRemoteUrl
10017
10647
  } from "@basou/core";
10018
10648
 
10649
+ // src/lib/repo-url.ts
10650
+ function toBrowserUrl(remote) {
10651
+ const raw = remote.trim();
10652
+ if (raw.length === 0) return null;
10653
+ let host;
10654
+ let path;
10655
+ if (raw.includes("://")) {
10656
+ let parsed;
10657
+ try {
10658
+ parsed = new URL(raw);
10659
+ } catch {
10660
+ return null;
10661
+ }
10662
+ const scheme = parsed.protocol;
10663
+ if (scheme !== "ssh:" && scheme !== "git:" && scheme !== "http:" && scheme !== "https:") {
10664
+ return null;
10665
+ }
10666
+ host = scheme === "http:" || scheme === "https:" ? parsed.host : parsed.hostname;
10667
+ path = parsed.pathname;
10668
+ } else {
10669
+ const match = /^[^@/\s]+@([^:/\s@]+):(.+)$/.exec(raw);
10670
+ if (match === null || match[1] === void 0 || match[2] === void 0) return null;
10671
+ host = match[1];
10672
+ path = match[2];
10673
+ }
10674
+ const cleanPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
10675
+ if (host.length === 0 || cleanPath.length === 0) return null;
10676
+ if (/\s/.test(host) || /\s/.test(cleanPath)) return null;
10677
+ if (cleanPath.split("/").some((seg) => seg === "." || seg === "..")) return null;
10678
+ return `https://${host}/${cleanPath}`;
10679
+ }
10680
+
10019
10681
  // src/lib/view-ui.ts
10020
10682
  var VIEW_HTML = `<!doctype html>
10021
10683
  <html lang="en">
@@ -10417,9 +11079,30 @@ var VIEW_HTML = `<!doctype html>
10417
11079
  card(c.approvalsPending, 'approvals pending')
10418
11080
  ]);
10419
11081
  detail.appendChild(cards);
11082
+ renderRepos(detail, d.repos || []);
10420
11083
  detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
10421
11084
  }).catch(fail);
10422
11085
  }
11086
+ // The declared roster repos, each with a LIVE git link (derived server-side
11087
+ // from the repo's local git config at request time, never stored). A repo
11088
+ // with no remote renders as "local only". Links open in a new tab.
11089
+ function renderRepos(detail, repos) {
11090
+ if (!repos.length) return;
11091
+ detail.appendChild(el('h3', { text: 'Repos' }));
11092
+ var rows = el('div', { class: 'repos' }, []);
11093
+ repos.forEach(function (r) {
11094
+ var vis = r.visibility ? (' (' + r.visibility + ')') : '';
11095
+ var link = r.url
11096
+ ? el('a', { href: r.url, target: '_blank', rel: 'noopener noreferrer', text: r.url })
11097
+ : el('span', { class: 'muted', text: 'local only' });
11098
+ rows.appendChild(el('div', { class: 'f' }, [
11099
+ el('strong', { text: r.name }),
11100
+ el('span', { class: 'muted', text: vis + ' ' }),
11101
+ link
11102
+ ]));
11103
+ });
11104
+ detail.appendChild(rows);
11105
+ }
10423
11106
  function card(n, label) {
10424
11107
  return el('div', { class: 'card' }, [
10425
11108
  el('div', { class: 'n', text: String(n) }),
@@ -10658,7 +11341,7 @@ function startViewServer(opts) {
10658
11341
  };
10659
11342
  let boundPort = port;
10660
11343
  const getPort = () => boundPort;
10661
- return new Promise((resolve13, reject) => {
11344
+ return new Promise((resolve14, reject) => {
10662
11345
  const server = createServer((req, res) => {
10663
11346
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
10664
11347
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -10669,7 +11352,7 @@ function startViewServer(opts) {
10669
11352
  const address = server.address();
10670
11353
  boundPort = isAddressInfo(address) ? address.port : port;
10671
11354
  server.off("error", reject);
10672
- resolve13({
11355
+ resolve14({
10673
11356
  url: `http://${host}:${boundPort}`,
10674
11357
  port: boundPort,
10675
11358
  close: () => closeServer(server)
@@ -10681,8 +11364,8 @@ function isAddressInfo(value) {
10681
11364
  return value !== null && typeof value === "object";
10682
11365
  }
10683
11366
  function closeServer(server) {
10684
- return new Promise((resolve13) => {
10685
- server.close(() => resolve13());
11367
+ return new Promise((resolve14) => {
11368
+ server.close(() => resolve14());
10686
11369
  server.closeAllConnections();
10687
11370
  });
10688
11371
  }
@@ -10725,20 +11408,29 @@ async function handleGet(res, pathname, deps) {
10725
11408
  sendError(res, 404, "Unknown workspace");
10726
11409
  return;
10727
11410
  }
10728
- if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
11411
+ if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider, remoteUrlOf(deps))) {
10729
11412
  sendError(res, 404, "Not found");
10730
11413
  }
10731
11414
  return;
10732
11415
  }
10733
11416
  if (pathname.startsWith(API_PREFIX)) {
10734
11417
  const sub = pathname.slice(API_PREFIX.length);
10735
- if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
11418
+ if (!await handleWorkspaceGet(
11419
+ res,
11420
+ sub,
11421
+ primaryWorkspace(deps),
11422
+ deps.nowProvider,
11423
+ remoteUrlOf(deps)
11424
+ )) {
10736
11425
  sendError(res, 404, "Not found");
10737
11426
  }
10738
11427
  return;
10739
11428
  }
10740
11429
  sendError(res, 404, "Not found");
10741
11430
  }
11431
+ function remoteUrlOf(deps) {
11432
+ return deps.remoteUrlOf ?? tryRemoteUrl;
11433
+ }
10742
11434
  async function handlePost(res, pathname, body, deps, runExclusive) {
10743
11435
  const scoped = matchWsRoute(pathname);
10744
11436
  if (scoped !== null) {
@@ -10761,9 +11453,9 @@ async function handlePost(res, pathname, body, deps, runExclusive) {
10761
11453
  }
10762
11454
  sendError(res, 404, "Not found");
10763
11455
  }
10764
- async function handleWorkspaceGet(res, sub, ws, nowProvider) {
11456
+ async function handleWorkspaceGet(res, sub, ws, nowProvider, resolveRemoteUrl) {
10765
11457
  if (sub === "overview") {
10766
- sendJson(res, 200, await overview(ws, nowProvider));
11458
+ sendJson(res, 200, await overview(ws, nowProvider, resolveRemoteUrl));
10767
11459
  return true;
10768
11460
  }
10769
11461
  if (sub === "sessions") {
@@ -10896,7 +11588,7 @@ async function captureStaleness(ws, nowIso) {
10896
11588
  const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
10897
11589
  return probe === null ? { checked: false } : { checked: true, ...probe };
10898
11590
  }
10899
- async function overview(ws, nowProvider) {
11591
+ async function overview(ws, nowProvider, resolveRemoteUrl) {
10900
11592
  let manifest;
10901
11593
  try {
10902
11594
  manifest = await readManifest13(ws.paths);
@@ -10909,6 +11601,7 @@ async function overview(ws, nowProvider) {
10909
11601
  const nowIso = nowProvider().toISOString();
10910
11602
  const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
10911
11603
  const approvals = await enumerateApprovals2(ws.paths);
11604
+ const repos = await rosterRepos(ws.repoRoot, manifest, resolveRemoteUrl);
10912
11605
  return {
10913
11606
  initialized: true,
10914
11607
  repoRoot: ws.repoRoot,
@@ -10926,9 +11619,26 @@ async function overview(ws, nowProvider) {
10926
11619
  approvalsPending: approvals.pending.length,
10927
11620
  approvalsResolved: approvals.resolved.length
10928
11621
  },
11622
+ repos,
10929
11623
  generatedAt: nowIso
10930
11624
  };
10931
11625
  }
11626
+ async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
11627
+ const roster = manifest.repos ?? [];
11628
+ return Promise.all(
11629
+ roster.map(async (repo) => {
11630
+ const abs = resolve12(repoRoot, repo.path);
11631
+ const remote = await resolveRemoteUrl(abs);
11632
+ const url = remote !== void 0 ? toBrowserUrl(remote) : null;
11633
+ return {
11634
+ name: basename7(abs),
11635
+ path: repo.path,
11636
+ ...url !== null ? { url } : {},
11637
+ ...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
11638
+ };
11639
+ })
11640
+ );
11641
+ }
10932
11642
  async function sessionsList(ws, nowProvider) {
10933
11643
  const entries = await loadSessionEntries4(ws.paths, { now: nowProvider() });
10934
11644
  const sessions = entries.map((entry) => ({
@@ -11173,15 +11883,20 @@ async function buildSingleDeps(ctx, cwd) {
11173
11883
  const paths = basouPaths21(repositoryRoot);
11174
11884
  await assertWorkspaceInitialized15(paths.root);
11175
11885
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
11176
- return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
11886
+ return {
11887
+ workspaces: [entry],
11888
+ mode: "single",
11889
+ nowProvider: nowProviderOf(ctx),
11890
+ ...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
11891
+ };
11177
11892
  }
11178
11893
  async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
11179
- const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve12(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
11894
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve13(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
11180
11895
  const entries = [];
11181
11896
  const seenPath = /* @__PURE__ */ new Set();
11182
11897
  const seenKey = /* @__PURE__ */ new Set();
11183
11898
  for (const spec of specs) {
11184
- const repoRoot = resolve12(spec.path);
11899
+ const repoRoot = resolve13(spec.path);
11185
11900
  if (seenPath.has(repoRoot)) continue;
11186
11901
  seenPath.add(repoRoot);
11187
11902
  const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
@@ -11191,7 +11906,12 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
11191
11906
  entries.push({ ...entry, key });
11192
11907
  }
11193
11908
  if (entries.length === 0) throw new Error("No workspaces to show.");
11194
- return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
11909
+ return {
11910
+ workspaces: entries,
11911
+ mode: "portfolio",
11912
+ nowProvider: nowProviderOf(ctx),
11913
+ ...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
11914
+ };
11195
11915
  }
11196
11916
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
11197
11917
  const paths = basouPaths21(repoRoot);
@@ -11214,7 +11934,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
11214
11934
  const notFound = error instanceof Error && error.message === "YAML file not found";
11215
11935
  return {
11216
11936
  key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
11217
- label: labelOverride ?? basename7(repoRoot),
11937
+ label: labelOverride ?? basename8(repoRoot),
11218
11938
  paths,
11219
11939
  repoRoot,
11220
11940
  importCtx,
@@ -11253,7 +11973,7 @@ function openInBrowser(url, override) {
11253
11973
  }
11254
11974
  }
11255
11975
  function waitForShutdown(signal) {
11256
- return new Promise((resolve13) => {
11976
+ return new Promise((resolve14) => {
11257
11977
  const cleanup = () => {
11258
11978
  process.off("SIGINT", onSignal);
11259
11979
  process.off("SIGTERM", onSignal);
@@ -11261,18 +11981,18 @@ function waitForShutdown(signal) {
11261
11981
  };
11262
11982
  const onSignal = () => {
11263
11983
  cleanup();
11264
- resolve13();
11984
+ resolve14();
11265
11985
  };
11266
11986
  const onAbort = () => {
11267
11987
  cleanup();
11268
- resolve13();
11988
+ resolve14();
11269
11989
  };
11270
11990
  process.on("SIGINT", onSignal);
11271
11991
  process.on("SIGTERM", onSignal);
11272
11992
  if (signal !== void 0) {
11273
11993
  if (signal.aborted) {
11274
11994
  cleanup();
11275
- resolve13();
11995
+ resolve14();
11276
11996
  return;
11277
11997
  }
11278
11998
  signal.addEventListener("abort", onAbort);