@basou/cli 0.34.0 → 0.35.1

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
@@ -3415,6 +3415,8 @@ async function runImport(adapter, fn) {
3415
3415
  skippedNoAction: readCount(json.skipped_no_action),
3416
3416
  skippedAlreadyImported: readCount(json.skipped_already_imported),
3417
3417
  skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
3418
+ skippedDecreased: readCount(json.skipped_decreased),
3419
+ skippedDuplicate: readCount(json.skipped_duplicate),
3418
3420
  skippedUnverifiable: readCount(json.skipped_unverifiable),
3419
3421
  eventTotal: readCount(json.event_total),
3420
3422
  dryRun: json.dry_run === true
@@ -3616,11 +3618,104 @@ async function assertWorkspaceInitialized7(basouRoot) {
3616
3618
  }
3617
3619
  }
3618
3620
 
3621
+ // src/commands/portfolio.ts
3622
+ import { existsSync, statSync } from "fs";
3623
+ import { join as join9 } from "path";
3624
+ function registerPortfolioCommand(program2) {
3625
+ program2.command("portfolio").description(
3626
+ "List the workspaces you orient across (read-only): every planning master registered in ~/.basou/portfolio.yaml, with its path and whether it exists / is initialized. The headless text/JSON counterpart to the `basou view --portfolio` GUI \u2014 for discovering where a sibling project lives without opening a browser"
3627
+ ).argument("[action]", "optional literal `list` (the only, and default, action)").option("--json", "Output the result as JSON").option(
3628
+ "--check",
3629
+ "moved: the redundancy/footprint safety preflight is `basou view --portfolio --check` (this prints that pointer and exits)"
3630
+ ).option("-v, --verbose", "Show error causes").action(async (action, opts) => {
3631
+ await runPortfolioCommand(action, opts);
3632
+ });
3633
+ }
3634
+ function isDirectory(path) {
3635
+ try {
3636
+ return statSync(path).isDirectory();
3637
+ } catch {
3638
+ return false;
3639
+ }
3640
+ }
3641
+ async function runPortfolioCommand(action, options, ctx = {}) {
3642
+ if (options.check === true) {
3643
+ console.error(
3644
+ "`basou portfolio` is a read-only listing; it has no safety preflight.\nRun `basou view --portfolio --check` for the redundancy/footprint check."
3645
+ );
3646
+ process.exitCode = 1;
3647
+ return;
3648
+ }
3649
+ if (action !== void 0 && action !== "list") {
3650
+ console.error(
3651
+ `Unknown portfolio action '${action}'. Run \`basou portfolio\` or \`basou portfolio list\` to list the registered workspaces.`
3652
+ );
3653
+ process.exitCode = 1;
3654
+ return;
3655
+ }
3656
+ await runPortfolioList(options, ctx);
3657
+ }
3658
+ async function runPortfolioList(options, ctx = {}) {
3659
+ try {
3660
+ await doRunPortfolioList(options, ctx);
3661
+ } catch (error) {
3662
+ renderCliError(error, { verbose: isVerbose(options) });
3663
+ process.exitCode = 1;
3664
+ }
3665
+ }
3666
+ async function doRunPortfolioList(options, ctx) {
3667
+ const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
3668
+ const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
3669
+ const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join9(p, ".basou")));
3670
+ const workspaces = await loadPortfolioConfig(configPath);
3671
+ const result = {
3672
+ configPath,
3673
+ workspaces: workspaces.map((w) => {
3674
+ const exists = pathExists2(w.path);
3675
+ return {
3676
+ label: w.label ?? null,
3677
+ path: w.path,
3678
+ exists,
3679
+ initialized: exists && isInitialized(w.path)
3680
+ };
3681
+ })
3682
+ };
3683
+ if (options.json === true) {
3684
+ console.log(JSON.stringify(result));
3685
+ } else {
3686
+ console.log(renderPortfolioList(result));
3687
+ }
3688
+ return result;
3689
+ }
3690
+ var LABEL_COLUMN_CAP = 24;
3691
+ function renderPortfolioList(result) {
3692
+ const lines = [];
3693
+ const n = result.workspaces.length;
3694
+ lines.push("# Portfolio (workspaces you orient across)");
3695
+ lines.push("");
3696
+ lines.push(`${n} workspace${n === 1 ? "" : "s"} registered in ~/.basou/portfolio.yaml:`);
3697
+ lines.push("");
3698
+ const labelWidth = Math.min(
3699
+ LABEL_COLUMN_CAP,
3700
+ Math.max(0, ...result.workspaces.map((w) => (w.label ?? "(no label)").length))
3701
+ );
3702
+ for (const w of result.workspaces) {
3703
+ const label = (w.label ?? "(no label)").padEnd(labelWidth);
3704
+ const status = !w.exists ? "\u26A0 path not found" : w.initialized ? "\u2713 initialized" : "\u26A0 no .basou (not a planning master?)";
3705
+ lines.push(`- ${label} ${w.path} ${status}`);
3706
+ }
3707
+ lines.push("");
3708
+ lines.push(
3709
+ "Note: read-only listing of ~/.basou/portfolio.yaml. Run `basou view --portfolio` for the cross-workspace GUI, or `basou view --portfolio --check` for the redundancy/footprint safety preflight."
3710
+ );
3711
+ return lines.join("\n");
3712
+ }
3713
+
3619
3714
  // src/commands/project.ts
