@basou/cli 0.38.0 → 0.39.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/program.js CHANGED
@@ -632,10 +632,243 @@ function printNoApprovals(options) {
632
632
  }
633
633
  }
634
634
 
635
+ // src/lib/context-channel.ts
636
+ import { homedir } from "os";
637
+ import { join as join3 } from "path";
638
+ import {
639
+ ORIENTATION_END,
640
+ ORIENTATION_START,
641
+ parseMarkers,
642
+ readMarkdownFile,
643
+ removeMarkerSection
644
+ } from "@basou/core";
645
+
646
+ // src/lib/durable-write.ts
647
+ import { randomUUID } from "crypto";
648
+ import { lstat, open, rename, stat, unlink as unlink2 } from "fs/promises";
649
+ import { basename, dirname, join as join2 } from "path";
650
+ async function assertNotSymlink(targetPath) {
651
+ try {
652
+ const st = await lstat(targetPath);
653
+ if (st.isSymbolicLink()) {
654
+ throw new Error(
655
+ "Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
656
+ );
657
+ }
658
+ } catch (error) {
659
+ if (error instanceof Error && error.code === "ENOENT") return;
660
+ throw error;
661
+ }
662
+ }
663
+ async function writeFileDurable(targetPath, content) {
664
+ const dir = dirname(targetPath);
665
+ const tmpPath = join2(dir, `.${basename(targetPath)}.tmp.${randomUUID()}`);
666
+ let mode = 420;
667
+ try {
668
+ mode = (await stat(targetPath)).mode & 511;
669
+ } catch (error) {
670
+ if (!(error instanceof Error && error.code === "ENOENT")) {
671
+ throw error;
672
+ }
673
+ }
674
+ let handle;
675
+ try {
676
+ handle = await open(tmpPath, "wx", mode);
677
+ await handle.writeFile(content, "utf8");
678
+ await handle.chmod(mode);
679
+ await handle.sync();
680
+ await handle.close();
681
+ handle = void 0;
682
+ await rename(tmpPath, targetPath);
683
+ } catch (error) {
684
+ if (handle) await handle.close().catch(() => void 0);
685
+ await unlink2(tmpPath).catch(() => void 0);
686
+ throw error;
687
+ }
688
+ try {
689
+ const dirHandle = await open(dir, "r");
690
+ try {
691
+ await dirHandle.sync();
692
+ } finally {
693
+ await dirHandle.close();
694
+ }
695
+ } catch {
696
+ }
697
+ }
698
+
699
+ // src/lib/context-channel.ts
700
+ var CODEX_TARGET_PATH = join3(homedir(), ".codex", "AGENTS.md");
701
+ var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
702
+ 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. -->";
703
+ function buildTargetBody(existing, block, markers) {
704
+ const wrapped = `${markers.start}
705
+ ${block}${markers.end}
706
+ `;
707
+ if (existing === null || existing === "") return wrapped;
708
+ const section = parseMarkers(existing, markers);
709
+ switch (section.kind) {
710
+ case "ok":
711
+ return `${section.before}${markers.start}
712
+ ${block}${markers.end}${section.after}`;
713
+ case "no_markers": {
714
+ const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
715
+ return `${existing}${sep}${wrapped}`;
716
+ }
717
+ default:
718
+ throw new Error(
719
+ "The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
720
+ );
721
+ }
722
+ }
723
+ async function backupOnce(target, existing) {
724
+ const bak = `${target}.basou-bak`;
725
+ const already = await readMarkdownFile(bak);
726
+ if (already !== null) return;
727
+ await writeFileDurable(bak, existing ?? "");
728
+ }
729
+ async function syncMarkerBlock(opts) {
730
+ const { target, markers, block } = opts;
731
+ await assertNotSymlink(target);
732
+ const existing = await readMarkdownFile(target);
733
+ const newBody = buildTargetBody(existing, block, markers);
734
+ if (newBody === existing) return { action: "unchanged" };
735
+ const hadBlock = existing !== null && parseMarkers(existing, markers).kind === "ok";
736
+ const action = hadBlock ? "updated" : "installed";
737
+ if (opts.dryRun === true) return { action };
738
+ const recheck = await readMarkdownFile(target);
739
+ if (recheck !== existing) {
740
+ throw new Error(
741
+ "The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
742
+ );
743
+ }
744
+ await backupOnce(target, existing);
745
+ await writeFileDurable(target, newBody);
746
+ return { action };
747
+ }
748
+ function assertNoMarkerLine(body, markers) {
749
+ for (const line of body.split(/\r?\n/)) {
750
+ if (line === markers.start || line === markers.end) {
751
+ throw new Error(
752
+ "The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
753
+ );
754
+ }
755
+ }
756
+ }
757
+ async function removeMarkerBlock(opts) {
758
+ const { target, markers, fileLabel } = opts;
759
+ await assertNotSymlink(target);
760
+ const existing = await readMarkdownFile(target);
761
+ if (existing === null) return { removed: false };
762
+ const newBody = removeMarkerSection(existing, fileLabel, markers);
763
+ if (newBody === existing) return { removed: false };
764
+ if (opts.dryRun === true) return { removed: true };
765
+ const recheck = await readMarkdownFile(target);
766
+ if (recheck !== existing) {
767
+ throw new Error(
768
+ "The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
769
+ );
770
+ }
771
+ if (opts.backup !== false) await backupOnce(target, existing);
772
+ await writeFileDurable(target, newBody);
773
+ return { removed: true };
774
+ }
775
+ async function syncOrientationChannel(opts) {
776
+ assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
777
+ const block = `${ORIENTATION_MANAGED_NOTE}
778
+
779
+ ${opts.body.replace(/\s+$/, "")}
780
+ `;
781
+ return syncMarkerBlock({
782
+ target: opts.target ?? CODEX_TARGET_PATH,
783
+ markers: ORIENTATION_MARKERS,
784
+ block,
785
+ ...opts.dryRun === true ? { dryRun: true } : {}
786
+ });
787
+ }
788
+ async function clearOrientationChannel(opts) {
789
+ return removeMarkerBlock({
790
+ target: opts.target ?? CODEX_TARGET_PATH,
791
+ markers: ORIENTATION_MARKERS,
792
+ // Name the file actually acted on, so an error under the test seam does not
793
+ // point at the locked path.
794
+ fileLabel: opts.target ?? "~/.codex/AGENTS.md",
795
+ // The block is being removed because it should not be on this machine;
796
+ // preserving it in `.basou-bak` would defeat the command.
797
+ backup: false,
798
+ ...opts.dryRun === true ? { dryRun: true } : {}
799
+ });
800
+ }
801
+ async function renderOrientationToCodexChannel(opts) {
802
+ const body = await readMarkdownFile(opts.orientationPath);
803
+ if (body === null) return null;
804
+ const { action } = await syncOrientationChannel({
805
+ body,
806
+ ...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
807
+ });
808
+ return {
809
+ action,
810
+ line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
811
+ };
812
+ }
813
+
814
+ // src/commands/channel.ts
815
+ function registerChannelCommand(program) {
816
+ const channel = program.command("channel").description(
817
+ "Manage the user-global context faces basou renders into \u2014 files every project's AI tool auto-loads (~/.codex/AGENTS.md)"
818
+ );
819
+ channel.command("clear").argument(
820
+ "<face>",
821
+ "the face to clear: `codex` (the basou:orientation block in ~/.codex/AGENTS.md)"
822
+ ).description(
823
+ "Remove basou's block from a user-global context face, so no workspace's position is left in a file that another project's tool reads"
824
+ ).option("--dry-run", "Report whether a block would be removed without writing").option("--json", "Output the result as JSON").option("--target <path>", "Override the target file (intended for tests)").option("-v, --verbose", "Show error causes").action(async (face, opts) => {
825
+ await runChannelClear(face, opts);
826
+ });
827
+ }
828
+ async function runChannelClear(face, options) {
829
+ if (face !== "codex") {
830
+ console.error(
831
+ `Unknown face '${face}'. Faces: codex (~/.codex/AGENTS.md). The basou:protocols block in ~/.claude/CLAUDE.md is removed with \`basou protocol unsync\`.`
832
+ );
833
+ process.exitCode = 1;
834
+ return;
835
+ }
836
+ try {
837
+ await doRunChannelClear(face, options);
838
+ } catch (error) {
839
+ renderCliError(error, { verbose: isVerbose(options) });
840
+ process.exitCode = 1;
841
+ }
842
+ }
843
+ async function doRunChannelClear(face, options) {
844
+ const isDry = options.dryRun === true;
845
+ const { removed } = await clearOrientationChannel({
846
+ ...options.target !== void 0 ? { target: options.target } : {},
847
+ ...isDry ? { dryRun: true } : {}
848
+ });
849
+ const target = options.target ?? CODEX_TARGET_PATH;
850
+ const label = options.target ?? "~/.codex/AGENTS.md";
851
+ const result = { face, target, removed, dry_run: isDry };
852
+ if (options.json === true) {
853
+ console.log(JSON.stringify(result));
854
+ return result;
855
+ }
856
+ if (!removed) {
857
+ console.log(`Nothing to clear: ${label} carries no basou:orientation block.`);
858
+ } else if (isDry) {
859
+ console.log(`[dry-run] Would remove the basou:orientation block from ${label}.`);
860
+ } else {
861
+ console.log(
862
+ `Removed the basou:orientation block from ${label}. Nothing basou wrote remains in that file; the next opted-in \`basou refresh\` renders it again.`
863
+ );
864
+ }
865
+ return result;
866
+ }
867
+
635
868
  // src/commands/decision.ts
636
869
  import { readFile } from "fs/promises";
