@basou/cli 0.45.0 → 0.46.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
@@ -2443,8 +2443,8 @@ async function assertWorkspaceInitialized4(basouRoot) {
2443
2443
  // src/commands/hook.ts
2444
2444
  import { execFile } from "child_process";
2445
2445
  import { open as open2, readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
2446
- import { homedir as homedir7 } from "os";
2447
- import { join as join9 } from "path";
2446
+ import { homedir as homedir8 } from "os";
2447
+ import { join as join10 } from "path";
2448
2448
  import { fileURLToPath } from "url";
2449
2449
  import { promisify } from "util";
2450
2450
  import {
@@ -2454,12 +2454,20 @@ import {
2454
2454
  evaluateStopHook,
2455
2455
  findBasouSessionStartHook,
2456
2456
  findBasouStopHookCommand,
2457
+ isProtocolUpdateDue,
2457
2458
  ORIENTATION_END as ORIENTATION_END2,
2458
2459
  ORIENTATION_START as ORIENTATION_START2,
2460
+ PROTOCOL_END,
2461
+ PROTOCOL_START,
2459
2462
  parseMarkers as parseMarkers2,
2463
+ parseProtocolStamp,
2464
+ protocolSectionsFrom,
2465
+ protocolUpdateToken,
2460
2466
  readMarkdownFile as readMarkdownFile6,
2461
2467
  removeSessionStartHook,
2462
2468
  removeStopHook,
2469
+ renderProtocolUpdate,
2470
+ transcriptStartedAt,
2463
2471
  upsertSessionStartHook,
2464
2472
  upsertStopHook
2465
2473
  } from "@basou/core";
@@ -2714,6 +2722,83 @@ async function warnIfPositionNamesOtherWorkspaces(args) {
2714
2722
  }
2715
2723
  }
2716
2724
 
2725
+ // src/lib/protocols-config.ts
2726
+ import { homedir as homedir5 } from "os";
2727
+ import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
2728
+ import { readYamlFile as readYamlFile4 } from "@basou/core";
2729
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join7(homedir5(), ".basou", "protocols.yaml");
2730
+ var DEFAULT_TARGET_PATH = join7(homedir5(), ".claude", "CLAUDE.md");
2731
+ var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
2732
+ var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
2733
+ function expandTilde2(p) {
2734
+ if (p === "~") return homedir5();
2735
+ if (p.startsWith("~/")) return join7(homedir5(), p.slice(2));
2736
+ return p;
2737
+ }
2738
+ function isRecord2(value) {
2739
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2740
+ }
2741
+ async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
2742
+ let raw;
2743
+ try {
2744
+ raw = await readYamlFile4(configPath);
2745
+ } catch (error) {
2746
+ if (error instanceof Error && error.message === "YAML file not found") {
2747
+ throw new Error(
2748
+ "No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
2749
+ );
2750
+ }
2751
+ if (error instanceof Error && error.message === "Failed to parse YAML content") {
2752
+ throw new Error("~/.basou/protocols.yaml is not valid YAML.");
2753
+ }
2754
+ throw error;
2755
+ }
2756
+ if (!isRecord2(raw) || !Array.isArray(raw.protocols)) {
2757
+ throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
2758
+ }
2759
+ for (const key of Object.keys(raw)) {
2760
+ if (!ALLOWED_TOP_KEYS.has(key)) {
2761
+ throw new Error(
2762
+ `~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
2763
+ );
2764
+ }
2765
+ }
2766
+ const seen = /* @__PURE__ */ new Set();
2767
+ const result = [];
2768
+ for (const entry of raw.protocols) {
2769
+ if (!isRecord2(entry)) {
2770
+ throw new Error("Each protocol entry must be a mapping with a 'source' key.");
2771
+ }
2772
+ for (const key of Object.keys(entry)) {
2773
+ if (!ALLOWED_ENTRY_KEYS.has(key)) {
2774
+ throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
2775
+ }
2776
+ }
2777
+ if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
2778
+ throw new Error("Each protocol entry needs a non-empty string 'source'.");
2779
+ }
2780
+ if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
2781
+ throw new Error("A protocol entry 'title' must be a non-empty string when present.");
2782
+ }
2783
+ const expanded = expandTilde2(entry.source.trim());
2784
+ if (!isAbsolute2(expanded)) {
2785
+ throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
2786
+ }
2787
+ const abs = resolve4(expanded);
2788
+ if (seen.has(abs)) {
2789
+ throw new Error("Duplicate protocol source (each source path may appear only once).");
2790
+ }
2791
+ seen.add(abs);
2792
+ result.push(
2793
+ entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
2794
+ );
2795
+ }
2796
+ if (result.length === 0) {
2797
+ throw new Error("~/.basou/protocols.yaml has no protocols.");
2798
+ }
2799
+ return result;
2800
+ }
2801
+
2717
2802
  // src/commands/orient.ts
2718
2803
  import {
2719
2804
  assertBasouRootSafe as assertBasouRootSafe7,
@@ -2724,22 +2809,22 @@ import {
2724
2809
  } from "@basou/core";
2725
2810
 
2726
2811
  // src/lib/hosts-config.ts
2727
- import { homedir as homedir5 } from "os";
2728
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
2729
- import { readYamlFile as readYamlFile4 } from "@basou/core";
2730
- var DEFAULT_HOSTS_CONFIG_PATH = join7(homedir5(), ".basou", "hosts.yaml");
2731
- function expandTilde2(p) {
2732
- if (p === "~") return homedir5();
2733
- if (p.startsWith("~/")) return join7(homedir5(), p.slice(2));
2812
+ import { homedir as homedir6 } from "os";
2813
+ import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "path";
2814
+ import { readYamlFile as readYamlFile5 } from "@basou/core";
2815
+ var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
2816
+ function expandTilde3(p) {
2817
+ if (p === "~") return homedir6();
2818
+ if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
2734
2819
  return p;
2735
2820
  }
2736
- function isRecord2(value) {
2821
+ function isRecord3(value) {
2737
2822
  return typeof value === "object" && value !== null && !Array.isArray(value);
2738
2823
  }
2739
2824
  async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
2740
2825
  let raw;
2741
2826
  try {
2742
- raw = await readYamlFile4(configPath);
2827
+ raw = await readYamlFile5(configPath);
2743
2828
  } catch (error) {
2744
2829
  if (error instanceof Error && error.message === "YAML file not found") {
2745
2830
  return null;
@@ -2749,25 +2834,25 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
2749
2834
  }
2750
2835
  throw error;
2751
2836
  }
2752
- if (!isRecord2(raw) || !Array.isArray(raw.hosts)) {
2837
+ if (!isRecord3(raw) || !Array.isArray(raw.hosts)) {
2753
2838
  throw new Error("~/.basou/hosts.yaml must contain a 'hosts:' list.");
2754
2839
  }
2755
2840
  const seenPaths = /* @__PURE__ */ new Set();
2756
2841
  const seenLabels = /* @__PURE__ */ new Set();
2757
2842
  const result = [];
2758
2843
  for (const entry of raw.hosts) {
2759
- if (!isRecord2(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
2844
+ if (!isRecord3(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
2760
2845
  throw new Error("Each host needs a non-empty string 'label'.");
2761
2846
  }
2762
2847
  const label = entry.label.trim();
2763
2848
  if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
2764
2849
  throw new Error("Each host needs a non-empty string 'path'.");
2765
2850
  }
2766
- const expanded = expandTilde2(entry.path.trim());
2767
- if (!isAbsolute2(expanded)) {
2851
+ const expanded = expandTilde3(entry.path.trim());
2852
+ if (!isAbsolute3(expanded)) {
2768
2853
  throw new Error("Host paths must be absolute (or start with '~').");
2769
2854
  }
2770
- const abs = resolve4(expanded);
2855
+ const abs = resolve5(expanded);
2771
2856
  if (seenPaths.has(abs)) continue;
2772
2857
  if (seenLabels.has(label)) {
2773
2858
  throw new Error(`Duplicate host label '${label}'; each host needs a distinct label.`);
@@ -2792,8 +2877,8 @@ import {
2792
2877
  // src/commands/import.ts
2793
2878
  import { createReadStream } from "fs";
2794
2879
  import { readdir, readFile as readFile2, rm, stat as stat3 } from "fs/promises";
2795
- import { homedir as homedir6 } from "os";
2796
- import { basename as basename4, dirname as dirname3, join as join8, resolve as resolve5 } from "path";
2880
+ import { homedir as homedir7 } from "os";
2881
+ import { basename as basename4, dirname as dirname3, join as join9, resolve as resolve6 } from "path";
2797
2882
  import { createInterface } from "readline";
2798
2883
  import {
2799
2884
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
@@ -2864,10 +2949,10 @@ function resolveSourceRoots(args) {
2864
2949
  const { projectFlags, manifest, repoRoot, cwd } = args;
2865
2950
  let resolved;
2866
2951
  if (projectFlags.length > 0) {
2867
- resolved = projectFlags.map((p) => resolve5(cwd, p));
2952
+ resolved = projectFlags.map((p) => resolve6(cwd, p));
2868
2953
  } else {
2869
2954
  const roots = manifest.import?.source_roots;
2870
- resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve5(repoRoot, r)) : [repoRoot];
2955
+ resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve6(repoRoot, r)) : [repoRoot];
2871
2956
  }
2872
2957
  return [...new Set(resolved)];
2873
2958
  }
@@ -2880,7 +2965,7 @@ async function doRunImportClaudeCode(options, ctx) {
2880
2965
  repoRoot: repositoryRoot,
2881
2966
  cwd: ctx.cwd ?? process.cwd()
2882
2967
  });
2883
- const projectsRoot = ctx.claudeProjectsDir ?? join8(homedir6(), ".claude", "projects");
2968
+ const projectsRoot = ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects");
2884
2969
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
2885
2970
  const projectSet = new Set(projectPaths);
2886
2971
  const candidates = files.map((file) => {
@@ -2919,7 +3004,7 @@ async function doRunImportCodex(options, ctx) {
2919
3004
  repoRoot: repositoryRoot,
2920
3005
  cwd: ctx.cwd ?? process.cwd()
2921
3006
  });
2922
- const sessionsRoot = ctx.codexSessionsDir ?? join8(homedir6(), ".codex", "sessions");
3007
+ const sessionsRoot = ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions");
2923
3008
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
2924
3009
  const candidates = rollouts.map(({ file, externalId }) => ({
2925
3010
  externalId,
@@ -3050,7 +3135,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
3050
3135
  if (priors.length > 0 && options.force === true) {
3051
3136
  if (options.dryRun !== true) {
3052
3137
  for (const { sessionId } of priors) {
3053
- await rm(join8(paths.sessions, sessionId), { recursive: true, force: true });
3138
+ await rm(join9(paths.sessions, sessionId), { recursive: true, force: true });
3054
3139
  }
3055
3140
  }
3056
3141
  counts.replaced++;
@@ -3161,7 +3246,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3161
3246
  if (options.session !== void 0) {
3162
3247
  const matches = [];
3163
3248
  for (const projectPath of projectPaths) {
3164
- const file = join8(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
3249
+ const file = join9(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
3165
3250
  if (await pathExists(file)) matches.push(file);
3166
3251
  }
3167
3252
  if (matches.length === 0) {
@@ -3172,7 +3257,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3172
3257
  const files = [];
3173
3258
  let anyDirFound = false;
3174
3259
  for (const projectPath of projectPaths) {
3175
- const transcriptDir = join8(projectsRoot, encodeProjectDir(projectPath));
3260
+ const transcriptDir = join9(projectsRoot, encodeProjectDir(projectPath));
3176
3261
  let entries;
3177
3262
  try {
3178
3263
  entries = await readdir(transcriptDir);
@@ -3182,7 +3267,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3182
3267
  }
3183
3268
  anyDirFound = true;
3184
3269
  for (const name of entries) {
3185
- if (name.endsWith(".jsonl")) files.push(join8(transcriptDir, name));
3270
+ if (name.endsWith(".jsonl")) files.push(join9(transcriptDir, name));
3186
3271
  }
3187
3272
  }
3188
3273
  if (!anyDirFound) {
@@ -3239,7 +3324,7 @@ async function findRolloutFiles(sessionsRoot) {
3239
3324
  throw new Error("Failed to read Codex sessions directory", { cause: error });
3240
3325
  }
3241
3326
  for (const entry of entries) {
3242
- const full = join8(dir, entry.name);
3327
+ const full = join9(dir, entry.name);
3243
3328
  if (entry.isDirectory()) {
3244
3329
  await walk(full, false);
3245
3330
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -3696,6 +3781,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
3696
3781
 
3697
3782
  // src/commands/hook.ts
3698
3783
  var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
3784
+ var MAX_TRANSCRIPT_HEAD_BYTES = 256 * 1024;
3785
+ var TOKEN_SCAN_CHUNK_BYTES = 1024 * 1024;
3699
3786
  var execFileAsync = promisify(execFile);
3700
3787
  function registerHookCommand(program) {
3701
3788
  const hook = program.command("hook").description(
@@ -3707,13 +3794,13 @@ function registerHookCommand(program) {
3707
3794
  await runHookSessionStart();
3708
3795
  });
3709
3796
  hook.command("stop").description(
3710
- "Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
3797
+ "Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Also hands a running session the standing protocols when they changed after it started \u2014 the copy it read at start is stale, and only this hook reaches a session still running. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
3711
3798
  ).option(
3712
3799
  "--min-edits <n>",
3713
3800
  `Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
3714
3801
  ).option(
3715
3802
  "--block",
3716
- "Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking reminder"
3803
+ "Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking message"
3717
3804
  ).option(
3718
3805
  "--require-review",
3719
3806
  "Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
@@ -3826,12 +3913,25 @@ gh pr create|merge) without recording a review ('basou review record'). This
3826
3913
  gate is off by default; when on, its reminder is composed into the same
3827
3914
  envelope as the capture reminder.
3828
3915
 
3829
- By default the reminder is non-blocking: Claude sees it and may act on it or
3830
- stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
3831
- returns decision:block, holding the agent in-turn to act on the reminder; the
3832
- 'stop_hook_active' flag and Claude Code's own loop prevention bound it to a
3833
- single turn. Either way the hook fails open: a bad payload or unreadable
3834
- transcript exits cleanly with no output.
3916
+ It also hands a RUNNING session the standing protocols when they changed after
3917
+ that session started. The protocol block in ~/.claude/CLAUDE.md is read at
3918
+ session start, so an update made mid-session never reaches the session it was
3919
+ meant to correct; this is the only channel that does. The complete current set
3920
+ is delivered, not a diff, so what it supersedes -- including a protocol that is
3921
+ no longer there -- is unambiguous. It reads the rendered block and nothing else,
3922
+ so an edit not yet published by 'basou protocol sync' cannot reach a session,
3923
+ and it delivers once per block state: a second update in the same session still
3924
+ lands, the same one twice does not. This part is always on, needs no flag, and
3925
+ says nothing at all unless the block actually changed.
3926
+
3927
+ By default every message here is non-blocking: Claude sees it and may act on it
3928
+ or stop. With --block (opt-in enforcement, 'basou hook install --block') it
3929
+ instead returns decision:block, holding the agent in-turn; the 'stop_hook_active'
3930
+ flag and Claude Code's own loop prevention bound it to a single turn. Note that
3931
+ this covers the protocol delivery too, which carries no action to take -- with
3932
+ --block the turn is held so the new text lands before more work is done on the
3933
+ old. Either way the hook fails open: a bad payload or unreadable transcript
3934
+ exits cleanly with no output.
3835
3935
  `;
3836
3936
  async function runHookStop(options, ctx = {}) {
3837
3937
  try {
@@ -3869,7 +3969,12 @@ async function doRunHookStop(options, ctx) {
3869
3969
  stopHookActive: false,
3870
3970
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
3871
3971
  });
3972
+ const protocolUpdate = await evaluateProtocolUpdateGate({
3973
+ transcriptPath,
3974
+ target: ctx.protocolTargetPath ?? DEFAULT_TARGET_PATH
3975
+ });
3872
3976
  const parts = [];
3977
+ if (protocolUpdate !== null) parts.push(protocolUpdate);
3873
3978
  if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
3874
3979
  if (options.requireReview === true && evaluation.review.fires) {
3875
3980
  parts.push(evaluation.review.additionalContext);
@@ -3885,6 +3990,56 @@ async function doRunHookStop(options, ctx) {
3885
3990
  write(`${payloadJson}
3886
3991
  `);
3887
3992
  }
3993
+ async function evaluateProtocolUpdateGate(input) {
3994
+ try {
3995
+ const head = await readTranscriptHead(input.transcriptPath);
3996
+ const sessionStartedAt = transcriptStartedAt(parseTranscript(head));
3997
+ if (sessionStartedAt === void 0) return null;
3998
+ const touchedAt = await targetModifiedAt(input.target);
3999
+ if (touchedAt !== null && touchedAt <= Date.parse(sessionStartedAt)) return null;
4000
+ const existing = await readMarkdownFile6(input.target);
4001
+ if (existing === null) return null;
4002
+ const section = parseMarkers2(existing, { start: PROTOCOL_START, end: PROTOCOL_END });
4003
+ if (section.kind !== "ok") return null;
4004
+ const stamp = parseProtocolStamp(section.generated);
4005
+ if (stamp === null) return null;
4006
+ if (!isProtocolUpdateDue({ stamp, sessionStartedAt })) return null;
4007
+ const sections = protocolSectionsFrom(section.generated);
4008
+ if (sections === null || sections.trim().length === 0) return null;
4009
+ if (await transcriptCarries(input.transcriptPath, protocolUpdateToken(stamp.contentHash))) {
4010
+ return null;
4011
+ }
4012
+ return renderProtocolUpdate(sections, stamp);
4013
+ } catch {
4014
+ return null;
4015
+ }
4016
+ }
4017
+ async function targetModifiedAt(target) {
4018
+ try {
4019
+ return (await stat4(target)).mtimeMs;
4020
+ } catch {
4021
+ return null;
4022
+ }
4023
+ }
4024
+ async function transcriptCarries(path, token) {
4025
+ const handle = await open2(path, "r");
4026
+ try {
4027
+ const overlap = Math.max(token.length - 1, 0);
4028
+ const chunk = Buffer.alloc(TOKEN_SCAN_CHUNK_BYTES);
4029
+ let carry = "";
4030
+ let position = 0;
4031
+ for (; ; ) {
4032
+ const { bytesRead } = await handle.read(chunk, 0, TOKEN_SCAN_CHUNK_BYTES, position);
4033
+ if (bytesRead === 0) return false;
4034
+ position += bytesRead;
4035
+ const text = carry + chunk.subarray(0, bytesRead).toString("utf8");
4036
+ if (text.includes(token)) return true;
4037
+ carry = overlap > 0 ? text.slice(-overlap) : "";
4038
+ }
4039
+ } finally {
4040
+ await handle.close();
4041
+ }
4042
+ }
3888
4043
  async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
3889
4044
  const root = await resolveBasouRootForCommand(cwd, "hook session-start", {
3890
4045
  portfolioConfigPath
@@ -3984,11 +4139,25 @@ async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
3984
4139
  await handle.close();
3985
4140
  }
3986
4141
  }
4142
+ async function readTranscriptHead(path, maxBytes = MAX_TRANSCRIPT_HEAD_BYTES) {
4143
+ const { size } = await stat4(path);
4144
+ if (size <= maxBytes) return readFile3(path, "utf8");
4145
+ const handle = await open2(path, "r");
4146
+ try {
4147
+ const buffer = Buffer.alloc(maxBytes);
4148
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
4149
+ const text = buffer.subarray(0, bytesRead).toString("utf8");
4150
+ const lastNewline = text.lastIndexOf("\n");
4151
+ return lastNewline >= 0 ? text.slice(0, lastNewline + 1) : text;
4152
+ } finally {
4153
+ await handle.close();
4154
+ }
4155
+ }
3987
4156
  function parseMinEdits(raw) {
3988
4157
  if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
3989
4158
  return Number(raw);
3990
4159
  }
3991
- var DEFAULT_CLAUDE_SETTINGS_PATH = join9(homedir7(), ".claude", "settings.json");
4160
+ var DEFAULT_CLAUDE_SETTINGS_PATH = join10(homedir8(), ".claude", "settings.json");
3992
4161
  function resolveCliEntry() {
3993
4162
  return fileURLToPath(import.meta.url);
3994
4163
  }
@@ -4236,7 +4405,7 @@ function describeHookMode(tiers) {
4236
4405
  const gates = tiers.review ? "capture + review" : "capture";
4237
4406
  return `${enforcement}, ${gates}`;
4238
4407
  }
4239
- var DEFAULT_CODEX_FACE_PATH = join9(homedir7(), ".codex", "AGENTS.md");
4408
+ var DEFAULT_CODEX_FACE_PATH = join10(homedir8(), ".codex", "AGENTS.md");
4240
4409
  var LEFTOVER_FACE_NOTE = (label) => `${label} still carries an orientation block rendered by an earlier basou (0.39 or before); every Codex session on this machine reads it. \`basou channel clear codex\` removes it.`;
4241
4410
  async function faceHasLeftoverOrientationBlock(facePath) {
4242
4411
  try {
@@ -4248,8 +4417,8 @@ async function faceHasLeftoverOrientationBlock(facePath) {
4248
4417
  return false;
4249
4418
  }
4250
4419
  }
4251
- var DEFAULT_CODEX_HOOKS_PATH = join9(homedir7(), ".codex", "hooks.json");
4252
- var DEFAULT_CODEX_CONFIG_PATH = join9(homedir7(), ".codex", "config.toml");
4420
+ var DEFAULT_CODEX_HOOKS_PATH = join10(homedir8(), ".codex", "hooks.json");
4421
+ var DEFAULT_CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
4253
4422
  async function readHooksFile(path) {
4254
4423
  let raw;
4255
4424
  try {
@@ -4421,7 +4590,7 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
4421
4590
  }
4422
4591
 
4423
4592
  // src/commands/init.ts
4424
- import { basename as basename5, relative, resolve as resolve6 } from "path";
4593
+ import { basename as basename5, relative, resolve as resolve7 } from "path";
4425
4594
  import {
4426
4595
  appendBasouGitignore,
4427
4596
  createManifest,
@@ -4466,7 +4635,7 @@ async function doRunInit(options, ctx) {
4466
4635
  );
4467
4636
  }
4468
4637
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
4469
- const rel = relative(repositoryRoot, resolve6(cwd, p));
4638
+ const rel = relative(repositoryRoot, resolve7(cwd, p));
4470
4639
  return rel === "" ? "." : rel;
4471
4640
  });
4472
4641
  const paths = await ensureBasouDirectory(repositoryRoot);
@@ -4676,7 +4845,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
4676
4845
 
4677
4846
  // src/commands/portfolio.ts
4678
4847
  import { existsSync, statSync } from "fs";
4679
- import { join as join10 } from "path";
4848
+ import { join as join11 } from "path";
4680
4849
  function registerPortfolioCommand(program) {
4681
4850
  program.command("portfolio").description(
4682
4851
  "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"
@@ -4722,7 +4891,7 @@ async function runPortfolioList(options, ctx = {}) {
4722
4891
  async function doRunPortfolioList(options, ctx) {
4723
4892
  const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
4724
4893
  const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
4725
- const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join10(p, ".basou")));
4894
+ const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join11(p, ".basou")));
4726
4895
  const workspaces = await loadPortfolioConfig(configPath);
4727
4896
  const result = {
4728
4897
  configPath,
@@ -4787,7 +4956,7 @@ import {
4787
4956
  writeFileSync,
4788
4957
  writeSync
4789
4958
  } from "fs";
4790
- import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve7 } from "path";
4959
+ import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute4, join as join12, relative as relative2, resolve as resolve8 } from "path";
4791
4960
  import {
4792
4961
  appendBasouGitignore as appendBasouGitignore2,
4793
4962
  basouPaths as basouPaths11,
@@ -5162,14 +5331,14 @@ async function runProjectAdopt(options, ctx = {}) {
5162
5331
  }
5163
5332
  }
5164
5333
  function classifySourceRoot(repositoryRoot, declaredPath) {
5165
- const absolute = resolve7(repositoryRoot, declaredPath);
5334
+ const absolute = resolve8(repositoryRoot, declaredPath);
5166
5335
  let real;
5167
5336
  try {
5168
5337
  real = realpathSync(absolute);
5169
5338
  } catch {
5170
5339
  return { path: declaredPath, kind: "unresolved" };
5171
5340
  }
5172
- return { path: declaredPath, kind: existsSync2(join11(real, ".git")) ? "repo" : "non-repo" };
5341
+ return { path: declaredPath, kind: existsSync2(join12(real, ".git")) ? "repo" : "non-repo" };
5173
5342
  }
5174
5343
  async function doRunProjectAdopt(options, ctx) {
5175
5344
  const cwd = ctx.cwd ?? process.cwd();
@@ -5269,11 +5438,11 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5269
5438
  };
5270
5439
  let real;
5271
5440
  try {
5272
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5441
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5273
5442
  } catch {
5274
5443
  return { ...base, reachable: false, instructionFiles: [] };
5275
5444
  }
5276
- if (!existsSync2(join11(real, ".git"))) {
5445
+ if (!existsSync2(join12(real, ".git"))) {
5277
5446
  return { ...base, reachable: false, instructionFiles: [] };
5278
5447
  }
5279
5448
  try {
@@ -5281,7 +5450,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5281
5450
  for (const name of INSTRUCTION_FILES) {
5282
5451
  let present = true;
5283
5452
  try {
5284
- lstatSync(join11(real, name));
5453
+ lstatSync(join12(real, name));
5285
5454
  } catch {
5286
5455
  present = false;
5287
5456
  }
@@ -5388,14 +5557,14 @@ function gatherRepoGitignore(repositoryRoot, entry) {
5388
5557
  };
5389
5558
  let real;
5390
5559
  try {
5391
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5560
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5392
5561
  } catch {
5393
5562
  return { ...base, reachable: false, currentLines: [] };
5394
5563
  }
5395
- if (!existsSync2(join11(real, ".git"))) {
5564
+ if (!existsSync2(join12(real, ".git"))) {
5396
5565
  return { ...base, reachable: false, currentLines: [] };
5397
5566
  }
5398
- return { ...base, reachable: true, currentLines: readGitignoreLines(join11(real, ".gitignore")) };
5567
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join12(real, ".gitignore")) };
5399
5568
  }
5400
5569
  function hasErrorCode(error) {
5401
5570
  return error instanceof Error && typeof error.code === "string";
@@ -5409,7 +5578,7 @@ function readGitignoreLines(file) {
5409
5578
  }
5410
5579
  }
5411
5580
  function applyGitignorePlan(repositoryRoot, plan) {
5412
- const file = join11(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
5581
+ const file = join12(realpathSync(resolve8(repositoryRoot, plan.path)), ".gitignore");
5413
5582
  let existing = "";
5414
5583
  try {
5415
5584
  existing = readFileSync(file, "utf8");
@@ -5544,12 +5713,12 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5544
5713
  const base = { path: entry.path, ...isSelf ? { self: true } : {} };
5545
5714
  let real;
5546
5715
  try {
5547
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5716
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5548
5717
  } catch {
5549
5718
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
5550
5719
  }
5551
5720
  if (real === anchorReal) {
5552
- const anchorCanonical = join11(real, CANONICAL_FILE);
5721
+ const anchorCanonical = join12(real, CANONICAL_FILE);
5553
5722
  const anchorState = anchorCanonicalState(anchorCanonical);
5554
5723
  if (anchorState === "absent") {
5555
5724
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
@@ -5569,7 +5738,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5569
5738
  anchorCanonical,
5570
5739
  "self"
5571
5740
  ).map((spec) => {
5572
- const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
5741
+ const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
5573
5742
  return {
5574
5743
  name: spec.name,
5575
5744
  expectedTarget: spec.target,
@@ -5586,16 +5755,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5586
5755
  files: anchorFiles
5587
5756
  };
5588
5757
  }
5589
- if (!existsSync2(join11(real, ".git"))) {
5758
+ if (!existsSync2(join12(real, ".git"))) {
5590
5759
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
5591
5760
  }
5592
- const canonicalFile = isSelf ? join11(real, CANONICAL_FILE) : join11(anchorReal, "agents", basename6(real), CANONICAL_FILE);
5761
+ const canonicalFile = isSelf ? join12(real, CANONICAL_FILE) : join12(anchorReal, "agents", basename6(real), CANONICAL_FILE);
5593
5762
  if (!existsSync2(canonicalFile)) {
5594
5763
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
5595
5764
  }
5596
5765
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
5597
5766
  (spec) => {
5598
- const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
5767
+ const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
5599
5768
  return {
5600
5769
  name: spec.name,
5601
5770
  expectedTarget: spec.target,
@@ -5616,7 +5785,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5616
5785
  function applySymlinkPlan(repositoryRoot, plan) {
5617
5786
  let real;
5618
5787
  try {
5619
- real = realpathSync(resolve7(repositoryRoot, plan.path));
5788
+ real = realpathSync(resolve8(repositoryRoot, plan.path));
5620
5789
  } catch (error) {
5621
5790
  const message = failureReason(error);
5622
5791
  return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
@@ -5624,7 +5793,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
5624
5793
  const created = [];
5625
5794
  const failed = [];
5626
5795
  for (const { name, target } of plan.toCreate) {
5627
- const filePath = join11(real, name);
5796
+ const filePath = join12(real, name);
5628
5797
  try {
5629
5798
  mkdirSync(dirname4(filePath), { recursive: true });
5630
5799
  symlinkSync(target, filePath);
@@ -5642,9 +5811,9 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
5642
5811
  for (const entry of roster) {
5643
5812
  let name;
5644
5813
  try {
5645
- name = basename6(realpathSync(resolve7(repositoryRoot, entry.path)));
5814
+ name = basename6(realpathSync(resolve8(repositoryRoot, entry.path)));
5646
5815
  } catch {
5647
- name = basename6(resolve7(repositoryRoot, entry.path));
5816
+ name = basename6(resolve8(repositoryRoot, entry.path));
5648
5817
  }
5649
5818
  if (name === viewName) return entry.path;
5650
5819
  }
@@ -5658,7 +5827,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
5658
5827
  if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
5659
5828
  const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
5660
5829
  (spec) => {
5661
- const { state, actualTarget } = inspectSymlink(join11(viewDir, spec.name), spec.target);
5830
+ const { state, actualTarget } = inspectSymlink(join12(viewDir, spec.name), spec.target);
5662
5831
  return {
5663
5832
  name: spec.name,
5664
5833
  expectedTarget: spec.target,
@@ -5674,7 +5843,7 @@ function applyViewSymlinks(viewDir, files) {
5674
5843
  const failed = [];
5675
5844
  for (const f of files) {
5676
5845
  if (f.state !== "missing") continue;
5677
- const filePath = join11(viewDir, f.name);
5846
+ const filePath = join12(viewDir, f.name);
5678
5847
  try {
5679
5848
  mkdirSync(dirname4(filePath), { recursive: true });
5680
5849
  symlinkSync(f.expectedTarget, filePath);
@@ -5891,12 +6060,12 @@ async function runProjectWorkspace(options, ctx = {}) {
5891
6060
  }
5892
6061
  }
5893
6062
  function resolveViewDir(repositoryRoot, viewPath) {
5894
- const abs = resolve7(repositoryRoot, viewPath);
6063
+ const abs = resolve8(repositoryRoot, viewPath);
5895
6064
  try {
5896
6065
  return realpathSync(abs);
5897
6066
  } catch {
5898
6067
  try {
5899
- return join11(realpathSync(dirname4(abs)), basename6(abs));
6068
+ return join12(realpathSync(dirname4(abs)), basename6(abs));
5900
6069
  } catch {
5901
6070
  return abs;
5902
6071
  }
@@ -5905,7 +6074,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
5905
6074
  function gatherViewRepo(repositoryRoot, viewDir, entry) {
5906
6075
  let repoReal;
5907
6076
  try {
5908
- repoReal = realpathSync(resolve7(repositoryRoot, entry.path));
6077
+ repoReal = realpathSync(resolve8(repositoryRoot, entry.path));
5909
6078
  } catch {
5910
6079
  return { path: entry.path, reachable: false };
5911
6080
  }
@@ -5914,7 +6083,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
5914
6083
  return { path: entry.path, reachable: false };
5915
6084
  }
5916
6085
  const linkName = basename6(repoReal);
5917
- const { state, actualTarget } = inspectSymlink(join11(viewDir, linkName), expectedTarget);
6086
+ const { state, actualTarget } = inspectSymlink(join12(viewDir, linkName), expectedTarget);
5918
6087
  return {
5919
6088
  path: entry.path,
5920
6089
  reachable: true,
@@ -5928,7 +6097,7 @@ function applyViewPlan(viewDir, toCreate) {
5928
6097
  const created = [];
5929
6098
  const failed = [];
5930
6099
  for (const { name, target } of toCreate) {
5931
- const filePath = join11(viewDir, name);
6100
+ const filePath = join12(viewDir, name);
5932
6101
  try {
5933
6102
  mkdirSync(dirname4(filePath), { recursive: true });
5934
6103
  symlinkSync(target, filePath);
@@ -5943,7 +6112,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
5943
6112
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
5944
6113
  );
5945
6114
  function classifyViewLink(viewDir, name, rosterRealpaths) {
5946
- const filePath = join11(viewDir, name);
6115
+ const filePath = join12(viewDir, name);
5947
6116
  let isLink;
5948
6117
  try {
5949
6118
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -5957,12 +6126,12 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
5957
6126
  } catch {
5958
6127
  return null;
5959
6128
  }
5960
- const resolved = isAbsolute3(target) ? target : resolve7(viewDir, target);
6129
+ const resolved = isAbsolute4(target) ? target : resolve8(viewDir, target);
5961
6130
  try {
5962
6131
  if (rosterRealpaths.has(realpathSync(resolved))) return null;
5963
6132
  } catch {
5964
6133
  }
5965
- if (isAbsolute3(target)) return { target, kind: "absolute" };
6134
+ if (isAbsolute4(target)) return { target, kind: "absolute" };
5966
6135
  let isDir = false;
5967
6136
  try {
5968
6137
  isDir = statSync2(resolved).isDirectory();
@@ -5972,7 +6141,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
5972
6141
  if (!isDir) {
5973
6142
  return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
5974
6143
  }
5975
- return { target, kind: existsSync2(join11(resolved, ".git")) ? "repo" : "non-repo" };
6144
+ return { target, kind: existsSync2(join12(resolved, ".git")) ? "repo" : "non-repo" };
5976
6145
  }
5977
6146
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
5978
6147
  let names;
@@ -5997,7 +6166,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
5997
6166
  const pruned = [];
5998
6167
  const failed = [];
5999
6168
  for (const { name } of toPrune) {
6000
- const filePath = join11(viewDir, name);
6169
+ const filePath = join12(viewDir, name);
6001
6170
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
6002
6171
  if (c === null || c.kind !== "repo") {
6003
6172
  failed.push({
@@ -6043,11 +6212,11 @@ async function doRunProjectWorkspace(options, ctx) {
6043
6212
  } else {
6044
6213
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6045
6214
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
6046
- const rosterNames = roster.map((entry) => basename6(resolve7(repositoryRoot, entry.path)));
6215
+ const rosterNames = roster.map((entry) => basename6(resolve8(repositoryRoot, entry.path)));
6047
6216
  const rosterRealpaths = /* @__PURE__ */ new Set();
6048
6217
  for (const entry of roster) {
6049
6218
  try {
6050
- rosterRealpaths.add(realpathSync(resolve7(repositoryRoot, entry.path)));
6219
+ rosterRealpaths.add(realpathSync(resolve8(repositoryRoot, entry.path)));
6051
6220
  } catch {
6052
6221
  }
6053
6222
  }
@@ -6210,10 +6379,10 @@ async function runProjectPreset(options, ctx = {}) {
6210
6379
  }
6211
6380
  }
6212
6381
  function canonicalFileFor(anchorReal, canonicalName) {
6213
- return join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6382
+ return join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6214
6383
  }
6215
6384
  function canonicalLabelFor(canonicalName) {
6216
- return join11("agents", canonicalName, CANONICAL_FILE);
6385
+ return join12("agents", canonicalName, CANONICAL_FILE);
6217
6386
  }
6218
6387
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
6219
6388
  const declared = {
@@ -6227,14 +6396,14 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
6227
6396
  }
6228
6397
  let real;
6229
6398
  try {
6230
- real = realpathSync(resolve7(repositoryRoot, entry.path));
6399
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
6231
6400
  } catch {
6232
6401
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
6233
6402
  }
6234
6403
  if (real === anchorReal) {
6235
6404
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
6236
6405
  }
6237
- if (!existsSync2(join11(real, ".git"))) {
6406
+ if (!existsSync2(join12(real, ".git"))) {
6238
6407
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
6239
6408
  }
6240
6409
  const canonicalName = basename6(real);
@@ -6283,7 +6452,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
6283
6452
  anchorReal = void 0;
6284
6453
  }
6285
6454
  return roster.map((entry) => {
6286
- const abs = resolve7(repositoryRoot, entry.path);
6455
+ const abs = resolve8(repositoryRoot, entry.path);
6287
6456
  let real;
6288
6457
  try {
6289
6458
  real = realpathSync(abs);
@@ -6607,7 +6776,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6607
6776
  };
6608
6777
  let real;
6609
6778
  try {
6610
- real = realpathSync(resolve7(repositoryRoot, target));
6779
+ real = realpathSync(resolve8(repositoryRoot, target));
6611
6780
  } catch {
6612
6781
  return empty;
6613
6782
  }
@@ -6616,24 +6785,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6616
6785
  const instructionFiles = [];
6617
6786
  for (const name of INSTRUCTION_FILES) {
6618
6787
  try {
6619
- lstatSync(join11(real, name));
6788
+ lstatSync(join12(real, name));
6620
6789
  instructionFiles.push(name);
6621
6790
  } catch {
6622
6791
  }
6623
6792
  }
6624
6793
  let ignored;
6625
6794
  try {
6626
- ignored = new Set(readGitignoreLines(join11(real, ".gitignore")).map((l) => l.trim()));
6795
+ ignored = new Set(readGitignoreLines(join12(real, ".gitignore")).map((l) => l.trim()));
6627
6796
  } catch {
6628
6797
  ignored = /* @__PURE__ */ new Set();
6629
6798
  }
6630
6799
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
6631
- const canonical2 = existsSync2(join11(anchorReal, "agents", canonicalName, CANONICAL_FILE));
6800
+ const canonical2 = existsSync2(join12(anchorReal, "agents", canonicalName, CANONICAL_FILE));
6632
6801
  let viewLink = false;
6633
6802
  const viewPath = manifest.workspace.view;
6634
6803
  if (viewPath !== void 0) {
6635
6804
  try {
6636
- lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), canonicalName));
6805
+ lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), canonicalName));
6637
6806
  viewLink = true;
6638
6807
  } catch {
6639
6808
  }
@@ -6647,27 +6816,27 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6647
6816
  };
6648
6817
  }
6649
6818
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
6650
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6819
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6651
6820
  return expectedSymlinkTargets(repoReal, canonicalFile);
6652
6821
  }
6653
6822
  function viewLinkPointsAt(viewDir, name, repoReal) {
6654
- const filePath = join11(viewDir, name);
6823
+ const filePath = join12(viewDir, name);
6655
6824
  try {
6656
6825
  if (!lstatSync(filePath).isSymbolicLink()) return false;
6657
6826
  const target = readlinkSync(filePath);
6658
- if (isAbsolute3(target)) return false;
6659
- return realpathSync(resolve7(viewDir, target)) === repoReal;
6827
+ if (isAbsolute4(target)) return false;
6828
+ return realpathSync(resolve8(viewDir, target)) === repoReal;
6660
6829
  } catch {
6661
6830
  return false;
6662
6831
  }
6663
6832
  }
6664
6833
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
6665
- const filePath = join11(viewDir, name);
6834
+ const filePath = join12(viewDir, name);
6666
6835
  try {
6667
6836
  if (!lstatSync(filePath).isSymbolicLink()) return false;
6668
6837
  const target = readlinkSync(filePath);
6669
- if (isAbsolute3(target)) return false;
6670
- return resolve7(viewDir, target) === expectedRepoPath;
6838
+ if (isAbsolute4(target)) return false;
6839
+ return resolve8(viewDir, target) === expectedRepoPath;
6671
6840
  } catch {
6672
6841
  return false;
6673
6842
  }
@@ -6676,19 +6845,19 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6676
6845
  const anchorReal = realpathSync(repositoryRoot);
6677
6846
  let repoReal;
6678
6847
  try {
6679
- repoReal = realpathSync(resolve7(repositoryRoot, target));
6848
+ repoReal = realpathSync(resolve8(repositoryRoot, target));
6680
6849
  } catch {
6681
6850
  repoReal = void 0;
6682
6851
  }
6683
6852
  const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
6684
- const targetAbs = resolve7(repositoryRoot, target);
6853
+ const targetAbs = resolve8(repositoryRoot, target);
6685
6854
  const canonicalName = basename6(repoReal ?? targetAbs);
6686
6855
  const roster = manifest.repos ?? [];
6687
6856
  const declaredEntry = roster.find((r) => {
6688
6857
  try {
6689
- return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
6858
+ return realpathSync(resolve8(repositoryRoot, r.path)) === (repoReal ?? "\0");
6690
6859
  } catch {
6691
- return resolve7(repositoryRoot, r.path) === targetAbs;
6860
+ return resolve8(repositoryRoot, r.path) === targetAbs;
6692
6861
  }
6693
6862
  });
6694
6863
  const inRoster = declaredEntry !== void 0;
@@ -6697,7 +6866,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6697
6866
  const canonicalShared = roster.some((r) => {
6698
6867
  let rReal = null;
6699
6868
  try {
6700
- rReal = realpathSync(resolve7(repositoryRoot, r.path));
6869
+ rReal = realpathSync(resolve8(repositoryRoot, r.path));
6701
6870
  } catch {
6702
6871
  rReal = null;
6703
6872
  }
@@ -6705,15 +6874,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6705
6874
  if (repoReal !== void 0 && rReal === repoReal) return false;
6706
6875
  return basename6(rReal).toLowerCase() === cnFold;
6707
6876
  }
6708
- if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
6709
- return basename6(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
6877
+ if (resolve8(repositoryRoot, r.path) === targetAbs) return false;
6878
+ return basename6(resolve8(repositoryRoot, r.path)).toLowerCase() === cnFold;
6710
6879
  });
6711
6880
  const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
6712
6881
  const items = [];
6713
6882
  if (!isAnchor) {
6714
6883
  if (repoReal !== void 0) {
6715
6884
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
6716
- const { state, actualTarget } = inspectSymlink(join11(repoReal, spec.name), spec.target);
6885
+ const { state, actualTarget } = inspectSymlink(join12(repoReal, spec.name), spec.target);
6717
6886
  if (isSelf) {
6718
6887
  if (state !== "missing")
6719
6888
  items.push({
@@ -6750,7 +6919,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6750
6919
  }
6751
6920
  let ignored;
6752
6921
  try {
6753
- ignored = new Set(readGitignoreLines(join11(repoReal, ".gitignore")).map((l) => l.trim()));
6922
+ ignored = new Set(readGitignoreLines(join12(repoReal, ".gitignore")).map((l) => l.trim()));
6754
6923
  for (const p of INSTRUCTION_FILES) {
6755
6924
  if (ignored.has(p) || ignored.has(`/${p}`)) {
6756
6925
  items.push({
@@ -6773,7 +6942,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6773
6942
  const viewPath = manifest.workspace.view;
6774
6943
  if (viewPath !== void 0) {
6775
6944
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6776
- const linkPath = join11(viewDir, canonicalName);
6945
+ const linkPath = join12(viewDir, canonicalName);
6777
6946
  let isLink = false;
6778
6947
  try {
6779
6948
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -6799,8 +6968,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6799
6968
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
6800
6969
  }
6801
6970
  }
6802
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6803
- const canonicalLabel = join11("agents", canonicalName, CANONICAL_FILE);
6971
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6972
+ const canonicalLabel = join12("agents", canonicalName, CANONICAL_FILE);
6804
6973
  let canonicalIsLink = false;
6805
6974
  try {
6806
6975
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -6887,7 +7056,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6887
7056
  const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
6888
7057
  let currentRepoReal = null;
6889
7058
  try {
6890
- currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
7059
+ currentRepoReal = realpathSync(resolve8(repositoryRoot, plan.target));
6891
7060
  } catch {
6892
7061
  currentRepoReal = null;
6893
7062
  }
@@ -6904,12 +7073,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6904
7073
  );
6905
7074
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
6906
7075
  const expected = expectedByName.get(item.label);
6907
- if (repoReal === null || expected === void 0 || inspectSymlink(join11(repoReal, item.label), expected).state !== "correct") {
7076
+ if (repoReal === null || expected === void 0 || inspectSymlink(join12(repoReal, item.label), expected).state !== "correct") {
6908
7077
  changed(item.label);
6909
7078
  continue;
6910
7079
  }
6911
7080
  try {
6912
- unlinkSync(join11(repoReal, item.label));
7081
+ unlinkSync(join12(repoReal, item.label));
6913
7082
  removed.push(item.label);
6914
7083
  } catch (error) {
6915
7084
  failed.push({ label: item.label, message: failureReason(error) });
@@ -6922,13 +7091,13 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6922
7091
  continue;
6923
7092
  }
6924
7093
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6925
- const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve7(repositoryRoot, plan.target));
7094
+ const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve8(repositoryRoot, plan.target));
6926
7095
  if (!owned) {
6927
7096
  changed(item.label);
6928
7097
  continue;
6929
7098
  }
6930
7099
  try {
6931
- unlinkSync(join11(viewDir, item.label));
7100
+ unlinkSync(join12(viewDir, item.label));
6932
7101
  removed.push(`view/${item.label}`);
6933
7102
  } catch (error) {
6934
7103
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -6936,7 +7105,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6936
7105
  }
6937
7106
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
6938
7107
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
6939
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7108
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6940
7109
  try {
6941
7110
  if (lstatSync(canonicalFile).isSymbolicLink()) {
6942
7111
  changed(item.label);
@@ -7084,7 +7253,7 @@ async function doRunProjectArchive(target, options, ctx) {
7084
7253
  const roster = manifest.repos ?? [];
7085
7254
  let targetIsAnchor = false;
7086
7255
  try {
7087
- targetIsAnchor = realpathSync(resolve7(repositoryRoot, target)) === realpathSync(repositoryRoot);
7256
+ targetIsAnchor = realpathSync(resolve8(repositoryRoot, target)) === realpathSync(repositoryRoot);
7088
7257
  } catch {
7089
7258
  targetIsAnchor = false;
7090
7259
  }
@@ -7210,12 +7379,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
7210
7379
  } catch {
7211
7380
  return { canonicalDirOld: false, viewLinkOld: false };
7212
7381
  }
7213
- const canonicalDirOld = existsSync2(join11(anchorReal, "agents", oldBasename));
7382
+ const canonicalDirOld = existsSync2(join12(anchorReal, "agents", oldBasename));
7214
7383
  let viewLinkOld = false;
7215
7384
  const viewPath = manifest.workspace.view;
7216
7385
  if (viewPath !== void 0) {
7217
7386
  try {
7218
- lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), oldBasename));
7387
+ lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), oldBasename));
7219
7388
  viewLinkOld = true;
7220
7389
  } catch {
7221
7390
  }
@@ -7241,7 +7410,7 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
7241
7410
  const roster = manifest.repos ?? [];
7242
7411
  let oldIsAnchor = false;
7243
7412
  try {
7244
- oldIsAnchor = realpathSync(resolve7(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
7413
+ oldIsAnchor = realpathSync(resolve8(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
7245
7414
  } catch {
7246
7415
  oldIsAnchor = false;
7247
7416
  }
@@ -7387,7 +7556,7 @@ async function doRunProjectNew(repos, options, ctx) {
7387
7556
  const viewStem = productName ?? workspaceName;
7388
7557
  const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
7389
7558
  const declared = repos.map((p) => {
7390
- const abs = resolve7(cwd, p);
7559
+ const abs = resolve8(cwd, p);
7391
7560
  let real;
7392
7561
  try {
7393
7562
  real = realpathSync(abs);
@@ -7573,7 +7742,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
7573
7742
  console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
7574
7743
  return;
7575
7744
  }
7576
- const anchorDoc = join11(repositoryRoot, CANONICAL_FILE);
7745
+ const anchorDoc = join12(repositoryRoot, CANONICAL_FILE);
7577
7746
  if (pathPresent(anchorDoc)) {
7578
7747
  console.log(
7579
7748
  `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
@@ -7635,7 +7804,7 @@ function regularFileSpokes(repoReal) {
7635
7804
  const out = [];
7636
7805
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
7637
7806
  try {
7638
- const st = lstatSync(join11(repoReal, spoke));
7807
+ const st = lstatSync(join12(repoReal, spoke));
7639
7808
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
7640
7809
  } catch {
7641
7810
  }
@@ -7652,7 +7821,7 @@ function pathPresent(p) {
7652
7821
  }
7653
7822
  function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, viewCanonicalName) {
7654
7823
  const declaredEntry = roster.find((entry) => {
7655
- const entryAbs = resolve7(repositoryRoot, entry.path);
7824
+ const entryAbs = resolve8(repositoryRoot, entry.path);
7656
7825
  if (argReal !== void 0) {
7657
7826
  try {
7658
7827
  if (realpathSync(entryAbs) === argReal) return true;
@@ -7681,8 +7850,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
7681
7850
  };
7682
7851
  }
7683
7852
  const isAnchor = argReal === anchorReal;
7684
- const reachable = existsSync2(join11(argReal, ".git"));
7685
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7853
+ const reachable = existsSync2(join12(argReal, ".git"));
7854
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7686
7855
  return {
7687
7856
  path,
7688
7857
  declared,
@@ -7691,13 +7860,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
7691
7860
  reachable,
7692
7861
  canonicalName,
7693
7862
  ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
7694
- agentsState: inspectAgentsState(join11(argReal, CANONICAL_FILE)),
7863
+ agentsState: inspectAgentsState(join12(argReal, CANONICAL_FILE)),
7695
7864
  canonicalExists: pathPresent(canonicalFile),
7696
7865
  regularSpokes: regularFileSpokes(argReal)
7697
7866
  };
7698
7867
  }
7699
7868
  function relocateAgentsFile(repoReal, canonicalFile) {
7700
- const agentsFile = join11(repoReal, CANONICAL_FILE);
7869
+ const agentsFile = join12(repoReal, CANONICAL_FILE);
7701
7870
  try {
7702
7871
  mkdirSync(dirname4(canonicalFile), { recursive: true });
7703
7872
  } catch (error) {
@@ -7792,7 +7961,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7792
7961
  }
7793
7962
  return result2;
7794
7963
  }
7795
- const argAbs = resolve7(repositoryRoot, repo);
7964
+ const argAbs = resolve8(repositoryRoot, repo);
7796
7965
  let argReal;
7797
7966
  try {
7798
7967
  argReal = realpathSync(argAbs);
@@ -7813,7 +7982,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7813
7982
  let failure;
7814
7983
  let partial = false;
7815
7984
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
7816
- const canonicalFile = join11(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
7985
+ const canonicalFile = join12(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
7817
7986
  const res = relocateAgentsFile(argReal, canonicalFile);
7818
7987
  if (res.ok) {
7819
7988
  applied = true;
@@ -8013,88 +8182,19 @@ function renderProjectRetrofit(result) {
8013
8182
  }
8014
8183
 
8015
8184
  // src/commands/protocol.ts
8016
- import { readFile as readFile4 } from "fs/promises";
8017
- import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers4, readMarkdownFile as readMarkdownFile8 } from "@basou/core";
8018
-
8019
- // src/lib/protocols-config.ts
8020
- import { homedir as homedir8 } from "os";
8021
- import { isAbsolute as isAbsolute4, join as join12, resolve as resolve8 } from "path";
8022
- import { readYamlFile as readYamlFile5 } from "@basou/core";
8023
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join12(homedir8(), ".basou", "protocols.yaml");
8024
- var DEFAULT_TARGET_PATH = join12(homedir8(), ".claude", "CLAUDE.md");
8025
- var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
8026
- var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
8027
- function expandTilde3(p) {
8028
- if (p === "~") return homedir8();
8029
- if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
8030
- return p;
8031
- }
8032
- function isRecord3(value) {
8033
- return typeof value === "object" && value !== null && !Array.isArray(value);
8034
- }
8035
- async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
8036
- let raw;
8037
- try {
8038
- raw = await readYamlFile5(configPath);
8039
- } catch (error) {
8040
- if (error instanceof Error && error.message === "YAML file not found") {
8041
- throw new Error(
8042
- "No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
8043
- );
8044
- }
8045
- if (error instanceof Error && error.message === "Failed to parse YAML content") {
8046
- throw new Error("~/.basou/protocols.yaml is not valid YAML.");
8047
- }
8048
- throw error;
8049
- }
8050
- if (!isRecord3(raw) || !Array.isArray(raw.protocols)) {
8051
- throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
8052
- }
8053
- for (const key of Object.keys(raw)) {
8054
- if (!ALLOWED_TOP_KEYS.has(key)) {
8055
- throw new Error(
8056
- `~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
8057
- );
8058
- }
8059
- }
8060
- const seen = /* @__PURE__ */ new Set();
8061
- const result = [];
8062
- for (const entry of raw.protocols) {
8063
- if (!isRecord3(entry)) {
8064
- throw new Error("Each protocol entry must be a mapping with a 'source' key.");
8065
- }
8066
- for (const key of Object.keys(entry)) {
8067
- if (!ALLOWED_ENTRY_KEYS.has(key)) {
8068
- throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
8069
- }
8070
- }
8071
- if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
8072
- throw new Error("Each protocol entry needs a non-empty string 'source'.");
8073
- }
8074
- if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
8075
- throw new Error("A protocol entry 'title' must be a non-empty string when present.");
8076
- }
8077
- const expanded = expandTilde3(entry.source.trim());
8078
- if (!isAbsolute4(expanded)) {
8079
- throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
8080
- }
8081
- const abs = resolve8(expanded);
8082
- if (seen.has(abs)) {
8083
- throw new Error("Duplicate protocol source (each source path may appear only once).");
8084
- }
8085
- seen.add(abs);
8086
- result.push(
8087
- entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
8088
- );
8089
- }
8090
- if (result.length === 0) {
8091
- throw new Error("~/.basou/protocols.yaml has no protocols.");
8092
- }
8093
- return result;
8094
- }
8095
-
8096
- // src/commands/protocol.ts
8097
- var PROTOCOL_MARKERS = { start: PROTOCOL_START, end: PROTOCOL_END };
8185
+ import { readFile as readFile4, stat as stat5 } from "fs/promises";
8186
+ import {
8187
+ carryForwardProtocolStamp,
8188
+ PROTOCOL_END as PROTOCOL_END2,
8189
+ PROTOCOL_START as PROTOCOL_START2,
8190
+ parseMarkers as parseMarkers4,
8191
+ parseProtocolStamp as parseProtocolStamp2,
8192
+ protocolBlockHash,
8193
+ readMarkdownFile as readMarkdownFile8,
8194
+ renderProtocolStamp,
8195
+ unstampedProtocolSectionsFrom
8196
+ } from "@basou/core";
8197
+ var PROTOCOL_MARKERS = { start: PROTOCOL_START2, end: PROTOCOL_END2 };
8098
8198
  var MANAGED_NOTE = "<!-- Managed by basou: 'basou protocol sync' regenerates everything between the BASOU:PROTOCOLS markers from ~/.basou/protocols.yaml. Manual edits inside the block are overwritten; edit the source files instead. -->";
8099
8199
  function registerProtocolCommand(program) {
8100
8200
  const protocol = program.command("protocol").description("Manage the basou-managed standing-protocol block in the global CLAUDE.md");
@@ -8154,24 +8254,54 @@ async function readProtocolSources(entries) {
8154
8254
  }
8155
8255
  return out;
8156
8256
  }
8157
- function buildBlock(sources) {
8158
- const sections = sources.map(({ entry, content }) => {
8257
+ function buildSections(sources) {
8258
+ return sources.map(({ entry, content }) => {
8159
8259
  const body = content.replace(/\s+$/, "");
8160
8260
  return entry.title !== void 0 ? `## ${entry.title}
8161
8261
 
8162
8262
  ${body}` : body;
8163
- });
8263
+ }).join("\n\n");
8264
+ }
8265
+ function buildBlock(sections, stamp) {
8164
8266
  return `${MANAGED_NOTE}
8267
+ ${renderProtocolStamp(stamp)}
8165
8268
 
8166
- ${sections.join("\n\n")}
8269
+ ${sections}
8167
8270
  `;
8168
8271
  }
8272
+ async function readPreviousStamp(target) {
8273
+ const existing = await readMarkdownFile8(target);
8274
+ if (existing === null) return null;
8275
+ const section = parseMarkers4(existing, PROTOCOL_MARKERS);
8276
+ if (section.kind !== "ok") return null;
8277
+ const stamp = parseProtocolStamp2(section.generated);
8278
+ if (stamp !== null) return stamp;
8279
+ const writtenAt = await lastWrittenAt(target);
8280
+ if (writtenAt === null) return null;
8281
+ return {
8282
+ changedAt: writtenAt,
8283
+ contentHash: protocolBlockHash(unstampedProtocolSectionsFrom(section.generated))
8284
+ };
8285
+ }
8286
+ async function lastWrittenAt(target) {
8287
+ try {
8288
+ return new Date((await stat5(target)).mtimeMs).toISOString();
8289
+ } catch {
8290
+ return null;
8291
+ }
8292
+ }
8169
8293
  async function doRunProtocolSync(options, ctx = {}) {
8170
8294
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
8171
8295
  const target = options.target ?? DEFAULT_TARGET_PATH;
8172
8296
  const entries = await loadProtocolsConfig(configPath);
8173
8297
  const sources = await readProtocolSources(entries);
8174
- const block = buildBlock(sources);
8298
+ const sections = buildSections(sources);
8299
+ const stamp = carryForwardProtocolStamp({
8300
+ sections,
8301
+ previous: await readPreviousStamp(target),
8302
+ now: (/* @__PURE__ */ new Date()).toISOString()
8303
+ });
8304
+ const block = buildBlock(sections, stamp);
8175
8305
  const foreign = await findForeignWorkspaceNames({
8176
8306
  text: block,
8177
8307
  configPath: ctx.portfolioConfigPath
@@ -8241,7 +8371,7 @@ import {
8241
8371
  import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
8242
8372
 
8243
8373
  // src/commands/refresh-watch.ts
8244
- import { readdir as readdir2, stat as stat5 } from "fs/promises";
8374
+ import { readdir as readdir2, stat as stat6 } from "fs/promises";
8245
8375
  import { homedir as homedir9 } from "os";
8246
8376
  import { join as join13 } from "path";
8247
8377
  import { findErrorCode as findErrorCode8 } from "@basou/core";
@@ -8270,7 +8400,7 @@ async function scanSourceLogs(roots) {
8270
8400
  await walk(full);
8271
8401
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
8272
8402
  try {
8273
- const info = await stat5(full);
8403
+ const info = await stat6(full);
8274
8404
  out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
8275
8405
  } catch (error) {
8276
8406
  if (findErrorCode8(error, "ENOENT")) continue;
@@ -11730,7 +11860,7 @@ import { InvalidArgumentError as InvalidArgumentError8 } from "commander";
11730
11860
 
11731
11861
  // src/lib/portfolio-coverage.ts
11732
11862
  import { createReadStream as createReadStream2 } from "fs";
11733
- import { readdir as readdir3, stat as stat6 } from "fs/promises";
11863
+ import { readdir as readdir3, stat as stat7 } from "fs/promises";
11734
11864
  import { homedir as homedir12 } from "os";
11735
11865
  import { basename as basename8, dirname as dirname5, join as join17 } from "path";
11736
11866
  import { createInterface as createInterface2 } from "readline";
@@ -11877,7 +12007,7 @@ async function isDirEntry(parent, entry) {
11877
12007
  if (entry.isDirectory()) return true;
11878
12008
  if (!entry.isSymbolicLink()) return false;
11879
12009
  try {
11880
- return (await stat6(join17(parent, entry.name))).isDirectory();
12010
+ return (await stat7(join17(parent, entry.name))).isDirectory();
11881
12011
  } catch {
11882
12012
  return false;
11883
12013
  }
@@ -13646,7 +13776,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
13646
13776
  function readBuildStamp() {
13647
13777
  if (false) return void 0;
13648
13778
  try {
13649
- return JSON.parse('{"version":"0.45.0","commit":"6b04208","committedAt":"2026-09-19T01:07:28+09:00"}');
13779
+ return JSON.parse('{"version":"0.46.0","commit":"dbc9c57","committedAt":"2026-09-19T15:43:35+09:00"}');
13650
13780
  } catch {
13651
13781
  return void 0;
13652
13782
  }