3620
3715
  import {
3621
3716
  closeSync,
3622
3717
  copyFileSync,
3623
- existsSync,
3718
+ existsSync as existsSync2,
3624
3719
  constants as fsConstants,
3625
3720
  ftruncateSync,
3626
3721
  lstatSync,
@@ -3630,13 +3725,13 @@ import {
3630
3725
  readFileSync,
3631
3726
  readlinkSync,
3632
3727
  realpathSync,
3633
- statSync,
3728
+ statSync as statSync2,
3634
3729
  symlinkSync,
3635
3730
  unlinkSync,
3636
3731
  writeFileSync,
3637
3732
  writeSync
3638
3733
  } from "fs";
3639
- import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join9, relative as relative2, resolve as resolve7 } from "path";
3734
+ import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join10, relative as relative2, resolve as resolve7 } from "path";
3640
3735
  import {
3641
3736
  appendBasouGitignore as appendBasouGitignore2,
3642
3737
  basouPaths as basouPaths10,
@@ -4018,7 +4113,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
4018
4113
  } catch {
4019
4114
  return { path: declaredPath, kind: "unresolved" };
4020
4115
  }
4021
- return { path: declaredPath, kind: existsSync(join9(real, ".git")) ? "repo" : "non-repo" };
4116
+ return { path: declaredPath, kind: existsSync2(join10(real, ".git")) ? "repo" : "non-repo" };
4022
4117
  }
4023
4118
  async function doRunProjectAdopt(options, ctx) {
4024
4119
  const cwd = ctx.cwd ?? process.cwd();
@@ -4122,7 +4217,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
4122
4217
  } catch {
4123
4218
  return { ...base, reachable: false, instructionFiles: [] };
4124
4219
  }
4125
- if (!existsSync(join9(real, ".git"))) {
4220
+ if (!existsSync2(join10(real, ".git"))) {
4126
4221
  return { ...base, reachable: false, instructionFiles: [] };
4127
4222
  }
4128
4223
  try {
@@ -4130,7 +4225,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
4130
4225
  for (const name of INSTRUCTION_FILES) {
4131
4226
  let present = true;
4132
4227
  try {
4133
- lstatSync(join9(real, name));
4228
+ lstatSync(join10(real, name));
4134
4229
  } catch {
4135
4230
  present = false;
4136
4231
  }
@@ -4241,10 +4336,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
4241
4336
  } catch {
4242
4337
  return { ...base, reachable: false, currentLines: [] };
4243
4338
  }
4244
- if (!existsSync(join9(real, ".git"))) {
4339
+ if (!existsSync2(join10(real, ".git"))) {
4245
4340
  return { ...base, reachable: false, currentLines: [] };
4246
4341
  }
4247
- return { ...base, reachable: true, currentLines: readGitignoreLines(join9(real, ".gitignore")) };
4342
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join10(real, ".gitignore")) };
4248
4343
  }
4249
4344
  function hasErrorCode(error) {
4250
4345
  return error instanceof Error && typeof error.code === "string";
@@ -4258,7 +4353,7 @@ function readGitignoreLines(file) {
4258
4353
  }
4259
4354
  }