637
- import { homedir as homedir2 } from "os";
638
- import { join as join3, resolve as resolve3 } from "path";
870
+ import { homedir as homedir3 } from "os";
871
+ import { join as join5, resolve as resolve3 } from "path";
639
872
  import {
640
873
  AGENT_INFRA_DIRS,
641
874
  acquireLock as acquireLock2,
@@ -657,18 +890,18 @@ import {
657
890
  import { InvalidArgumentError } from "commander";
658
891
 
659
892
  // src/lib/repo-root.ts
660
- import { realpath, stat } from "fs/promises";
661
- import { basename, resolve as resolve2 } from "path";
893
+ import { realpath, stat as stat2 } from "fs/promises";
894
+ import { basename as basename2, resolve as resolve2 } from "path";
662
895
  import { basouPaths as basouPaths2, readManifest, resolveBasouRepositoryRoot } from "@basou/core";
663
896
 
664
897
  // src/lib/portfolio-config.ts
665
- import { homedir } from "os";
666
- import { isAbsolute, join as join2, resolve } from "path";
898
+ import { homedir as homedir2 } from "os";
899
+ import { isAbsolute, join as join4, resolve } from "path";
667
900
  import { readYamlFile as readYamlFile2 } from "@basou/core";
668
- var DEFAULT_PORTFOLIO_CONFIG_PATH = join2(homedir(), ".basou", "portfolio.yaml");
901
+ var DEFAULT_PORTFOLIO_CONFIG_PATH = join4(homedir2(), ".basou", "portfolio.yaml");
669
902
  function expandTilde(p) {
670
- if (p === "~") return homedir();
671
- if (p.startsWith("~/")) return join2(homedir(), p.slice(2));
903
+ if (p === "~") return homedir2();
904
+ if (p.startsWith("~/")) return join4(homedir2(), p.slice(2));
672
905
  return p;
673
906
  }
674
907
  function isRecord(value) {
@@ -750,7 +983,7 @@ async function resolveBasouRootForCommand(cwd, commandName, opts = {}) {
750
983
  }
751
984
  async function hasBasouStore(root) {
752
985
  try {
753
- return (await stat(basouPaths2(root).root)).isDirectory();
986
+ return (await stat2(basouPaths2(root).root)).isDirectory();
754
987
  } catch {
755
988
  return false;
756
989
  }
@@ -782,7 +1015,7 @@ async function resolveMemberToMaster(repoRoot, configPath) {
782
1015
  } catch (error) {
783
1016
  if (error instanceof Error && error.message !== "YAML file not found") {
784
1017
  console.error(
785
- `Skipping portfolio workspace '${ws.label ?? basename(masterReal)}': could not read its manifest (${error.message}).`
1018
+ `Skipping portfolio workspace '${ws.label ?? basename2(masterReal)}': could not read its manifest (${error.message}).`
786
1019
  );
787
1020
  }
788
1021
  continue;
@@ -791,7 +1024,7 @@ async function resolveMemberToMaster(repoRoot, configPath) {
791
1024
  for (const sr of sourceRoots) {
792
1025
  const real = await realpathOrNull(resolve2(masterReal, sr));
793
1026
  if (real !== null && real === memberReal) {
794
- claimants.set(masterReal, { root: masterReal, label: ws.label ?? basename(masterReal) });
1027
+ claimants.set(masterReal, { root: masterReal, label: ws.label ?? basename2(masterReal) });
795
1028
  break;
796
1029
  }
797
1030
  }
@@ -1044,7 +1277,7 @@ async function doRunDecisionCapture(options, ctx) {
1044
1277
  "--file",
1045
1278
  sanitizePath(resolve3(cwd, options.file), {
1046
1279
  workingDirectory: repositoryRoot,
1047
- homedir: homedir2()
1280
+ homedir: homedir3()
1048
1281
  })
1049
1282
  ] : [];
1050
1283
  const adHoc = await createAdHocSessionWithEvent({
@@ -1181,7 +1414,7 @@ function isDecisionId(value) {
1181
1414
  async function decisionExists(paths, decisionId) {
1182
1415
  const entries = await loadSessionEntries(paths, { now: /* @__PURE__ */ new Date() });
1183
1416
  for (const entry of entries) {
1184
- const sessionDir = join3(paths.sessions, entry.sessionId);
1417
+ const sessionDir = join5(paths.sessions, entry.sessionId);
1185
1418
  try {
1186
1419
  for await (const ev of replayEvents2(sessionDir, {})) {
1187
1420
  if (ev.type === "decision_recorded" && ev.decision_id === decisionId) return true;
@@ -1576,7 +1809,7 @@ import {
1576
1809
  assertBasouRootSafe as assertBasouRootSafe3,
1577
1810
  basouPaths as basouPaths4,
1578
1811
  findErrorCode as findErrorCode3,
1579
- readMarkdownFile,
1812
+ readMarkdownFile as readMarkdownFile2,
1580
1813
  renderDecisions,
1581
1814
  renderWithMarkers,
1582
1815
  resolveRepositoryRoot as resolveRepositoryRoot3,
@@ -1609,7 +1842,7 @@ async function doRunDecisionsGenerate(options, ctx) {
1609
1842
  onWarning: (w, sid) => printReplayWarning(w, sid),
1610
1843
  onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
1611
1844
  });
1612
- const existing = await readMarkdownFile(paths.files.decisions);
1845
+ const existing = await readMarkdownFile2(paths.files.decisions);
1613
1846
  const finalBody = renderWithMarkers(existing, result.body, "decisions.md");
1614
1847
  await writeMarkdownFile(paths.files.decisions, finalBody);
1615
1848
  console.log(`Generated .basou/decisions.md (decisions: ${result.decisionCount})`);
@@ -1640,8 +1873,8 @@ async function assertWorkspaceInitialized3(basouRoot) {
1640
1873
 
1641
1874
  // src/commands/exec.ts
1642
1875
  import { mkdir } from "fs/promises";
1643
- import { homedir as homedir3 } from "os";
1644
- import { join as join4 } from "path";
1876
+ import { homedir as homedir4 } from "os";
1877
+ import { join as join6 } from "path";
1645
1878
  import {
1646
1879
  acquireLock as acquireLock3,
1647
1880
  assertBasouRootSafe as assertBasouRootSafe4,
@@ -1681,13 +1914,13 @@ async function runExec(command, args, options, ctx = {}) {
1681
1914
  await assertBasouRootSafe4(paths.root);
1682
1915
  const manifest = await readManifest3(paths);
1683
1916
  const sessionId = prefixedUlid3("ses");
1684
- const sessionDir = join4(paths.sessions, sessionId);
1917
+ const sessionDir = join6(paths.sessions, sessionId);
1685
1918
  await mkdir(sessionDir, { recursive: true });
1686
1919
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
1687
1920
  await coreAppendChainedEvent(paths, sessionId, event);
1688
1921
  });
1689
1922
  const startedAt = now().toISOString();
1690
- const sessionYamlPath = join4(sessionDir, "session.yaml");
1923
+ const sessionYamlPath = join6(sessionDir, "session.yaml");
1691
1924
  const session = buildInitialSession({
1692
1925
  id: sessionId,
1693
1926
  command,
@@ -1892,7 +2125,7 @@ function buildInitialSession(input) {
1892
2125
  source: { kind: "terminal", version: "0.1.0" },
1893
2126
  started_at: input.startedAt,
1894
2127
  status: "initialized",
1895
- working_directory: sanitizeWorkingDirectory(input.cwd, { homedir: homedir3() }),
2128
+ working_directory: sanitizeWorkingDirectory(input.cwd, { homedir: homedir4() }),
1896
2129
  invocation: {
1897
2130
  command: input.command,
1898
2131
  args: [...input.args],
@@ -1968,7 +2201,7 @@ import {
1968
2201
  assertBasouRootSafe as assertBasouRootSafe5,
1969
2202
  basouPaths as basouPaths6,
1970
2203
  findErrorCode as findErrorCode4,
1971
- readMarkdownFile as readMarkdownFile2,
2204
+ readMarkdownFile as readMarkdownFile3,
1972
2205
  renderHandoff,
1973
2206
  renderWithMarkers as renderWithMarkers2,
1974
2207
  resolveRepositoryRoot as resolveRepositoryRoot5,
@@ -2002,7 +2235,7 @@ async function doRunHandoffGenerate(options, ctx) {
2002
2235
  onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
2003
2236
  onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
2004
2237
  });
2005
- const existing = await readMarkdownFile2(paths.files.handoff);
2238
+ const existing = await readMarkdownFile3(paths.files.handoff);
2006
2239
  const finalBody = renderWithMarkers2(existing, result.body, "handoff.md");
2007
2240
  await writeMarkdownFile2(paths.files.handoff, finalBody);
2008
2241
  console.log(
@@ -2035,8 +2268,8 @@ async function assertWorkspaceInitialized4(basouRoot) {
2035
2268
 
2036
2269
  // src/commands/hook.ts
2037
2270
  import { open as open2, readFile as readFile2, stat as stat3 } from "fs/promises";
2038
- import { homedir as homedir4 } from "os";
2039
- import { join as join6 } from "path";
2271
+ import { homedir as homedir5 } from "os";
2272
+ import { join as join7 } from "path";
2040
2273
  import { fileURLToPath } from "url";
2041
2274
  import {
2042
2275
  buildStopHookCommand,
@@ -2046,61 +2279,6 @@ import {
2046
2279
  removeStopHook,
2047
2280
  upsertStopHook
2048
2281
  } from "@basou/core";
2049
-
2050
- // src/lib/durable-write.ts
2051
- import { randomUUID } from "crypto";
2052
- import { lstat, open, rename, stat as stat2, unlink as unlink2 } from "fs/promises";
2053
- import { basename as basename2, dirname, join as join5 } from "path";
2054
- async function assertNotSymlink(targetPath) {
2055
- try {
2056
- const st = await lstat(targetPath);
2057
- if (st.isSymbolicLink()) {
2058
- throw new Error(
2059
- "Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
2060
- );
2061
- }
2062
- } catch (error) {
2063
- if (error instanceof Error && error.code === "ENOENT") return;
2064
- throw error;
2065
- }
2066
- }
2067
- async function writeFileDurable(targetPath, content) {
2068
- const dir = dirname(targetPath);
2069
- const tmpPath = join5(dir, `.${basename2(targetPath)}.tmp.${randomUUID()}`);
2070
- let mode = 420;
2071
- try {
2072
- mode = (await stat2(targetPath)).mode & 511;
2073
- } catch (error) {
2074
- if (!(error instanceof Error && error.code === "ENOENT")) {
2075
- throw error;
2076
- }
2077
- }
2078
- let handle;
2079
- try {
2080
- handle = await open(tmpPath, "wx", mode);
2081
- await handle.writeFile(content, "utf8");
2082
- await handle.chmod(mode);
2083
- await handle.sync();
2084
- await handle.close();
2085
- handle = void 0;
2086
- await rename(tmpPath, targetPath);
2087
- } catch (error) {
2088
- if (handle) await handle.close().catch(() => void 0);
2089
- await unlink2(tmpPath).catch(() => void 0);
2090
- throw error;
2091
- }
2092
- try {
2093
- const dirHandle = await open(dir, "r");
2094
- try {
2095
- await dirHandle.sync();
2096
- } finally {
2097
- await dirHandle.close();
2098
- }
2099
- } catch {
2100
- }
2101
- }
2102
-
2103
- // src/commands/hook.ts
2104
2282
  var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
2105
2283
  function registerHookCommand(program) {
2106
2284
  const hook = program.command("hook").description(
@@ -2256,7 +2434,7 @@ function parseMinEdits(raw) {
2256
2434
  if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
2257
2435
  return Number(raw);
2258
2436
  }
2259
- var DEFAULT_CLAUDE_SETTINGS_PATH = join6(homedir4(), ".claude", "settings.json");
2437
+ var DEFAULT_CLAUDE_SETTINGS_PATH = join7(homedir5(), ".claude", "settings.json");
2260
2438
  function resolveCliEntry() {
2261
2439
  return fileURLToPath(import.meta.url);
2262
2440
  }
@@ -2421,8 +2599,8 @@ function describeHookMode(tiers) {
2421
2599
  // src/commands/import.ts
2422
2600
  import { createReadStream } from "fs";
2423
2601
  import { readdir, readFile as readFile3, rm, stat as stat4 } from "fs/promises";
2424
- import { homedir as homedir5 } from "os";
2425
- import { basename as basename3, dirname as dirname2, join as join7, resolve as resolve4 } from "path";
2602
+ import { homedir as homedir6 } from "os";
2603
+ import { basename as basename3, dirname as dirname2, join as join8, resolve as resolve4 } from "path";
2426
2604
  import { createInterface } from "readline";
2427
2605
  import {
2428
2606
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
@@ -2508,7 +2686,7 @@ async function doRunImportClaudeCode(options, ctx) {
2508
2686
  repoRoot: repositoryRoot,
2509
2687
  cwd: ctx.cwd ?? process.cwd()
2510
2688
  });
2511
- const projectsRoot = ctx.claudeProjectsDir ?? join7(homedir5(), ".claude", "projects");
2689
+ const projectsRoot = ctx.claudeProjectsDir ?? join8(homedir6(), ".claude", "projects");
2512
2690
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
2513
2691
  const projectSet = new Set(projectPaths);
2514
2692
  const candidates = files.map((file) => {
@@ -2547,7 +2725,7 @@ async function doRunImportCodex(options, ctx) {
2547
2725
  repoRoot: repositoryRoot,
2548
2726
  cwd: ctx.cwd ?? process.cwd()
2549
2727
  });
2550
- const sessionsRoot = ctx.codexSessionsDir ?? join7(homedir5(), ".codex", "sessions");
2728
+ const sessionsRoot = ctx.codexSessionsDir ?? join8(homedir6(), ".codex", "sessions");
2551
2729
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
2552
2730
  const candidates = rollouts.map(({ file, externalId }) => ({
2553
2731
  externalId,
@@ -2676,7 +2854,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2676
2854
  if (priors.length > 0 && options.force === true) {
2677
2855
  if (options.dryRun !== true) {
2678
2856
  for (const { sessionId } of priors) {
2679
- await rm(join7(paths.sessions, sessionId), { recursive: true, force: true });
2857
+ await rm(join8(paths.sessions, sessionId), { recursive: true, force: true });
2680
2858
  }
2681
2859
  }
2682
2860
  counts.replaced++;
@@ -2787,7 +2965,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2787
2965
  if (options.session !== void 0) {
2788
2966
  const matches = [];
2789
2967
  for (const projectPath of projectPaths) {
2790
- const file = join7(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2968
+ const file = join8(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2791
2969
  if (await pathExists(file)) matches.push(file);
2792
2970
  }
2793
2971
  if (matches.length === 0) {
@@ -2798,7 +2976,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2798
2976
  const files = [];
2799
2977
  let anyDirFound = false;
2800
2978
  for (const projectPath of projectPaths) {
2801
- const transcriptDir = join7(projectsRoot, encodeProjectDir(projectPath));
2979
+ const transcriptDir = join8(projectsRoot, encodeProjectDir(projectPath));
2802
2980
  let entries;
2803
2981
  try {
2804
2982
  entries = await readdir(transcriptDir);
@@ -2808,7 +2986,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2808
2986
  }
2809
2987
  anyDirFound = true;
2810
2988
  for (const name of entries) {
2811
- if (name.endsWith(".jsonl")) files.push(join7(transcriptDir, name));
2989
+ if (name.endsWith(".jsonl")) files.push(join8(transcriptDir, name));
2812
2990
  }
2813
2991
  }
2814
2992
  if (!anyDirFound) {
@@ -2865,7 +3043,7 @@ async function findRolloutFiles(sessionsRoot) {
2865
3043
  throw new Error("Failed to read Codex sessions directory", { cause: error });
2866
3044
  }
2867
3045
  for (const entry of entries) {
2868
- const full = join7(dir, entry.name);
3046
+ const full = join8(dir, entry.name);
2869
3047
  if (entry.isDirectory()) {
2870
3048
  await walk(full, false);
2871
3049
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -3304,13 +3482,13 @@ import {
3304
3482
  } from "@basou/core";
3305
3483
 
3306
3484
  // src/lib/hosts-config.ts
3307
- import { homedir as homedir6 } from "os";
3308
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve6 } from "path";
3485
+ import { homedir as homedir7 } from "os";
3486
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve6 } from "path";
3309
3487
  import { readYamlFile as readYamlFile4 } from "@basou/core";
3310
- var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
3488
+ var DEFAULT_HOSTS_CONFIG_PATH = join9(homedir7(), ".basou", "hosts.yaml");
3311
3489
  function expandTilde2(p) {
3312
- if (p === "~") return homedir6();
3313
- if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
3490
+ if (p === "~") return homedir7();
3491
+ if (p.startsWith("~/")) return join9(homedir7(), p.slice(2));
3314
3492
  return p;
3315
3493
  }
3316
3494
  function isRecord2(value) {
@@ -3361,7 +3539,7 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
3361
3539
 
3362
3540
  // src/lib/provenance-actions.ts
3363
3541
  import {
3364
- readMarkdownFile as readMarkdownFile3,
3542
+ readMarkdownFile as readMarkdownFile4,
3365
3543
  renderDecisions as renderDecisions2,
3366
3544
  renderHandoff as renderHandoff2,
3367
3545
  renderOrientation,
@@ -3445,7 +3623,7 @@ function importCodex(options, ctx) {
3445
3623
  }
3446
3624
  async function regenerateHandoff(paths, nowIso, callbacks) {
3447
3625
  const result = await renderHandoff2({ paths, nowIso, ...callbacks });
3448
- const existing = await readMarkdownFile3(paths.files.handoff);
3626
+ const existing = await readMarkdownFile4(paths.files.handoff);
3449
3627
  await writeMarkdownFile3(
3450
3628
  paths.files.handoff,
3451
3629
  renderWithMarkers3(existing, result.body, "handoff.md")
@@ -3459,7 +3637,7 @@ async function regenerateHandoff(paths, nowIso, callbacks) {
3459
3637
  }
3460
3638
  async function regenerateDecisions(paths, nowIso, callbacks) {
3461
3639
  const result = await renderDecisions2({ paths, nowIso, ...callbacks });
3462
- const existing = await readMarkdownFile3(paths.files.decisions);
3640
+ const existing = await readMarkdownFile4(paths.files.decisions);
3463
3641
  await writeMarkdownFile3(
3464
3642
  paths.files.decisions,
3465
3643
  renderWithMarkers3(existing, result.body, "decisions.md")
@@ -3620,13 +3798,13 @@ async function assertWorkspaceInitialized7(basouRoot) {
3620
3798
 
3621
3799
  // src/commands/portfolio.ts
3622
3800
  import { existsSync, statSync } from "fs";
3623
- import { join as join9 } from "path";
3801
+ import { join as join10 } from "path";
3624
3802
  function registerPortfolioCommand(program) {
3625
3803
  program.command("portfolio").description(
3626
3804
  "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
3805
  ).argument("[action]", "optional literal `list` (the only, and default, action)").option("--json", "Output the result as JSON").option(
3628
3806
  "--check",
3629
- "moved: the redundancy/footprint safety preflight is `basou view --portfolio --check` (this prints that pointer and exits)"
3807
+ "moved: the redundancy/footprint preflight and the capture-coverage report are `basou view --portfolio --check` (this prints that pointer and exits)"
3630
3808
  ).option("-v, --verbose", "Show error causes").action(async (action, opts) => {
3631
3809
  await runPortfolioCommand(action, opts);
3632
3810
  });
@@ -3641,7 +3819,7 @@ function isDirectory(path) {
3641
3819
  async function runPortfolioCommand(action, options, ctx = {}) {
3642
3820
  if (options.check === true) {
3643
3821
  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."
3822
+ "`basou portfolio` is a read-only listing; it has no preflight.\nRun `basou view --portfolio --check` for the redundancy/footprint check and the capture-coverage report."
3645
3823
  );
3646
3824
  process.exitCode = 1;
3647
3825
  return;
@@ -3666,7 +3844,7 @@ async function runPortfolioList(options, ctx = {}) {
3666
3844
  async function doRunPortfolioList(options, ctx) {
3667
3845
  const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
3668
3846
  const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
3669
- const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join9(p, ".basou")));
3847
+ const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join10(p, ".basou")));
3670
3848
  const workspaces = await loadPortfolioConfig(configPath);
3671
3849
  const result = {
3672
3850
  configPath,
@@ -3706,7 +3884,7 @@ function renderPortfolioList(result) {
3706
3884
  }
3707
3885
  lines.push("");
3708
3886
  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."
3887
+ "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 preflight and the capture-coverage report (which session logs no registered workspace imports)."
3710
3888
  );
3711
3889
  return lines.join("\n");
3712
3890
  }
@@ -3731,7 +3909,7 @@ import {
3731
3909
  writeFileSync,
3732
3910
  writeSync
3733
3911
  } from "fs";
3734
- import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join10, relative as relative2, resolve as resolve7 } from "path";
3912
+ import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve7 } from "path";
3735
3913
  import {
3736
3914
  appendBasouGitignore as appendBasouGitignore2,
3737
3915
  basouPaths as basouPaths10,
@@ -3742,7 +3920,7 @@ import {
3742
3920
  GENERATED_START,
3743
3921
  instructionMode,
3744
3922
  isGitNotFound,
3745
- parseMarkers,
3923
+ parseMarkers as parseMarkers2,
3746
3924
  pathBasename,
3747
3925
  planArchive,
3748
3926
  planGitignore,
@@ -3750,9 +3928,9 @@ import {
3750
3928
  planRosterAdoption,
3751
3929
  planWorkspaceView,
3752
3930
  readManifest as readManifest6,
3753
- readMarkdownFile as readMarkdownFile4,
3931
+ readMarkdownFile as readMarkdownFile5,
3754
3932
  reconcileSourceRoots,
3755
- removeMarkerSection,
3933
+ removeMarkerSection as removeMarkerSection2,
3756
3934
  renderAnchorStarter,
3757
3935
  renderViewPresetBlock,
3758
3936
  renderWithMarkers as renderWithMarkers4,
@@ -4113,7 +4291,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
4113
4291
  } catch {
4114
4292
  return { path: declaredPath, kind: "unresolved" };
4115
4293
  }
4116
- return { path: declaredPath, kind: existsSync2(join10(real, ".git")) ? "repo" : "non-repo" };
4294
+ return { path: declaredPath, kind: existsSync2(join11(real, ".git")) ? "repo" : "non-repo" };
4117
4295
  }
4118
4296
  async function doRunProjectAdopt(options, ctx) {
4119
4297
  const cwd = ctx.cwd ?? process.cwd();
@@ -4217,7 +4395,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
4217
4395
  } catch {
4218
4396
  return { ...base, reachable: false, instructionFiles: [] };
4219
4397
  }
4220
- if (!existsSync2(join10(real, ".git"))) {
4398
+ if (!existsSync2(join11(real, ".git"))) {
4221
4399
  return { ...base, reachable: false, instructionFiles: [] };
4222
4400
  }
4223
4401
  try {
@@ -4225,7 +4403,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
4225
4403
  for (const name of INSTRUCTION_FILES) {
4226
4404
  let present = true;
4227
4405
  try {
4228
- lstatSync(join10(real, name));
4406
+ lstatSync(join11(real, name));
4229
4407
  } catch {
4230
4408
  present = false;
4231
4409
  }
@@ -4336,10 +4514,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
4336
4514
  } catch {
4337
4515
  return { ...base, reachable: false, currentLines: [] };
4338
4516
  }
4339
- if (!existsSync2(join10(real, ".git"))) {
4517
+ if (!existsSync2(join11(real, ".git"))) {
4340
4518
  return { ...base, reachable: false, currentLines: [] };
4341
4519
  }
4342
- return { ...base, reachable: true, currentLines: readGitignoreLines(join10(real, ".gitignore")) };
4520
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join11(real, ".gitignore")) };
4343
4521
  }
4344
4522
  function hasErrorCode(error) {
4345
4523
  return error instanceof Error && typeof error.code === "string";
@@ -4353,7 +4531,7 @@ function readGitignoreLines(file) {
4353
4531
  }
4354
4532
  }
4355
4533
  function applyGitignorePlan(repositoryRoot, plan) {
4356
- const file = join10(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4534
+ const file = join11(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
4357
4535
  let existing = "";
4358
4536
  try {
4359
4537
  existing = readFileSync(file, "utf8");
@@ -4493,7 +4671,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4493
4671
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4494
4672
  }
4495
4673
  if (real === anchorReal) {
4496
- const anchorCanonical = join10(real, CANONICAL_FILE);
4674
+ const anchorCanonical = join11(real, CANONICAL_FILE);
4497
4675
  const anchorState = anchorCanonicalState(anchorCanonical);
4498
4676
  if (anchorState === "absent") {
4499
4677
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
@@ -4513,7 +4691,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4513
4691
  anchorCanonical,
4514
4692
  "self"
4515
4693
  ).map((spec) => {
4516
- const { state, actualTarget } = inspectSymlink(join10(real, spec.name), spec.target);
4694
+ const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
4517
4695
  return {
4518
4696
  name: spec.name,
4519
4697
  expectedTarget: spec.target,
@@ -4530,16 +4708,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4530
4708
  files: anchorFiles
4531
4709
  };
4532
4710
  }
4533
- if (!existsSync2(join10(real, ".git"))) {
4711
+ if (!existsSync2(join11(real, ".git"))) {
4534
4712
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4535
4713
  }
4536
- const canonicalFile = isSelf ? join10(real, CANONICAL_FILE) : join10(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4714
+ const canonicalFile = isSelf ? join11(real, CANONICAL_FILE) : join11(anchorReal, "agents", basename5(real), CANONICAL_FILE);
4537
4715
  if (!existsSync2(canonicalFile)) {
4538
4716
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
4539
4717
  }
4540
4718
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
4541
4719
  (spec) => {
4542
- const { state, actualTarget } = inspectSymlink(join10(real, spec.name), spec.target);
4720
+ const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
4543
4721
  return {
4544
4722
  name: spec.name,
4545
4723
  expectedTarget: spec.target,
@@ -4568,7 +4746,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
4568
4746
  const created = [];
4569
4747
  const failed = [];
4570
4748
  for (const { name, target } of plan.toCreate) {
4571
- const filePath = join10(real, name);
4749
+ const filePath = join11(real, name);
4572
4750
  try {
4573
4751
  mkdirSync(dirname3(filePath), { recursive: true });
4574
4752
  symlinkSync(target, filePath);
@@ -4602,7 +4780,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
4602
4780
  if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
4603
4781
  const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
4604
4782
  (spec) => {
4605
- const { state, actualTarget } = inspectSymlink(join10(viewDir, spec.name), spec.target);
4783
+ const { state, actualTarget } = inspectSymlink(join11(viewDir, spec.name), spec.target);
4606
4784
  return {
4607
4785
  name: spec.name,
4608
4786
  expectedTarget: spec.target,
@@ -4618,7 +4796,7 @@ function applyViewSymlinks(viewDir, files) {
4618
4796
  const failed = [];
4619
4797
  for (const f of files) {
4620
4798
  if (f.state !== "missing") continue;
4621
- const filePath = join10(viewDir, f.name);
4799
+ const filePath = join11(viewDir, f.name);
4622
4800
  try {
4623
4801
  mkdirSync(dirname3(filePath), { recursive: true });
4624
4802
  symlinkSync(f.expectedTarget, filePath);
@@ -4840,7 +5018,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
4840
5018
  return realpathSync(abs);
4841
5019
  } catch {
4842
5020
  try {
4843
- return join10(realpathSync(dirname3(abs)), basename5(abs));
5021
+ return join11(realpathSync(dirname3(abs)), basename5(abs));
4844
5022
  } catch {
4845
5023
  return abs;
4846
5024
  }
@@ -4858,7 +5036,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
4858
5036
  return { path: entry.path, reachable: false };
4859
5037
  }
4860
5038
  const linkName = basename5(repoReal);
4861
- const { state, actualTarget } = inspectSymlink(join10(viewDir, linkName), expectedTarget);
5039
+ const { state, actualTarget } = inspectSymlink(join11(viewDir, linkName), expectedTarget);
4862
5040
  return {
4863
5041
  path: entry.path,
4864
5042
  reachable: true,
@@ -4872,7 +5050,7 @@ function applyViewPlan(viewDir, toCreate) {
4872
5050
  const created = [];
4873
5051
  const failed = [];
4874
5052
  for (const { name, target } of toCreate) {
4875
- const filePath = join10(viewDir, name);
5053
+ const filePath = join11(viewDir, name);
4876
5054
  try {
4877
5055
  mkdirSync(dirname3(filePath), { recursive: true });
4878
5056
  symlinkSync(target, filePath);
@@ -4887,7 +5065,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
4887
5065
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
4888
5066
  );
4889
5067
  function classifyViewLink(viewDir, name, rosterRealpaths) {
4890
- const filePath = join10(viewDir, name);
5068
+ const filePath = join11(viewDir, name);
4891
5069
  let isLink;
4892
5070
  try {
4893
5071
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -4916,7 +5094,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
4916
5094
  if (!isDir) {
4917
5095
  return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
4918
5096
  }
4919
- return { target, kind: existsSync2(join10(resolved, ".git")) ? "repo" : "non-repo" };
5097
+ return { target, kind: existsSync2(join11(resolved, ".git")) ? "repo" : "non-repo" };
4920
5098
  }
4921
5099
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
4922
5100
  let names;
@@ -4941,7 +5119,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
4941
5119
  const pruned = [];
4942
5120
  const failed = [];
4943
5121
  for (const { name } of toPrune) {
4944
- const filePath = join10(viewDir, name);
5122
+ const filePath = join11(viewDir, name);
4945
5123
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
4946
5124
  if (c === null || c.kind !== "repo") {
4947
5125
  failed.push({
@@ -5154,10 +5332,10 @@ async function runProjectPreset(options, ctx = {}) {
5154
5332
  }
5155
5333
  }
5156
5334
  function canonicalFileFor(anchorReal, canonicalName) {
5157
- return join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5335
+ return join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5158
5336
  }
5159
5337
  function canonicalLabelFor(canonicalName) {
5160
- return join10("agents", canonicalName, CANONICAL_FILE);
5338
+ return join11("agents", canonicalName, CANONICAL_FILE);
5161
5339
  }
5162
5340
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5163
5341
  const declared = {
@@ -5178,13 +5356,13 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5178
5356
  if (real === anchorReal) {
5179
5357
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
5180
5358
  }
5181
- if (!existsSync2(join10(real, ".git"))) {
5359
+ if (!existsSync2(join11(real, ".git"))) {
5182
5360
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
5183
5361
  }
5184
5362
  const canonicalName = basename5(real);
5185
5363
  let content;
5186
5364
  try {
5187
- content = await readMarkdownFile4(canonicalFileFor(anchorReal, canonicalName));
5365
+ content = await readMarkdownFile5(canonicalFileFor(anchorReal, canonicalName));
5188
5366
  } catch {
5189
5367
  return {
5190
5368
  ...declared,
@@ -5204,7 +5382,7 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5204
5382
  canonicalPresent: false
5205
5383
  };
5206
5384
  }
5207
- const section = parseMarkers(content);
5385
+ const section = parseMarkers2(content);
5208
5386
  return {
5209
5387
  ...declared,
5210
5388
  isAnchor: false,
@@ -5267,7 +5445,7 @@ function gatherViewPreset(repositoryRoot, anchorReal, viewName, roster) {
5267
5445
  }
5268
5446
  return { kind: "unreadable", canonicalName: viewName, viewName };
5269
5447
  }
5270
- const section = parseMarkers(content);
5448
+ const section = parseMarkers2(content);
5271
5449
  if (section.kind === "ok") {
5272
5450
  if (normalizeViewBlock(section.generated) === normalizeViewBlock(desiredBlock)) {
5273
5451
  return { kind: "in-sync", canonicalName: viewName, viewName };
@@ -5293,7 +5471,7 @@ async function applyViewPreset(anchorReal, outcome) {
5293
5471
  }
5294
5472
  if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
5295
5473
  if (outcome.action === "create") mkdirSync(dirname3(file), { recursive: true });
5296
- const existing = await readMarkdownFile4(file);
5474
+ const existing = await readMarkdownFile5(file);
5297
5475
  await writeMarkdownFile5(file, renderWithMarkers4(existing, outcome.block, label));
5298
5476
  }
5299
5477
  async function applyPresetPlan(anchorReal, plan) {
@@ -5307,7 +5485,7 @@ async function applyPresetPlan(anchorReal, plan) {
5307
5485
  }
5308
5486
  if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
5309
5487
  if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
5310
- const existing = await readMarkdownFile4(file);
5488
+ const existing = await readMarkdownFile5(file);
5311
5489
  await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
5312
5490
  }
5313
5491
  function presetFailureReason(error) {
@@ -5560,24 +5738,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
5560
5738
  const instructionFiles = [];
5561
5739
  for (const name of INSTRUCTION_FILES) {
5562
5740
  try {
5563
- lstatSync(join10(real, name));
5741
+ lstatSync(join11(real, name));
5564
5742
  instructionFiles.push(name);
5565
5743
  } catch {
5566
5744
  }
5567
5745
  }
5568
5746
  let ignored;
5569
5747
  try {
5570
- ignored = new Set(readGitignoreLines(join10(real, ".gitignore")).map((l) => l.trim()));
5748
+ ignored = new Set(readGitignoreLines(join11(real, ".gitignore")).map((l) => l.trim()));
5571
5749
  } catch {
5572
5750
  ignored = /* @__PURE__ */ new Set();
5573
5751
  }
5574
5752
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
5575
- const canonical2 = existsSync2(join10(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5753
+ const canonical2 = existsSync2(join11(anchorReal, "agents", canonicalName, CANONICAL_FILE));
5576
5754
  let viewLink = false;
5577
5755
  const viewPath = manifest.workspace.view;
5578
5756
  if (viewPath !== void 0) {
5579
5757
  try {
5580
- lstatSync(join10(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5758
+ lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), canonicalName));
5581
5759
  viewLink = true;
5582
5760
  } catch {
5583
5761
  }
@@ -5591,11 +5769,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
5591
5769
  };
5592
5770
  }
5593
5771
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
5594
- const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5772
+ const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5595
5773
  return expectedSymlinkTargets(repoReal, canonicalFile);
5596
5774
  }
5597
5775
  function viewLinkPointsAt(viewDir, name, repoReal) {
5598
- const filePath = join10(viewDir, name);
5776
+ const filePath = join11(viewDir, name);
5599
5777
  try {
5600
5778
  if (!lstatSync(filePath).isSymbolicLink()) return false;
5601
5779
  const target = readlinkSync(filePath);
@@ -5606,7 +5784,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
5606
5784
  }
5607
5785
  }
5608
5786
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
5609
- const filePath = join10(viewDir, name);
5787
+ const filePath = join11(viewDir, name);
5610
5788
  try {
5611
5789
  if (!lstatSync(filePath).isSymbolicLink()) return false;
5612
5790
  const target = readlinkSync(filePath);
@@ -5657,7 +5835,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5657
5835
  if (!isAnchor) {
5658
5836
  if (repoReal !== void 0) {
5659
5837
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
5660
- const { state, actualTarget } = inspectSymlink(join10(repoReal, spec.name), spec.target);
5838
+ const { state, actualTarget } = inspectSymlink(join11(repoReal, spec.name), spec.target);
5661
5839
  if (isSelf) {
5662
5840
  if (state !== "missing")
5663
5841
  items.push({
@@ -5694,7 +5872,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5694
5872
  }
5695
5873
  let ignored;
5696
5874
  try {
5697
- ignored = new Set(readGitignoreLines(join10(repoReal, ".gitignore")).map((l) => l.trim()));
5875
+ ignored = new Set(readGitignoreLines(join11(repoReal, ".gitignore")).map((l) => l.trim()));
5698
5876
  for (const p of INSTRUCTION_FILES) {
5699
5877
  if (ignored.has(p) || ignored.has(`/${p}`)) {
5700
5878
  items.push({
@@ -5717,7 +5895,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5717
5895
  const viewPath = manifest.workspace.view;
5718
5896
  if (viewPath !== void 0) {
5719
5897
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
5720
- const linkPath = join10(viewDir, canonicalName);
5898
+ const linkPath = join11(viewDir, canonicalName);
5721
5899
  let isLink = false;
5722
5900
  try {
5723
5901
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -5743,8 +5921,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5743
5921
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
5744
5922
  }
5745
5923
  }
5746
- const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5747
- const canonicalLabel = join10("agents", canonicalName, CANONICAL_FILE);
5924
+ const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5925
+ const canonicalLabel = join11("agents", canonicalName, CANONICAL_FILE);
5748
5926
  let canonicalIsLink = false;
5749
5927
  try {
5750
5928
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -5771,7 +5949,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5771
5949
  });
5772
5950
  }
5773
5951
  if (content !== void 0 && content !== "") {
5774
- const section = parseMarkers(content);
5952
+ const section = parseMarkers2(content);
5775
5953
  if (section.kind === "ok" && canonicalShared) {
5776
5954
  items.push({
5777
5955
  kind: "canonical-block",
@@ -5787,7 +5965,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
5787
5965
  note: "repo could not be resolved, so ownership cannot be verified (check manually)"
5788
5966
  });
5789
5967
  } else if (section.kind === "ok") {
5790
- const emptyAfter = removeMarkerSection(content, canonicalLabel).trim().length === 0;
5968
+ const emptyAfter = removeMarkerSection2(content, canonicalLabel).trim().length === 0;
5791
5969
  items.push({
5792
5970
  kind: "canonical-block",
5793
5971
  label: canonicalLabel,
@@ -5848,12 +6026,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5848
6026
  );
5849
6027
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
5850
6028
  const expected = expectedByName.get(item.label);
5851
- if (repoReal === null || expected === void 0 || inspectSymlink(join10(repoReal, item.label), expected).state !== "correct") {
6029
+ if (repoReal === null || expected === void 0 || inspectSymlink(join11(repoReal, item.label), expected).state !== "correct") {
5852
6030
  changed(item.label);
5853
6031
  continue;
5854
6032
  }
5855
6033
  try {
5856
- unlinkSync(join10(repoReal, item.label));
6034
+ unlinkSync(join11(repoReal, item.label));
5857
6035
  removed.push(item.label);
5858
6036
  } catch (error) {
5859
6037
  failed.push({ label: item.label, message: failureReason(error) });
@@ -5872,7 +6050,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5872
6050
  continue;
5873
6051
  }
5874
6052
  try {
5875
- unlinkSync(join10(viewDir, item.label));
6053
+ unlinkSync(join11(viewDir, item.label));
5876
6054
  removed.push(`view/${item.label}`);
5877
6055
  } catch (error) {
5878
6056
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -5880,7 +6058,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5880
6058
  }
5881
6059
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
5882
6060
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
5883
- const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6061
+ const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5884
6062
  try {
5885
6063
  if (lstatSync(canonicalFile).isSymbolicLink()) {
5886
6064
  changed(item.label);
@@ -5900,11 +6078,11 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
5900
6078
  }
5901
6079
  try {
5902
6080
  const content = readFileSync(fd, "utf8");
5903
- if (parseMarkers(content).kind !== "ok") {
6081
+ if (parseMarkers2(content).kind !== "ok") {
5904
6082
  changed(item.label);
5905
6083
  continue;
5906
6084
  }
5907
- const next = Buffer.from(removeMarkerSection(content, item.label), "utf8");
6085
+ const next = Buffer.from(removeMarkerSection2(content, item.label), "utf8");
5908
6086
  ftruncateSync(fd, 0);
5909
6087
  writeSync(fd, next, 0, next.length, 0);
5910
6088
  removed.push(item.label);
@@ -6154,12 +6332,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
6154
6332
  } catch {
6155
6333
  return { canonicalDirOld: false, viewLinkOld: false };
6156
6334
  }
6157
- const canonicalDirOld = existsSync2(join10(anchorReal, "agents", oldBasename));
6335
+ const canonicalDirOld = existsSync2(join11(anchorReal, "agents", oldBasename));
6158
6336
  let viewLinkOld = false;
6159
6337
  const viewPath = manifest.workspace.view;
6160
6338
  if (viewPath !== void 0) {
6161
6339
  try {
6162
- lstatSync(join10(resolveViewDir(repositoryRoot, viewPath), oldBasename));
6340
+ lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), oldBasename));
6163
6341
  viewLinkOld = true;
6164
6342
  } catch {
6165
6343
  }
@@ -6517,7 +6695,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
6517
6695
  console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
6518
6696
  return;
6519
6697
  }
6520
- const anchorDoc = join10(repositoryRoot, CANONICAL_FILE);
6698
+ const anchorDoc = join11(repositoryRoot, CANONICAL_FILE);
6521
6699
  if (pathPresent(anchorDoc)) {
6522
6700
  console.log(
6523
6701
  `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
@@ -6579,7 +6757,7 @@ function regularFileSpokes(repoReal) {
6579
6757
  const out = [];
6580
6758
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
6581
6759
  try {
6582
- const st = lstatSync(join10(repoReal, spoke));
6760
+ const st = lstatSync(join11(repoReal, spoke));
6583
6761
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
6584
6762
  } catch {
6585
6763
  }
@@ -6625,8 +6803,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
6625
6803
  };
6626
6804
  }
6627
6805
  const isAnchor = argReal === anchorReal;
6628
- const reachable = existsSync2(join10(argReal, ".git"));
6629
- const canonicalFile = join10(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6806
+ const reachable = existsSync2(join11(argReal, ".git"));
6807
+ const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6630
6808
  return {
6631
6809
  path,
6632
6810
  declared,
@@ -6635,13 +6813,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
6635
6813
  reachable,
6636
6814
  canonicalName,
6637
6815
  ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
6638
- agentsState: inspectAgentsState(join10(argReal, CANONICAL_FILE)),
6816
+ agentsState: inspectAgentsState(join11(argReal, CANONICAL_FILE)),
6639
6817
  canonicalExists: pathPresent(canonicalFile),
6640
6818
  regularSpokes: regularFileSpokes(argReal)
6641
6819
  };
6642
6820
  }
6643
6821
  function relocateAgentsFile(repoReal, canonicalFile) {
6644
- const agentsFile = join10(repoReal, CANONICAL_FILE);
6822
+ const agentsFile = join11(repoReal, CANONICAL_FILE);
6645
6823
  try {
6646
6824
  mkdirSync(dirname3(canonicalFile), { recursive: true });
6647
6825
  } catch (error) {
@@ -6675,7 +6853,7 @@ function gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster) {
6675
6853
  if (hasErrorCode(error) && error.code === "ENOENT") return { kind: "absent", viewName };
6676
6854
  return { kind: "unreadable", viewName };
6677
6855
  }
6678
- const section = parseMarkers(content);
6856
+ const section = parseMarkers2(content);
6679
6857
  if (section.kind === "ok") return { kind: "already-marked", viewName };
6680
6858
  if (section.kind === "no_markers") {
6681
6859
  const block = renderViewPresetBlock({
@@ -6696,7 +6874,7 @@ async function applyViewRetrofit(anchorReal, outcome) {
6696
6874
  isLink = false;
6697
6875
  }
6698
6876
  if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
6699
- const existing = await readMarkdownFile4(file);
6877
+ const existing = await readMarkdownFile5(file);
6700
6878
  await writeMarkdownFile5(file, seedMarkers(existing, outcome.block, label));
6701
6879
  }
6702
6880
  async function doRunProjectRetrofit(repo, options, ctx) {
@@ -6758,7 +6936,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
6758
6936
  let failure;
6759
6937
  let partial = false;
6760
6938
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
6761
- const canonicalFile = join10(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6939
+ const canonicalFile = join11(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
6762
6940
  const res = relocateAgentsFile(argReal, canonicalFile);
6763
6941
  if (res.ok) {
6764
6942
  applied = true;
@@ -6961,118 +7139,6 @@ function renderProjectRetrofit(result) {
6961
7139
  import { readFile as readFile4 } from "fs/promises";
6962
7140
  import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdownFile as readMarkdownFile6 } from "@basou/core";
6963
7141
 
6964
- // src/lib/context-channel.ts
6965
- import { homedir as homedir7 } from "os";
6966
- import { join as join11 } from "path";
6967
- import {
6968
- ORIENTATION_END,
6969
- ORIENTATION_START,
6970
- parseMarkers as parseMarkers2,
6971
- readMarkdownFile as readMarkdownFile5,
6972
- removeMarkerSection as removeMarkerSection2
6973
- } from "@basou/core";
6974
- var CODEX_TARGET_PATH = join11(homedir7(), ".codex", "AGENTS.md");
6975
- var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
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. -->";
6977
- function buildTargetBody(existing, block, markers) {
6978
- const wrapped = `${markers.start}
6979
- ${block}${markers.end}
6980
- `;
6981
- if (existing === null || existing === "") return wrapped;
6982
- const section = parseMarkers2(existing, markers);
6983
- switch (section.kind) {
6984
- case "ok":
6985
- return `${section.before}${markers.start}
6986
- ${block}${markers.end}${section.after}`;
6987
- case "no_markers": {
6988
- const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
6989
- return `${existing}${sep}${wrapped}`;
6990
- }
6991
- default:
6992
- throw new Error(
6993
- "The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
6994
- );
6995
- }
6996
- }
6997
- async function backupOnce(target, existing) {
6998
- if (existing === null) return;
6999
- const bak = `${target}.basou-bak`;
7000
- const already = await readMarkdownFile5(bak);
7001
- if (already !== null) return;
7002
- await writeFileDurable(bak, existing);
7003
- }
7004
- async function syncMarkerBlock(opts) {
7005
- const { target, markers, block } = opts;
7006
- await assertNotSymlink(target);
7007
- const existing = await readMarkdownFile5(target);
7008
- const newBody = buildTargetBody(existing, block, markers);
7009
- if (newBody === existing) return { action: "unchanged" };
7010
- const hadBlock = existing !== null && parseMarkers2(existing, markers).kind === "ok";
7011
- const action = hadBlock ? "updated" : "installed";
7012
- if (opts.dryRun === true) return { action };
7013
- const recheck = await readMarkdownFile5(target);
7014
- if (recheck !== existing) {
7015
- throw new Error(
7016
- "The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
7017
- );
7018
- }
7019
- await backupOnce(target, existing);
7020
- await writeFileDurable(target, newBody);
7021
- return { action };
7022
- }
7023
- function assertNoMarkerLine(body, markers) {
7024
- for (const line of body.split(/\r?\n/)) {
7025
- if (line === markers.start || line === markers.end) {
7026
- throw new Error(
7027
- "The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
7028
- );
7029
- }
7030
- }
7031
- }
7032
- async function removeMarkerBlock(opts) {
7033
- const { target, markers, fileLabel } = opts;
7034
- await assertNotSymlink(target);
7035
- const existing = await readMarkdownFile5(target);
7036
- if (existing === null) return { removed: false };
7037
- const newBody = removeMarkerSection2(existing, fileLabel, markers);
7038
- if (newBody === existing) return { removed: false };
7039
- if (opts.dryRun === true) return { removed: true };
7040
- const recheck = await readMarkdownFile5(target);
7041
- if (recheck !== existing) {
7042
- throw new Error(
7043
- "The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
7044
- );
7045
- }
7046
- await backupOnce(target, existing);
7047
- await writeFileDurable(target, newBody);
7048
- return { removed: true };
7049
- }
7050
- async function syncOrientationChannel(opts) {
7051
- assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
7052
- const block = `${ORIENTATION_MANAGED_NOTE}
7053
-
7054
- ${opts.body.replace(/\s+$/, "")}
7055
- `;
7056
- return syncMarkerBlock({
7057
- target: opts.target ?? CODEX_TARGET_PATH,
7058
- markers: ORIENTATION_MARKERS,
7059
- block,
7060
- ...opts.dryRun === true ? { dryRun: true } : {}
7061
- });
7062
- }
7063
- async function renderOrientationToCodexChannel(opts) {
7064
- const body = await readMarkdownFile5(opts.orientationPath);
7065
- if (body === null) return null;
7066
- const { action } = await syncOrientationChannel({
7067
- body,
7068
- ...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
7069
- });
7070
- return {
7071
- action,
7072
- line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
7073
- };
7074
- }
7075
-
7076
7142
  // src/lib/protocols-config.ts
7077
7143
  import { homedir as homedir8 } from "os";
7078
7144
  import { isAbsolute as isAbsolute4, join as join12, resolve as resolve8 } from "path";
@@ -7282,9 +7348,29 @@ async function doRunProtocolUnsync(options) {
7282
7348
  }
7283
7349
 
7284
7350
  // src/commands/refresh.ts
7285
- import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths11, findErrorCode as findErrorCode9 } from "@basou/core";
7351
+ import {
7352
+ assertBasouRootSafe as assertBasouRootSafe9,
7353
+ basouPaths as basouPaths11,
7354
+ findErrorCode as findErrorCode9,
7355
+ readManifest as readManifest7
7356
+ } from "@basou/core";
7286
7357
  import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
7287
7358
 
7359
+ // src/lib/channel-policy.ts
7360
+ function decideCodexChannel(manifest) {
7361
+ if (manifest.policies?.confidential === true) return { write: false, reason: "confidential" };
7362
+ if (manifest.channels?.codex === true) return { write: true };
7363
+ return { write: false, reason: "not_enabled" };
7364
+ }
7365
+ function describeCodexChannelSkip(reason) {
7366
+ switch (reason) {
7367
+ case "confidential":
7368
+ return "codex channel: skipped (confidential workspace \u2014 nothing is written to the user-global ~/.codex/AGENTS.md)";
7369
+ case "not_enabled":
7370
+ return "codex channel: skipped (this workspace has not opted in; the user-global ~/.codex/AGENTS.md is left untouched \u2014 see docs/spec/schemas.md \xA74.2)";
7371
+ }
7372
+ }
7373
+
7288
7374
  // src/commands/refresh-watch.ts
7289
7375
  import { readdir as readdir2, stat as stat5 } from "fs/promises";
7290
7376
  import { homedir as homedir9 } from "os";
@@ -7567,24 +7653,47 @@ async function computeRefresh(options, ctx) {
7567
7653
  }
7568
7654
  async function doRunRefresh(options, ctx) {
7569
7655
  const { result, paths } = await computeRefresh(options, ctx);
7570
- const channelLine = options.dryRun === true ? null : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
7656
+ const channel = options.dryRun === true ? { outcome: { status: "skipped", reason: "dry_run" }, line: null } : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
7657
+ const reported = { ...result, codexChannel: channel.outcome };
7571
7658
  if (options.json === true) {
7572
- console.log(JSON.stringify(result));
7659
+ console.log(JSON.stringify(reported));
7573
7660
  } else {
7574
7661
  printRefreshSummary(result);
7575
- if (channelLine !== null) console.log(channelLine);
7662
+ if (channel.line !== null) console.log(channel.line);
7576
7663
  }
7577
- return result;
7664
+ return reported;
7578
7665
  }
7579
7666
  async function syncCodexOrientationChannel(paths, channelPath) {
7667
+ let decision;
7668
+ try {
7669
+ decision = decideCodexChannel(await readManifest7(paths));
7670
+ } catch (error) {
7671
+ const detail = error instanceof Error ? error.message : String(error);
7672
+ return {
7673
+ outcome: { status: "skipped", reason: "error", detail },
7674
+ line: `codex channel: skipped (manifest could not be re-read, so the opt-in cannot be confirmed: ${detail})`
7675
+ };
7676
+ }
7677
+ if (!decision.write) {
7678
+ return {
7679
+ outcome: { status: "skipped", reason: decision.reason },
7680
+ line: describeCodexChannelSkip(decision.reason)
7681
+ };
7682
+ }
7580
7683
  try {
7581
7684
  const rendered = await renderOrientationToCodexChannel({
7582
7685
  orientationPath: paths.files.orientation,
7583
7686
  ...channelPath !== void 0 ? { channelPath } : {}
7584
7687
  });
7585
- return rendered === null ? null : rendered.line;
7688
+ if (rendered === null)
7689
+ return { outcome: { status: "skipped", reason: "no_orientation" }, line: null };
7690
+ return { outcome: { status: "written", action: rendered.action }, line: rendered.line };
7586
7691
  } catch (error) {
7587
- return `codex channel skipped: ${error instanceof Error ? error.message : String(error)}`;
7692
+ const detail = error instanceof Error ? error.message : String(error);
7693
+ return {
7694
+ outcome: { status: "skipped", reason: "error", detail },
7695
+ line: `codex channel skipped: ${detail}`
7696
+ };
7588
7697
  }
7589
7698
  }
7590
7699
  function describeImport(outcome) {
@@ -7737,7 +7846,7 @@ import {
7737
7846
  findErrorCode as findErrorCode11,
7738
7847
  findUnbindableRepos,
7739
7848
  parseReviewRecordInput,
7740
- readManifest as readManifest7,
7849
+ readManifest as readManifest8,
7741
7850
  resolveRepoRoot,
7742
7851
  sanitizePath as sanitizePath2
7743
7852
  } from "@basou/core";
@@ -7824,7 +7933,7 @@ async function doRunReviewRecord(options, ctx) {
7824
7933
  }
7825
7934
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
7826
7935
  const occurredAt = now.toISOString();
7827
- const manifest = await readManifest7(paths);
7936
+ const manifest = await readManifest8(paths);
7828
7937
  const invocationArgs = options.file !== void 0 ? [
7829
7938
  "--file",
7830
7939
  sanitizePath2(resolve10(cwd, options.file), {
@@ -8178,7 +8287,7 @@ import {
8178
8287
  getSnapshot as getSnapshot2,
8179
8288
  overwriteYamlFile as overwriteYamlFile2,
8180
8289
  prefixedUlid as prefixedUlid4,
8181
- readManifest as readManifest8,
8290
+ readManifest as readManifest9,
8182
8291
  readYamlFile as readYamlFile6,
8183
8292
  resolveClaudeCodeCommand,
8184
8293
  resolveCodexCommand,
@@ -8238,7 +8347,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
8238
8347
  const repoRoot = await resolveRepositoryRootForRun(cwd);
8239
8348
  const paths = basouPaths15(repoRoot);
8240
8349
  await assertBasouRootSafe12(paths.root);
8241
- const manifest = await readManifest8(paths);
8350
+ const manifest = await readManifest9(paths);
8242
8351
  const sessionId = prefixedUlid4("ses");
8243
8352
  const sessionDir = join14(paths.sessions, sessionId);
8244
8353
  await mkdir2(sessionDir, { recursive: true });
@@ -8596,6 +8705,8 @@ async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
8596
8705
  try {
8597
8706
  const root = await resolveBasouRootForCommand(cwd, "run");
8598
8707
  const paths = basouPaths15(root);
8708
+ const decision = decideCodexChannel(await readManifest9(paths));
8709
+ if (!decision.write) return describeCodexChannelSkip(decision.reason);
8599
8710
  const rendered = await renderOrientationToCodexChannel({
8600
8711
  orientationPath: paths.files.orientation,
8601
8712
  ...ctx.codexChannelPath !== void 0 ? { channelPath: ctx.codexChannelPath } : {}
@@ -8619,7 +8730,7 @@ import {
8619
8730
  importSessionFromJson as importSessionFromJson2,
8620
8731
  loadSessionEntries as loadSessionEntries2,
8621
8732
  readAllEvents,
8622
- readManifest as readManifest9,
8733
+ readManifest as readManifest10,
8623
8734
  readYamlFile as readYamlFile7,
8624
8735
  rechainSessionInPlace,
8625
8736
  resolveSessionId as resolveSessionId3,
@@ -9031,7 +9142,7 @@ async function doRunSessionImport(options, ctx) {
9031
9142
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
9032
9143
  const paths = basouPaths16(repositoryRoot);
9033
9144
  await assertWorkspaceInitialized11(paths.root);
9034
- const manifest = await readManifest9(paths);
9145
+ const manifest = await readManifest10(paths);
9035
9146
  const rawBody = await readInputFile(options.from);
9036
9147
  const json = parseJsonStrict(rawBody);
9037
9148
  const parsed = SessionImportPayloadSchema2.safeParse(json);
@@ -9413,7 +9524,7 @@ import {
9413
9524
  basouPaths as basouPaths18,
9414
9525
  buildStatusSnapshot,
9415
9526
  findErrorCode as findErrorCode14,
9416
- readManifest as readManifest10,
9527
+ readManifest as readManifest11,
9417
9528
  resolveRepositoryRoot as resolveRepositoryRoot12,
9418
9529
  writeStatus
9419
9530
  } from "@basou/core";
@@ -9444,7 +9555,7 @@ async function doRunStatus(options, ctx) {
9444
9555
  }
9445
9556
  let manifest;
9446
9557
  try {
9447
- manifest = await readManifest10(paths);
9558
+ manifest = await readManifest11(paths);
9448
9559
  } catch (error) {
9449
9560
  if (findErrorCode14(error, "ENOENT")) {
9450
9561
  throw new Error("Workspace not initialized. Run 'basou init' first.");
@@ -9510,7 +9621,7 @@ import {
9510
9621
  loadSessionEntries as loadSessionEntries3,
9511
9622
  loadTaskEntries,
9512
9623
  prefixedUlid as prefixedUlid5,
9513
- readManifest as readManifest11,
9624
+ readManifest as readManifest12,
9514
9625
  readTaskFile,
9515
9626
  readTaskFileWithArchiveFallback,
9516
9627
  reconcileAllTasks,
@@ -9637,7 +9748,7 @@ async function doRunTaskNew(options, ctx) {
9637
9748
  });
9638
9749
  return;
9639
9750
  }
9640
- const manifest = await readManifest11(paths);
9751
+ const manifest = await readManifest12(paths);
9641
9752
  const result = await createTaskWithEvent({
9642
9753
  mode: "ad-hoc",
9643
9754
  paths,
@@ -9985,7 +10096,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
9985
10096
  });
9986
10097
  return;
9987
10098
  }
9988
- const manifest = await readManifest11(paths);
10099
+ const manifest = await readManifest12(paths);
9989
10100
  const result = await updateTaskStatusWithEvent({
9990
10101
  mode: "ad-hoc",
9991
10102
  paths,
@@ -10038,7 +10149,7 @@ async function doRunTaskReconcile(options, ctx) {
10038
10149
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
10039
10150
  const paths = basouPaths19(repositoryRoot);
10040
10151
  await assertWorkspaceInitialized13(paths.root);
10041
- const manifest = await readManifest11(paths);
10152
+ const manifest = await readManifest12(paths);
10042
10153
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
10043
10154
  const write = options.write === true;
10044
10155
  const verbose = isVerbose(options);
@@ -10218,7 +10329,7 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
10218
10329
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
10219
10330
  const paths = basouPaths19(repositoryRoot);
10220
10331
  await assertWorkspaceInitialized13(paths.root);
10221
- const manifest = await readManifest11(paths);
10332
+ const manifest = await readManifest12(paths);
10222
10333
  const taskId = await resolveTaskId2(paths, taskIdInput);
10223
10334
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
10224
10335
  const write = options.write === true;
@@ -10298,7 +10409,7 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
10298
10409
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
10299
10410
  const paths = basouPaths19(repositoryRoot);
10300
10411
  await assertWorkspaceInitialized13(paths.root);
10301
- const manifest = await readManifest11(paths);
10412
+ const manifest = await readManifest12(paths);
10302
10413
  const taskId = await resolveTaskId2(paths, taskIdInput);
10303
10414
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
10304
10415
  const occurredAt = now.toISOString();
@@ -10354,7 +10465,7 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
10354
10465
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
10355
10466
  const paths = basouPaths19(repositoryRoot);
10356
10467
  await assertWorkspaceInitialized13(paths.root);
10357
- const manifest = await readManifest11(paths);
10468
+ const manifest = await readManifest12(paths);
10358
10469
  const taskId = await resolveTaskId2(paths, taskIdInput);
10359
10470
  if (options.yes !== true) {
10360
10471
  await confirmDestructiveAction("delete", taskId);
@@ -10399,7 +10510,7 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
10399
10510
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
10400
10511
  const paths = basouPaths19(repositoryRoot);
10401
10512
  await assertWorkspaceInitialized13(paths.root);
10402
- const manifest = await readManifest11(paths);
10513
+ const manifest = await readManifest12(paths);
10403
10514
  const taskId = await resolveTaskId2(paths, taskIdInput);
10404
10515
  if (options.yes !== true) {
10405
10516
  await confirmDestructiveAction("archive", taskId);
@@ -10441,8 +10552,8 @@ async function confirmDestructiveAction(action, taskId) {
10441
10552
  }
10442
10553
  }
10443
10554
  async function readSingleLineFromStdin() {
10444
- const { createInterface: createInterface2 } = await import("readline/promises");
10445
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
10555
+ const { createInterface: createInterface3 } = await import("readline/promises");
10556
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
10446
10557
  try {
10447
10558
  const line = await rl.question("");
10448
10559
  return line;
@@ -10716,22 +10827,334 @@ async function assertWorkspaceInitialized14(basouRoot) {
10716
10827
  // src/commands/view.ts
10717
10828
  import { spawn } from "child_process";
10718
10829
  import { createHash } from "crypto";
10719
- import { basename as basename8, resolve as resolve13 } from "path";
10830
+ import { basename as basename9, resolve as resolve13 } from "path";
10720
10831
  import {
10721
10832
  assertBasouRootSafe as assertBasouRootSafe18,
10722
- basouPaths as basouPaths21,
10833
+ basouPaths as basouPaths22,
10723
10834
  findErrorCode as findErrorCode18,
10724
- readManifest as readManifest14,
10725
- resolveRepositoryRoot as resolveRepositoryRoot14
10835
+ readManifest as readManifest16,
10836
+ resolveRepositoryRoot as resolveRepositoryRoot15
10726
10837
  } from "@basou/core";
10727
10838
  import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
10728
10839
 
10840
+ // src/lib/portfolio-coverage.ts
10841
+ import { createReadStream as createReadStream2 } from "fs";
10842
+ import { readdir as readdir3, stat as stat6 } from "fs/promises";
10843
+ import { homedir as homedir12 } from "os";
10844
+ import { basename as basename7, dirname as dirname4, join as join17 } from "path";
10845
+ import { createInterface as createInterface2 } from "readline";
10846
+ import { basouPaths as basouPaths21, readManifest as readManifest13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
10847
+ function uncapturedTotal(result) {
10848
+ return result.groups.reduce((sum, g) => sum + g.logs, 0) + result.unplaceable;
10849
+ }
10850
+ async function checkPortfolioCoverage(workspaces, ctx = {}) {
10851
+ const { roots, inertWorkspaces } = await collectDeclaredRoots(workspaces);
10852
+ const listedDirs = new Set([...roots].map((root) => encodeProjectDir(root)));
10853
+ const claudeProjectsDir = ctx.claudeProjectsDir ?? join17(homedir12(), ".claude", "projects");
10854
+ const codexSessionsDir = ctx.codexSessionsDir ?? join17(homedir12(), ".codex", "sessions");
10855
+ const tally = new Tally(roots);
10856
+ const absentTrees = [];
10857
+ const claudeFiles = await listClaudeTranscripts(claudeProjectsDir);
10858
+ if (claudeFiles === void 0) absentTrees.push(claudeProjectsDir);
10859
+ else {
10860
+ for (const file of claudeFiles) {
10861
+ await tally.add(
10862
+ file,
10863
+ "claude-code",
10864
+ claudeTranscriptCwd,
10865
+ listedDirs.has(basename7(dirname4(file)))
10866
+ );
10867
+ }
10868
+ }
10869
+ const codexFiles = await listCodexRollouts(codexSessionsDir);
10870
+ if (codexFiles === void 0) absentTrees.push(codexSessionsDir);
10871
+ else {
10872
+ for (const file of codexFiles) await tally.add(file, "codex", codexRolloutCwd, true);
10873
+ }
10874
+ return tally.finish(absentTrees, inertWorkspaces);
10875
+ }
10876
+ async function collectDeclaredRoots(workspaces) {
10877
+ const roots = /* @__PURE__ */ new Set();
10878
+ const inertWorkspaces = [];
10879
+ for (const ws of workspaces) {
10880
+ let importRoot;
10881
+ try {
10882
+ importRoot = await resolveRepositoryRoot14(ws.repoRoot);
10883
+ } catch {
10884
+ inertWorkspaces.push({ path: ws.repoRoot, reason: "not_a_git_repo" });
10885
+ continue;
10886
+ }
10887
+ let resolved;
10888
+ try {
10889
+ const manifest = await readManifest13(basouPaths21(importRoot));
10890
+ resolved = resolveSourceRoots({
10891
+ projectFlags: [],
10892
+ manifest,
10893
+ repoRoot: importRoot,
10894
+ cwd: importRoot
10895
+ });
10896
+ } catch (error) {
10897
+ const absent = error instanceof Error && error.message === "YAML file not found";
10898
+ inertWorkspaces.push({
10899
+ path: ws.repoRoot,
10900
+ reason: absent ? "no_store" : "unreadable_store"
10901
+ });
10902
+ continue;
10903
+ }
10904
+ for (const root of resolved) roots.add(root);
10905
+ }
10906
+ return { roots, inertWorkspaces };
10907
+ }
10908
+ var Tally = class {
10909
+ constructor(declaredRoots) {
10910
+ this.declaredRoots = declaredRoots;
10911
+ }
10912
+ declaredRoots;
10913
+ groups = /* @__PURE__ */ new Map();
10914
+ kinds = /* @__PURE__ */ new Map();
10915
+ attributed = 0;
10916
+ scanned = 0;
10917
+ unplaceable = 0;
10918
+ unreadable = 0;
10919
+ /**
10920
+ * Record one source log. `listedByImport` is whether the importer would even
10921
+ * read this file (the Claude directory guard); a cwd match on a file import
10922
+ * never lists is not capture.
10923
+ */
10924
+ async add(file, source, readCwd, listedByImport) {
10925
+ this.scanned++;
10926
+ const cwd = await readCwd(file);
10927
+ if (cwd === null) {
10928
+ this.unreadable++;
10929
+ return;
10930
+ }
10931
+ if (cwd === void 0) {
10932
+ this.unplaceable++;
10933
+ return;
10934
+ }
10935
+ const declared = this.declaredRoots.has(cwd);
10936
+ if (declared && listedByImport) {
10937
+ this.attributed++;
10938
+ return;
10939
+ }
10940
+ const kind = declared ? "dir_not_listed" : enclosingRoot(cwd, this.declaredRoots) !== void 0 ? "below_declared_root" : "no_declared_root";
10941
+ this.kinds.set(cwd, kind);
10942
+ const group = this.groups.get(cwd);
10943
+ if (group === void 0) this.groups.set(cwd, { logs: 1, sources: /* @__PURE__ */ new Set([source]) });
10944
+ else {
10945
+ group.logs++;
10946
+ group.sources.add(source);
10947
+ }
10948
+ }
10949
+ finish(absentTrees, inertWorkspaces) {
10950
+ const groups = [];
10951
+ for (const [cwd, { logs, sources }] of this.groups) {
10952
+ const kind = this.kinds.get(cwd) ?? "no_declared_root";
10953
+ const declaredRoot = kind === "below_declared_root" ? enclosingRoot(cwd, this.declaredRoots) : void 0;
10954
+ groups.push({
10955
+ cwd,
10956
+ logs,
10957
+ sources: [...sources].sort(),
10958
+ kind,
10959
+ ...declaredRoot !== void 0 ? { declaredRoot } : {}
10960
+ });
10961
+ }
10962
+ const rank = (k) => k === "dir_not_listed" ? 0 : k === "below_declared_root" ? 1 : 2;
10963
+ groups.sort((a, b) => {
10964
+ if (a.kind !== b.kind) return rank(a.kind) - rank(b.kind);
10965
+ if (a.logs !== b.logs) return b.logs - a.logs;
10966
+ return a.cwd < b.cwd ? -1 : a.cwd > b.cwd ? 1 : 0;
10967
+ });
10968
+ return {
10969
+ logsScanned: this.scanned,
10970
+ attributed: this.attributed,
10971
+ groups,
10972
+ unplaceable: this.unplaceable,
10973
+ unreadable: this.unreadable,
10974
+ absentTrees,
10975
+ inertWorkspaces
10976
+ };
10977
+ }
10978
+ };
10979
+ function enclosingRoot(cwd, roots) {
10980
+ for (const root of roots) {
10981
+ if (cwd.startsWith(root.endsWith("/") ? root : `${root}/`)) return root;
10982
+ }
10983
+ return void 0;
10984
+ }
10985
+ async function isDirEntry(parent, entry) {
10986
+ if (entry.isDirectory()) return true;
10987
+ if (!entry.isSymbolicLink()) return false;
10988
+ try {
10989
+ return (await stat6(join17(parent, entry.name))).isDirectory();
10990
+ } catch {
10991
+ return false;
10992
+ }
10993
+ }
10994
+ async function listClaudeTranscripts(projectsRoot) {
10995
+ let entries;
10996
+ try {
10997
+ entries = await readdir3(projectsRoot, { withFileTypes: true });
10998
+ } catch {
10999
+ return void 0;
11000
+ }
11001
+ const files = [];
11002
+ for (const entry of entries) {
11003
+ if (!await isDirEntry(projectsRoot, entry)) continue;
11004
+ const full = join17(projectsRoot, entry.name);
11005
+ let names;
11006
+ try {
11007
+ names = await readdir3(full);
11008
+ } catch {
11009
+ continue;
11010
+ }
11011
+ for (const name of names) {
11012
+ if (name.endsWith(".jsonl")) files.push(join17(full, name));
11013
+ }
11014
+ }
11015
+ return files.sort();
11016
+ }
11017
+ async function listCodexRollouts(sessionsRoot) {
11018
+ const found = [];
11019
+ const walk = async (dir) => {
11020
+ let entries;
11021
+ try {
11022
+ entries = await readdir3(dir, { withFileTypes: true });
11023
+ } catch {
11024
+ return;
11025
+ }
11026
+ for (const entry of entries) {
11027
+ const full = join17(dir, entry.name);
11028
+ if (entry.isDirectory()) await walk(full);
11029
+ else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
11030
+ found.push(full);
11031
+ }
11032
+ }
11033
+ };
11034
+ try {
11035
+ await readdir3(sessionsRoot);
11036
+ } catch {
11037
+ return void 0;
11038
+ }
11039
+ await walk(sessionsRoot);
11040
+ return found.sort();
11041
+ }
11042
+ async function claudeTranscriptCwd(file) {
11043
+ const stream = createReadStream2(file, { encoding: "utf8" });
11044
+ const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
11045
+ try {
11046
+ for await (const line of lines) {
11047
+ if (line.length === 0) continue;
11048
+ let record;
11049
+ try {
11050
+ record = JSON.parse(line);
11051
+ } catch {
11052
+ continue;
11053
+ }
11054
+ if (typeof record !== "object" || record === null || Array.isArray(record)) continue;
11055
+ const cwd = record.cwd;
11056
+ if (typeof cwd === "string" && cwd.length > 0) return cwd;
11057
+ }
11058
+ return void 0;
11059
+ } catch {
11060
+ return null;
11061
+ } finally {
11062
+ lines.close();
11063
+ stream.destroy();
11064
+ }
11065
+ }
11066
+ async function codexRolloutCwd(file) {
11067
+ const meta = await readRolloutMeta(file);
11068
+ return meta === void 0 ? void 0 : meta.cwd;
11069
+ }
11070
+ var GROUP_LIST_CAP = 10;
11071
+ function groupNote(group) {
11072
+ if (group.kind === "below_declared_root") {
11073
+ return ` \u2014 inside declared root ${group.declaredRoot}, and the recorded cwd must EQUAL a source root`;
11074
+ }
11075
+ if (group.kind === "dir_not_listed") {
11076
+ return " \u2014 this cwd IS a declared root, but the transcript's per-project directory is not one import lists";
11077
+ }
11078
+ return "";
11079
+ }
11080
+ function formatCoverageReport(result) {
11081
+ const lines = [];
11082
+ if (result.logsScanned === 0) {
11083
+ const where = result.absentTrees.length > 0 ? ` (no source logs found: ${result.absentTrees.join(", ")})` : "";
11084
+ lines.push(
11085
+ `Capture coverage: nothing to check \u2014 no native session logs on this machine${where}.`
11086
+ );
11087
+ return [...lines, ...inertLines(result)];
11088
+ }
11089
+ const total = uncapturedTotal(result);
11090
+ const caveats = [];
11091
+ if (result.unreadable > 0) caveats.push(`${result.unreadable} unreadable, verdict unknown`);
11092
+ if (result.absentTrees.length > 0) caveats.push(`not scanned: ${result.absentTrees.join(", ")}`);
11093
+ const caveat = caveats.length > 0 ? ` (${caveats.join("; ")})` : "";
11094
+ if (total === 0 && result.unreadable === 0) {
11095
+ lines.push(
11096
+ `Capture coverage: OK. ${result.logsScanned} source log(s) scanned, all imported by a registered workspace${caveat}.`
11097
+ );
11098
+ return [...lines, ...inertLines(result)];
11099
+ }
11100
+ const pct = Math.round(total / result.logsScanned * 100);
11101
+ lines.push(
11102
+ `Capture coverage: ${total} of ${result.logsScanned} source log(s) (${pct}%) are imported by no registered workspace${caveat}:`
11103
+ );
11104
+ for (const g of result.groups.slice(0, GROUP_LIST_CAP)) {
11105
+ const via = g.sources.join("+");
11106
+ lines.push(` ${String(g.logs).padStart(4)} ${g.cwd} (${via})${groupNote(g)}`);
11107
+ }
11108
+ const rest = result.groups.length - GROUP_LIST_CAP;
11109
+ if (rest > 0) {
11110
+ const restLogs = result.groups.slice(GROUP_LIST_CAP).reduce((sum, g) => sum + g.logs, 0);
11111
+ lines.push(
11112
+ ` \u2026 +${rest} more working director${rest === 1 ? "y" : "ies"} (${restLogs} log(s))`
11113
+ );
11114
+ }
11115
+ if (result.unplaceable > 0) {
11116
+ lines.push(
11117
+ ` ${String(result.unplaceable).padStart(4)} (no directory to name: the log records no cwd import can use, so import drops it)`
11118
+ );
11119
+ }
11120
+ if (result.groups.some((g) => g.kind === "below_declared_root")) {
11121
+ lines.push(
11122
+ "A cwd inside a declared root is dropped by the exact-match rule: the enclosing declaration does not cover it. Declaring that subdirectory itself as a further import.source_roots entry captures it."
11123
+ );
11124
+ }
11125
+ if (result.groups.some((g) => g.kind === "dir_not_listed")) {
11126
+ lines.push(
11127
+ "A transcript whose cwd IS declared but whose per-project directory no declared root encodes to is never listed by the importer. Check that the workspace is registered by the same path spelling the sessions ran in."
11128
+ );
11129
+ }
11130
+ lines.push(
11131
+ "Register the project in a workspace's import.source_roots to start capturing it (registering a path in ~/.basou/portfolio.yaml alone imports nothing \u2014 it only adds the workspace to this view). A scratch directory, a temp path, or a GUI tool's own working directory has no repo to declare and is expected to stay here."
11132
+ );
11133
+ return [...lines, ...inertLines(result)];
11134
+ }
11135
+ function inertLines(result) {
11136
+ if (result.inertWorkspaces.length === 0) return [];
11137
+ const detail = {
11138
+ not_a_git_repo: "not a git repository",
11139
+ no_store: "no .basou store (never initialized)",
11140
+ unreadable_store: "the .basou manifest is unreadable"
11141
+ };
11142
+ const n = result.inertWorkspaces.length;
11143
+ const lines = [
11144
+ `Capture coverage: ${n} registered entr${n === 1 ? "y" : "ies"} import cannot run in, so ${n === 1 ? "it declares" : "they declare"} no source roots:`
11145
+ ];
11146
+ for (const ws of result.inertWorkspaces) {
11147
+ lines.push(` ${ws.path} \u2014 ${detail[ws.reason]}`);
11148
+ }
11149
+ return lines;
11150
+ }
11151
+
10729
11152
  // src/lib/portfolio-safety.ts
10730
11153
  import { execFile } from "child_process";
10731
11154
  import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
10732
- import { isAbsolute as isAbsolute7, join as join17, relative as relative4, resolve as resolve11 } from "path";
11155
+ import { isAbsolute as isAbsolute7, join as join18, relative as relative4, resolve as resolve11 } from "path";
10733
11156
  import { promisify } from "util";
10734
- import { readManifest as readManifest12 } from "@basou/core";
11157
+ import { readManifest as readManifest14 } from "@basou/core";
10735
11158
  var execFileAsync = promisify(execFile);
10736
11159
  function errorCode(error) {
10737
11160
  return error instanceof Error ? error.code : void 0;
@@ -10753,7 +11176,7 @@ function isBasouPath(p) {
10753
11176
  async function inspectRepo(repoPath) {
10754
11177
  let hasEntry = false;
10755
11178
  try {
10756
- await lstat2(join17(repoPath, ".basou"));
11179
+ await lstat2(join18(repoPath, ".basou"));
10757
11180
  hasEntry = true;
10758
11181
  } catch (error) {
10759
11182
  if (errorCode(error) !== "ENOENT") {
@@ -10787,7 +11210,7 @@ async function checkPortfolioSafety(workspaces) {
10787
11210
  let viewPath;
10788
11211
  let isMaster = false;
10789
11212
  try {
10790
- const manifest = await readManifest12(ws.paths);
11213
+ const manifest = await readManifest14(ws.paths);
10791
11214
  sourceRoots = manifest.import?.source_roots ?? [];
10792
11215
  viewPath = manifest.workspace.view;
10793
11216
  isMaster = true;
@@ -10907,7 +11330,7 @@ function formatSafetyReport(result) {
10907
11330
 
10908
11331
  // src/lib/view-server.ts
10909
11332
  import { createServer } from "http";
10910
- import { basename as basename7, join as join18, resolve as resolve12 } from "path";
11333
+ import { basename as basename8, join as join19, resolve as resolve12 } from "path";
10911
11334
  import {
10912
11335
  computeWorkStats as computeWorkStats2,
10913
11336
  enumerateApprovals as enumerateApprovals2,
@@ -10917,7 +11340,7 @@ import {
10917
11340
  loadSessionEntries as loadSessionEntries4,
10918
11341
  loadTaskEntries as loadTaskEntries2,
10919
11342
  readAllEvents as readAllEvents2,
10920
- readManifest as readManifest13,
11343
+ readManifest as readManifest15,
10921
11344
  readMarkdownFile as readMarkdownFile7,
10922
11345
  readSessionYaml as readSessionYaml3,
10923
11346
  readTaskFile as readTaskFile2,
@@ -11873,7 +12296,7 @@ async function captureStaleness(ws, nowIso) {
11873
12296
  async function overview(ws, nowProvider, resolveRemoteUrl) {
11874
12297
  let manifest;
11875
12298
  try {
11876
- manifest = await readManifest13(ws.paths);
12299
+ manifest = await readManifest15(ws.paths);
11877
12300
  } catch (error) {
11878
12301
  if (findErrorCode17(error, "ENOENT")) {
11879
12302
  return { initialized: false, repoRoot: ws.repoRoot };
@@ -11913,7 +12336,7 @@ async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
11913
12336
  const remote = await resolveRemoteUrl(abs);
11914
12337
  const url = remote !== void 0 ? toBrowserUrl(remote) : null;
11915
12338
  return {
11916
- name: basename7(abs),
12339
+ name: basename8(abs),
11917
12340
  path: repo.path,
11918
12341
  ...url !== null ? { url } : {},
11919
12342
  ...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
@@ -11948,7 +12371,7 @@ async function sessionDetail(ws, sessionId) {
11948
12371
  throw error;
11949
12372
  }
11950
12373
  try {
11951
- const events = await readAllEvents2(join18(ws.paths.sessions, sessionId));
12374
+ const events = await readAllEvents2(join19(ws.paths.sessions, sessionId));
11952
12375
  return { session, events };
11953
12376
  } catch {
11954
12377
  return { session, events: [], degraded: true };
@@ -12103,7 +12526,10 @@ function registerViewCommand(program) {
12103
12526
  "--workspace <path>",
12104
12527
  "Workspace repo path to include (repeatable; implies portfolio mode; resolved against the cwd)",
12105
12528
  collectPath3
12106
- ).option("--check", "Run the portfolio safety preflight and exit (no server)").option("--skip-safety-check", "Skip the portfolio safety preflight on start (not recommended)").option("-v, --verbose", "Show error causes").action(async (options) => {
12529
+ ).option(
12530
+ "--check",
12531
+ "Run the read-only preflight and exit (no server): the portfolio safety check, plus \u2014 in portfolio mode \u2014 a capture-coverage report of session logs no registered workspace imports"
12532
+ ).option("--skip-safety-check", "Skip the portfolio safety preflight on start (not recommended)").option("-v, --verbose", "Show error causes").action(async (options) => {
12107
12533
  await runView(options);
12108
12534
  });
12109
12535
  }
@@ -12119,10 +12545,19 @@ async function doRunView(options, ctx) {
12119
12545
  const cwd = ctx.cwd ?? process.cwd();
12120
12546
  const workspaceFlags = options.workspace ?? [];
12121
12547
  const isPortfolio = workspaceFlags.length > 0 || options.portfolio === true;
12548
+ const isWholeRegistry = options.portfolio === true && workspaceFlags.length === 0;
12122
12549
  const deps = isPortfolio ? await buildPortfolioDeps(workspaceFlags, ctx, cwd) : await buildSingleDeps(ctx, cwd);
12123
12550
  if (options.check === true) {
12124
12551
  const result = await checkPortfolioSafety(deps.workspaces);
12125
12552
  for (const line of formatSafetyReport(result)) console.log(line);
12553
+ if (isWholeRegistry) {
12554
+ const coverage = await checkPortfolioCoverage(deps.workspaces, {
12555
+ ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
12556
+ ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
12557
+ });
12558
+ console.log("");
12559
+ for (const line of formatCoverageReport(coverage)) console.log(line);
12560
+ }
12126
12561
  if (result.findings.length > 0) process.exitCode = 1;
12127
12562
  return;
12128
12563
  }
@@ -12162,7 +12597,7 @@ async function doRunView(options, ctx) {
12162
12597
  }
12163
12598
  async function buildSingleDeps(ctx, cwd) {
12164
12599
  const repositoryRoot = await resolveRepositoryRootForView(cwd);
12165
- const paths = basouPaths21(repositoryRoot);
12600
+ const paths = basouPaths22(repositoryRoot);
12166
12601
  await assertWorkspaceInitialized15(paths.root);
12167
12602
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
12168
12603
  return {
@@ -12196,14 +12631,14 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
12196
12631
  };
12197
12632
  }
12198
12633
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
12199
- const paths = basouPaths21(repoRoot);
12634
+ const paths = basouPaths22(repoRoot);
12200
12635
  const importCtx = {
12201
12636
  cwd: repoRoot,
12202
12637
  ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
12203
12638
  ...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
12204
12639
  };
12205
12640
  try {
12206
- const manifest = await readManifest14(paths);
12641
+ const manifest = await readManifest16(paths);
12207
12642
  return {
12208
12643
  key: manifest.workspace.id,
12209
12644
  label: labelOverride ?? manifest.workspace.name,
@@ -12216,7 +12651,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
12216
12651
  const notFound = error instanceof Error && error.message === "YAML file not found";
12217
12652
  return {
12218
12653
  key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
12219
- label: labelOverride ?? basename8(repoRoot),
12654
+ label: labelOverride ?? basename9(repoRoot),
12220
12655
  paths,
12221
12656
  repoRoot,
12222
12657
  importCtx,
@@ -12283,7 +12718,7 @@ function waitForShutdown(signal) {
12283
12718
  }
12284
12719
  async function resolveRepositoryRootForView(cwd) {
12285
12720
  try {
12286
- return await resolveRepositoryRoot14(cwd);
12721
+ return await resolveRepositoryRoot15(cwd);
12287
12722
  } catch (error) {
12288
12723
  if (error instanceof Error && error.message === "Not a git repository") {
12289
12724
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
@@ -12334,6 +12769,7 @@ function buildProgram() {
12334
12769
  registerReviewGapsCommand(program);
12335
12770
  registerProjectCommand(program);
12336
12771
  registerProtocolCommand(program);
12772
+ registerChannelCommand(program);
12337
12773
  registerHookCommand(program);
12338
12774
  return program;
12339
12775
  }