4260
4355
  function applyGitignorePlan(repositoryRoot, plan) {
4261
- const file = join9(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4356
+ const file = join10(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4262
4357
  let existing = "";
4263
4358
  try {
4264
4359
  existing = readFileSync(file, "utf8");
@@ -4384,7 +4479,7 @@ function anchorCanonicalState(filePath) {
4384
4479
  if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
4385
4480
  return "broken";
4386
4481
  }
4387
- if (st.isSymbolicLink()) return existsSync(filePath) ? "usable" : "broken";
4482
+ if (st.isSymbolicLink()) return existsSync2(filePath) ? "usable" : "broken";
4388
4483
  return st.isFile() ? "usable" : "broken";
4389
4484
  }
4390
4485
  function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
@@ -4398,7 +4493,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4398
4493
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4399
4494
  }
4400
4495
  if (real === anchorReal) {
4401
- const anchorCanonical = join9(real, CANONICAL_FILE);
4496
+ const anchorCanonical = join10(real, CANONICAL_FILE);
4402
4497
  const anchorState = anchorCanonicalState(anchorCanonical);
4403
4498
  if (anchorState === "absent") {
4404
4499
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
@@ -4418,7 +4513,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4418
4513
  anchorCanonical,
4419
4514
  "self"
4420
4515
  ).map((spec) => {
4421
- const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
4516
+ const { state, actualTarget } = inspectSymlink(join10(real, spec.name), spec.target);
4422
4517
  return {
4423
4518
  name: spec.name,
4424
4519
  expectedTarget: spec.target,
@@ -4435,16 +4530,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4435
4530
  files: anchorFiles
4436
4531
  };
4437
4532
  }
4438
- if (!existsSync(join9(real, ".git"))) {
4533
+ if (!existsSync2(join10(real, ".git"))) {
4439
4534
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4440
4535
  }
4441
- const canonicalFile = isSelf ? join9(real, CANONICAL_FILE) : join9(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4442
- if (!existsSync(canonicalFile)) {
4536
+ const canonicalFile = isSelf ? join10(real, CANONICAL_FILE) : join10(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4537
+ if (!existsSync2(canonicalFile)) {
4443
4538
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
4444
4539
  }
4445
4540
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
4446
4541
  (spec) => {
4447
- const { state, actualTarget } = inspectSymlink(join9(real, spec.name), spec.target);
4542
+ const { state, actualTarget } = inspectSymlink(join10(real, spec.name), spec.target);
4448
4543
  return {
4449
4544
  name: spec.name,
4450
4545
  expectedTarget: spec.target,
@@ -4473,7 +4568,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
4473
4568
  const created = [];
4474
4569
  const failed = [];
4475
4570
  for (const { name, target } of plan.toCreate) {
4476
- const filePath = join9(real, name);
4571
+ const filePath = join10(real, name);
4477
4572
  try {
4478
4573
  mkdirSync(dirname3(filePath), { recursive: true });
4479
4574
  symlinkSync(target, filePath);
@@ -4504,10 +4599,10 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
4504
4599
  const collision = viewCanonicalCollision(repositoryRoot, roster, viewName);
4505
4600
  if (collision !== void 0) return { kind: "collision", viewName, repoPath: collision };
4506
4601
  const canonicalFile = canonicalFileFor(anchorReal, viewName);
4507
- if (!existsSync(canonicalFile)) return { kind: "missing-canonical", viewName };
4602
+ if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
4508
4603
  const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
4509
4604
  (spec) => {
4510
- const { state, actualTarget } = inspectSymlink(join9(viewDir, spec.name), spec.target);
4605
+ const { state, actualTarget } = inspectSymlink(join10(viewDir, spec.name), spec.target);
4511
4606
  return {
4512
4607
  name: spec.name,
4513
4608
  expectedTarget: spec.target,
@@ -4523,7 +4618,7 @@ function applyViewSymlinks(viewDir, files) {
4523
4618
  const failed = [];
4524
4619
  for (const f of files) {
4525
4620
  if (f.state !== "missing") continue;
4526
- const filePath = join9(viewDir, f.name);
4621
+ const filePath = join10(viewDir, f.name);
4527
4622
  try {
4528
4623
  mkdirSync(dirname3(filePath), { recursive: true });
4529
4624
  symlinkSync(f.expectedTarget, filePath);
@@ -4745,7 +4840,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
4745
4840
  return realpathSync(abs);
4746
4841
  } catch {
4747
4842
  try {
4748
- return join9(realpathSync(dirname3(abs)), basename5(abs));
4843
+ return join10(realpathSync(dirname3(abs)), basename5(abs));
4749
4844
  } catch {
4750
4845
  return abs;
4751
4846
  }
@@ -4763,7 +4858,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
4763
4858
  return { path: entry.path, reachable: false };
4764
4859
  }
4765
4860
  const linkName = basename5(repoReal);
4766
- const { state, actualTarget } = inspectSymlink(join9(viewDir, linkName), expectedTarget);
4861
+ const { state, actualTarget } = inspectSymlink(join10(viewDir, linkName), expectedTarget);
4767
4862
  return {
4768
4863
  path: entry.path,
4769
4864
  reachable: true,
@@ -4777,7 +4872,7 @@ function applyViewPlan(viewDir, toCreate) {
4777
4872
  const created = [];
4778
4873
  const failed = [];
4779
4874
  for (const { name, target } of toCreate) {
4780
- const filePath = join9(viewDir, name);
4875
+ const filePath = join10(viewDir, name);
4781
4876
  try {
4782
4877
  mkdirSync(dirname3(filePath), { recursive: true });
4783
4878
  symlinkSync(target, filePath);
@@ -4792,7 +4887,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
4792
4887
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
4793
4888
  );
4794
4889
  function classifyViewLink(viewDir, name, rosterRealpaths) {
4795
- const filePath = join9(viewDir, name);
4890
+ const filePath = join10(viewDir, name);
4796
4891
  let isLink;
4797
4892
  try {
4798
4893
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -4814,14 +4909,14 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
4814
4909
  if (isAbsolute3(target)) return { target, kind: "absolute" };
4815
4910
  let isDir = false;
4816
4911
  try {
4817
- isDir = statSync(resolved).isDirectory();
4912
+ isDir = statSync2(resolved).isDirectory();
4818
4913
  } catch {
4819
4914
  isDir = false;
4820
4915
  }
4821
4916
  if (!isDir) {
4822
- return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
4917
+ return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
4823
4918
  }
4824
- return { target, kind: existsSync(join9(resolved, ".git")) ? "repo" : "non-repo" };
4919
+ return { target, kind: existsSync2(join10(resolved, ".git")) ? "repo" : "non-repo" };
4825
4920
  }
4826
4921
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4827
4922
  let names;
@@ -4846,7 +4941,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4846
4941
  const pruned = [];
4847
4942
  const failed = [];
4848
4943
  for (const { name } of toPrune) {
4849
- const filePath = join9(viewDir, name);
4944
+ const filePath = join10(viewDir, name);
4850
4945
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
4851
4946
  if (c === null || c.kind !== "repo") {
4852
4947
  failed.push({
@@ -5059,10 +5154,10 @@ async function runProjectPreset(options, ctx = {}) {
5059
5154
  }
5060
5155
  }
5061
5156
  function canonicalFileFor(anchorReal, canonicalName) {
5062
- return join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5157
+ return join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5063
5158
  }
5064
5159
  function canonicalLabelFor(canonicalName) {
5065
- return join9("agents", canonicalName, CANONICAL_FILE);
5160
+ return join10("agents", canonicalName, CANONICAL_FILE);
5066
5161
  }
5067
5162
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5068
5163
  const declared = {
@@ -5083,7 +5178,7 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5083
5178
  if (real === anchorReal) {
5084
5179
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
5085
5180
  }
5086
- if (!existsSync(join9(real, ".git"))) {
5181
+ if (!existsSync2(join10(real, ".git"))) {
5087
5182
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
5088
5183
  }
5089
5184
  const canonicalName = basename5(real);
@@ -5465,24 +5560,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
5465
5560
  const instructionFiles = [];
5466
5561
  for (const name of INSTRUCTION_FILES) {
5467
5562
  try {
5468
- lstatSync(join9(real, name));
5563
+ lstatSync(join10(real, name));
5469
5564
  instructionFiles.push(name);
5470
5565
  } catch {
5471
5566
  }
5472
5567
  }
5473
5568
  let ignored;
5474
5569
  try {
5475
- ignored = new Set(readGitignoreLines(join9(real, ".gitignore")).map((l) => l.trim()));
5570
+ ignored = new Set(readGitignoreLines(join10(real, ".gitignore")).map((l) => l.trim()));
5476
5571
  } catch {
5477
5572
  ignored = /* @__PURE__ */ new Set();
5478
5573
  }
5479
5574
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
5480
- const canonical2 = existsSync(join9(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5575
+ const canonical2 = existsSync2(join10(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5481
5576
  let viewLink = false;
5482
5577
  const viewPath = manifest.workspace.view;
5483
5578
  if (viewPath !== void 0) {
5484
5579
  try {
5485
- lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5580
+ lstatSync(join10(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5486
5581
  viewLink = true;
5487
5582
  } catch {
5488
5583
  }
@@ -5496,11 +5591,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
5496
5591
  };
5497
5592
  }
5498
5593
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
5499
- const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5594
+ const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5500
5595
  return expectedSymlinkTargets(repoReal, canonicalFile);
5501
5596
  }
5502
5597
  function viewLinkPointsAt(viewDir, name, repoReal) {
5503
- const filePath = join9(viewDir, name);
5598
+ const filePath = join10(viewDir, name);
5504
5599
  try {
5505
5600
  if (!lstatSync(filePath).isSymbolicLink()) return false;
5506
5601
  const target = readlinkSync(filePath);
@@ -5511,7 +5606,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
5511
5606
  }
5512
5607
  }
5513
5608
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
5514
- const filePath = join9(viewDir, name);
5609
+ const filePath = join10(viewDir, name);
5515
5610
  try {
5516
5611
  if (!lstatSync(filePath).isSymbolicLink()) return false;
5517
5612
  const target = readlinkSync(filePath);
@@ -5562,7 +5657,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5562
5657
  if (!isAnchor) {
5563
5658
  if (repoReal !== void 0) {
5564
5659
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
5565
- const { state, actualTarget } = inspectSymlink(join9(repoReal, spec.name), spec.target);
5660
+ const { state, actualTarget } = inspectSymlink(join10(repoReal, spec.name), spec.target);
5566
5661
  if (isSelf) {
5567
5662
  if (state !== "missing")
5568
5663
  items.push({
@@ -5599,7 +5694,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5599
5694
  }
5600
5695
  let ignored;
5601
5696
  try {
5602
- ignored = new Set(readGitignoreLines(join9(repoReal, ".gitignore")).map((l) => l.trim()));
5697
+ ignored = new Set(readGitignoreLines(join10(repoReal, ".gitignore")).map((l) => l.trim()));
5603
5698
  for (const p of INSTRUCTION_FILES) {
5604
5699
  if (ignored.has(p) || ignored.has(`/${p}`)) {
5605
5700
  items.push({
@@ -5622,7 +5717,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5622
5717
  const viewPath = manifest.workspace.view;
5623
5718
  if (viewPath !== void 0) {
5624
5719
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
5625
- const linkPath = join9(viewDir, canonicalName);
5720
+ const linkPath = join10(viewDir, canonicalName);
5626
5721
  let isLink = false;
5627
5722
  try {
5628
5723
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -5648,8 +5743,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5648
5743
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
5649
5744
  }
5650
5745
  }
5651
- const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5652
- const canonicalLabel = join9("agents", canonicalName, CANONICAL_FILE);
5746
+ const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5747
+ const canonicalLabel = join10("agents", canonicalName, CANONICAL_FILE);
5653
5748
  let canonicalIsLink = false;
5654
5749
  try {
5655
5750
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -5663,7 +5758,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5663
5758
  state: "foreign",
5664
5759
  note: "the canonical is a symlink (not generated)"
5665
5760
  });
5666
- } else if (existsSync(canonicalFile)) {
5761
+ } else if (existsSync2(canonicalFile)) {
5667
5762
  let content;
5668
5763
  try {
5669
5764
  content = readFileSync(canonicalFile, "utf8");
@@ -5753,12 +5848,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5753
5848
  );
5754
5849
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5755
5850
  const expected = expectedByName.get(item.label);
5756
- if (repoReal === null || expected === void 0 || inspectSymlink(join9(repoReal, item.label), expected).state !== "correct") {
5851
+ if (repoReal === null || expected === void 0 || inspectSymlink(join10(repoReal, item.label), expected).state !== "correct") {
5757
5852
  changed(item.label);
5758
5853
  continue;
5759
5854
  }
5760
5855
  try {
5761
- unlinkSync(join9(repoReal, item.label));
5856
+ unlinkSync(join10(repoReal, item.label));
5762
5857
  removed.push(item.label);
5763
5858
  } catch (error) {
5764
5859
  failed.push({ label: item.label, message: failureReason(error) });
@@ -5777,7 +5872,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5777
5872
  continue;
5778
5873
  }
5779
5874
  try {
5780
- unlinkSync(join9(viewDir, item.label));
5875
+ unlinkSync(join10(viewDir, item.label));
5781
5876
  removed.push(`view/${item.label}`);
5782
5877
  } catch (error) {
5783
5878
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -5785,7 +5880,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5785
5880
  }
5786
5881
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5787
5882
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5788
- const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5883
+ const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5789
5884
  try {
5790
5885
  if (lstatSync(canonicalFile).isSymbolicLink()) {
5791
5886
  changed(item.label);
@@ -6059,12 +6154,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
6059
6154
  } catch {
6060
6155
  return { canonicalDirOld: false, viewLinkOld: false };
6061
6156
  }
6062
- const canonicalDirOld = existsSync(join9(anchorReal, "agents", oldBasename));
6157
+ const canonicalDirOld = existsSync2(join10(anchorReal, "agents", oldBasename));
6063
6158
  let viewLinkOld = false;
6064
6159
  const viewPath = manifest.workspace.view;
6065
6160
  if (viewPath !== void 0) {
6066
6161
  try {
6067
- lstatSync(join9(resolveViewDir(repositoryRoot, viewPath), oldBasename));
6162
+ lstatSync(join10(resolveViewDir(repositoryRoot, viewPath), oldBasename));
6068
6163
  viewLinkOld = true;
6069
6164
  } catch {
6070
6165
  }
@@ -6262,7 +6357,7 @@ async function doRunProjectNew(repos, options, ctx) {
6262
6357
  const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
6263
6358
  const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
6264
6359
  const paths = basouPaths10(repositoryRoot);
6265
- const existed = existsSync(paths.files.manifest);
6360
+ const existed = existsSync2(paths.files.manifest);
6266
6361
  const manifest = createManifest2({
6267
6362
  workspaceName,
6268
6363
  sourceRoots,
@@ -6422,7 +6517,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
6422
6517
  console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
6423
6518
  return;
6424
6519
  }
6425
- const anchorDoc = join9(repositoryRoot, CANONICAL_FILE);
6520
+ const anchorDoc = join10(repositoryRoot, CANONICAL_FILE);
6426
6521
  if (pathPresent(anchorDoc)) {
6427
6522
  console.log(
6428
6523
  `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
@@ -6484,7 +6579,7 @@ function regularFileSpokes(repoReal) {
6484
6579
  const out = [];
6485
6580
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
6486
6581
  try {
6487
- const st = lstatSync(join9(repoReal, spoke));
6582
+ const st = lstatSync(join10(repoReal, spoke));
6488
6583
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
6489
6584
  } catch {
6490
6585
  }
@@ -6530,8 +6625,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
6530
6625
  };
6531
6626
  }
6532
6627
  const isAnchor = argReal === anchorReal;
6533
- const reachable = existsSync(join9(argReal, ".git"));
6534
- const canonicalFile = join9(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6628
+ const reachable = existsSync2(join10(argReal, ".git"));
6629
+ const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6535
6630
  return {
6536
6631
  path,
6537
6632
  declared,
@@ -6540,13 +6635,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
6540
6635
  reachable,
6541
6636
  canonicalName,
6542
6637
  ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
6543
- agentsState: inspectAgentsState(join9(argReal, CANONICAL_FILE)),
6638
+ agentsState: inspectAgentsState(join10(argReal, CANONICAL_FILE)),
6544
6639
  canonicalExists: pathPresent(canonicalFile),
6545
6640
  regularSpokes: regularFileSpokes(argReal)
6546
6641
  };
6547
6642
  }
6548
6643
  function relocateAgentsFile(repoReal, canonicalFile) {
6549
- const agentsFile = join9(repoReal, CANONICAL_FILE);
6644
+ const agentsFile = join10(repoReal, CANONICAL_FILE);
6550
6645
  try {
6551
6646
  mkdirSync(dirname3(canonicalFile), { recursive: true });
6552
6647
  } catch (error) {
@@ -6663,7 +6758,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
6663
6758
  let failure;
6664
6759
  let partial = false;
6665
6760
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
6666
- const canonicalFile = join9(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6761
+ const canonicalFile = join10(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6667
6762
  const res = relocateAgentsFile(argReal, canonicalFile);
6668
6763
  if (res.ok) {
6669
6764
  applied = true;
@@ -6868,7 +6963,7 @@ import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdo
6868
6963
 
6869
6964
  // src/lib/context-channel.ts
6870
6965
  import { homedir as homedir7 } from "os";
6871
- import { join as join10 } from "path";
6966
+ import { join as join11 } from "path";
6872
6967
  import {
6873
6968
  ORIENTATION_END,
6874
6969
  ORIENTATION_START,
@@ -6876,7 +6971,7 @@ import {
6876
6971
  readMarkdownFile as readMarkdownFile5,
6877
6972
  removeMarkerSection as removeMarkerSection2
6878
6973
  } from "@basou/core";
6879
- var CODEX_TARGET_PATH = join10(homedir7(), ".codex", "AGENTS.md");
6974
+ var CODEX_TARGET_PATH = join11(homedir7(), ".codex", "AGENTS.md");
6880
6975
  var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
6881
6976
  var ORIENTATION_MANAGED_NOTE = "<!-- Managed by basou: 'basou refresh' regenerates everything between the BASOU:ORIENTATION markers with the workspace's current position. This block is transient \u2014 it changes every refresh; do not edit it. -->";
6882
6977
  function buildTargetBody(existing, block, markers) {
@@ -6980,15 +7075,15 @@ async function renderOrientationToCodexChannel(opts) {
6980
7075
 
6981
7076
  // src/lib/protocols-config.ts
6982
7077
  import { homedir as homedir8 } from "os";
6983
- import { isAbsolute as isAbsolute4, join as join11, resolve as resolve8 } from "path";
7078
+ import { isAbsolute as isAbsolute4, join as join12, resolve as resolve8 } from "path";
6984
7079
  import { readYamlFile as readYamlFile5 } from "@basou/core";
6985
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join11(homedir8(), ".basou", "protocols.yaml");
6986
- var DEFAULT_TARGET_PATH = join11(homedir8(), ".claude", "CLAUDE.md");
7080
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join12(homedir8(), ".basou", "protocols.yaml");
7081
+ var DEFAULT_TARGET_PATH = join12(homedir8(), ".claude", "CLAUDE.md");
6987
7082
  var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
6988
7083
  var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
6989
7084
  function expandTilde3(p) {
6990
7085
  if (p === "~") return homedir8();
6991
- if (p.startsWith("~/")) return join11(homedir8(), p.slice(2));
7086
+ if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
6992
7087
  return p;
6993
7088
  }
6994
7089
  function isRecord3(value) {
@@ -7193,15 +7288,15 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
7193
7288
  // src/commands/refresh-watch.ts
7194
7289
  import { readdir as readdir2, stat as stat5 } from "fs/promises";
7195
7290
  import { homedir as homedir9 } from "os";
7196
- import { join as join12 } from "path";
7291
+ import { join as join13 } from "path";
7197
7292
  import { findErrorCode as findErrorCode8 } from "@basou/core";
7198
7293
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
7199
7294
  var MIN_WATCH_INTERVAL_SEC = 5;
7200
7295
  var MAX_WATCH_INTERVAL_SEC = 86400;
7201
7296
  function watchedRoots(ctx) {
7202
7297
  return [
7203
- ctx.codexSessionsDir ?? join12(homedir9(), ".codex", "sessions"),
7204
- ctx.claudeProjectsDir ?? join12(homedir9(), ".claude", "projects")
7298
+ ctx.codexSessionsDir ?? join13(homedir9(), ".codex", "sessions"),
7299
+ ctx.claudeProjectsDir ?? join13(homedir9(), ".claude", "projects")
7205
7300
  ];
7206
7301
  }
7207
7302
  async function scanSourceLogs(roots) {
@@ -7215,7 +7310,7 @@ async function scanSourceLogs(roots) {
7215
7310
  throw new Error("Failed to read a source log directory", { cause: error });
7216
7311
  }
7217
7312
  for (const entry of entries) {
7218
- const full = join12(dir, entry.name);
7313
+ const full = join13(dir, entry.name);
7219
7314
  if (entry.isDirectory()) {
7220
7315
  await walk(full);
7221
7316
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -7503,6 +7598,11 @@ function describeImport(outcome) {
7503
7598
  if (outcome.skippedAlreadyImported > 0)
7504
7599
  parts.push(`${outcome.skippedAlreadyImported} already imported`);
7505
7600
  if (outcome.skippedLegacyUntracked > 0) parts.push(`${outcome.skippedLegacyUntracked} legacy`);
7601
+ if (outcome.skippedNoAction > 0) parts.push(`${outcome.skippedNoAction} with no actions`);
7602
+ if (outcome.skippedDecreased > 0) parts.push(`${outcome.skippedDecreased} shrank`);
7603
+ if (outcome.skippedDuplicate > 0) parts.push(`${outcome.skippedDuplicate} duplicated`);
7604
+ if (outcome.skippedUnverifiable > 0)
7605
+ parts.push(`${outcome.skippedUnverifiable} unverifiable (run 'basou verify')`);
7506
7606
  return `${outcome.adapter}: ${verb} ${parts.join(", ")}`;
7507
7607
  }
7508
7608
  function printRefreshSummary(result) {
@@ -7937,7 +8037,7 @@ function renderReviewGaps(summary) {
7937
8037
  // src/commands/run.ts
7938
8038
  import { mkdir as mkdir2 } from "fs/promises";
7939
8039
  import { homedir as homedir11 } from "os";
7940
- import { join as join13 } from "path";
8040
+ import { join as join14 } from "path";
7941
8041
  import {
7942
8042
  acquireLock as acquireLock5,
7943
8043
  assertBasouRootSafe as assertBasouRootSafe12,
@@ -8013,13 +8113,13 @@ async function runTrackedTool(args, options, ctx, adapter) {
8013
8113
  await assertBasouRootSafe12(paths.root);
8014
8114
  const manifest = await readManifest8(paths);
8015
8115
  const sessionId = prefixedUlid4("ses");
8016
- const sessionDir = join13(paths.sessions, sessionId);
8116
+ const sessionDir = join14(paths.sessions, sessionId);
8017
8117
  await mkdir2(sessionDir, { recursive: true });
8018
8118
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
8019
8119
  await coreAppendChainedEvent2(paths, sessionId, event);
8020
8120
  });
8021
8121
  const startedAt = now().toISOString();
8022
- const sessionYamlPath = join13(sessionDir, "session.yaml");
8122
+ const sessionYamlPath = join14(sessionDir, "session.yaml");
8023
8123
  const session = buildInitialSession2({
8024
8124
  id: sessionId,
8025
8125
  command,
@@ -8381,7 +8481,7 @@ async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
8381
8481
 
8382
8482
  // src/commands/session.ts
8383
8483
  import { readFile as readFile6 } from "fs/promises";
8384
- import { basename as basename6, isAbsolute as isAbsolute6, join as join14, relative as relative3 } from "path";
8484
+ import { basename as basename6, isAbsolute as isAbsolute6, join as join15, relative as relative3 } from "path";
8385
8485
  import {
8386
8486
  acquireLock as acquireLock6,
8387
8487
  appendEventToExistingSession as appendEventToExistingSession3,
@@ -8506,8 +8606,8 @@ async function doRunSessionShow(idInput, options, ctx) {
8506
8606
  const paths = basouPaths16(repositoryRoot);
8507
8607
  await assertWorkspaceInitialized11(paths.root);
8508
8608
  const sessionId = await resolveSessionId3(paths, idInput);
8509
- const sessionDir = join14(paths.sessions, sessionId);
8510
- const sessionYamlPath = join14(sessionDir, "session.yaml");
8609
+ const sessionDir = join15(paths.sessions, sessionId);
8610
+ const sessionYamlPath = join15(sessionDir, "session.yaml");
8511
8611
  let session;
8512
8612
  try {
8513
8613
  const raw = await readYamlFile7(sessionYamlPath);
@@ -9269,7 +9369,7 @@ function formatVersionGateMessage(error) {
9269
9369
 
9270
9370
  // src/commands/task.ts
9271
9371
  import { readFile as readFile7 } from "fs/promises";
9272
- import { join as join15 } from "path";
9372
+ import { join as join16 } from "path";
9273
9373
  import {
9274
9374
  archiveTask,
9275
9375
  assertBasouRootSafe as assertBasouRootSafe16,
@@ -9595,7 +9695,7 @@ async function doRunTaskShow(idInput, options, ctx) {
9595
9695
  const events = [];
9596
9696
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
9597
9697
  for (const s of sessions) {
9598
- const sessionDir = join15(paths.sessions, s.sessionId);
9698
+ const sessionDir = join16(paths.sessions, s.sessionId);
9599
9699
  try {
9600
9700
  for await (const ev of replayEvents3(sessionDir, {
9601
9701
  onWarning: (w) => printReplayWarning(w, s.sessionId)
@@ -10501,7 +10601,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
10501
10601
  // src/lib/portfolio-safety.ts
10502
10602
  import { execFile } from "child_process";
10503
10603
  import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
10504
- import { isAbsolute as isAbsolute7, join as join16, relative as relative4, resolve as resolve11 } from "path";
10604
+ import { isAbsolute as isAbsolute7, join as join17, relative as relative4, resolve as resolve11 } from "path";
10505
10605
  import { promisify } from "util";
10506
10606
  import { readManifest as readManifest12 } from "@basou/core";
10507
10607
  var execFileAsync = promisify(execFile);
@@ -10525,7 +10625,7 @@ function isBasouPath(p) {
10525
10625
  async function inspectRepo(repoPath) {
10526
10626
  let hasEntry = false;
10527
10627
  try {
10528
- await lstat2(join16(repoPath, ".basou"));
10628
+ await lstat2(join17(repoPath, ".basou"));
10529
10629
  hasEntry = true;
10530
10630
  } catch (error) {
10531
10631
  if (errorCode(error) !== "ENOENT") {
@@ -10679,7 +10779,7 @@ function formatSafetyReport(result) {
10679
10779
 
10680
10780
  // src/lib/view-server.ts
10681
10781
  import { createServer } from "http";
10682
- import { basename as basename7, join as join17, resolve as resolve12 } from "path";
10782
+ import { basename as basename7, join as join18, resolve as resolve12 } from "path";
10683
10783
  import {
10684
10784
  computeWorkStats as computeWorkStats2,
10685
10785
  enumerateApprovals as enumerateApprovals2,
@@ -11719,7 +11819,7 @@ async function sessionDetail(ws, sessionId) {
11719
11819
  throw error;
11720
11820
  }
11721
11821
  try {
11722
- const events = await readAllEvents2(join17(ws.paths.sessions, sessionId));
11822
+ const events = await readAllEvents2(join18(ws.paths.sessions, sessionId));
11723
11823
  return { session, events };
11724
11824
  } catch {
11725
11825
  return { session, events: [], degraded: true };
@@ -12100,6 +12200,7 @@ function buildProgram() {
12100
12200
  registerDecisionsCommand(program2);
12101
12201
  registerReportCommand(program2);
12102
12202
  registerOrientCommand(program2);
12203
+ registerPortfolioCommand(program2);
12103
12204
  registerReviewCommand(program2);
12104
12205
  registerReviewGapsCommand(program2);
12105
12206
  registerProjectCommand(program2);