@basou/cli 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1250,7 +1250,8 @@ Input format (a JSON array; one object per decision):
1250
1250
  "rationale": "Workspace protocol and a content-addressed store fit our layout.",
1251
1251
  "alternatives": ["npm workspaces", "yarn"],
1252
1252
  "rejected_reason": "npm hoisting caused phantom-dependency bugs",
1253
- "linked_files": ["pnpm-workspace.yaml"]
1253
+ "linked_files": ["pnpm-workspace.yaml"],
1254
+ "kind": "decision"
1254
1255
  },
1255
1256
  {
1256
1257
  "title": "Form-based admin editing is the next track (only 6/19 sections done)",
@@ -1259,10 +1260,14 @@ Input format (a JSON array; one object per decision):
1259
1260
  }
1260
1261
  ]
1261
1262
 
1262
- Only "title" is required; every other field is optional. Set "kind": "track" to
1263
- record a strategic, UNFINISHED direction (+ why): orientation/handoff resurface
1264
- open tracks every session until you close one with 'basou decision void <id>'.
1265
- Absent / "decision" is a point-in-time decision (surfaced only as the latest).
1263
+ "title" is required. "kind" names the vessel: "track" records a strategic,
1264
+ UNFINISHED direction (+ why) and orientation/handoff resurface it every session
1265
+ until you close it with 'basou decision void <id>', while "decision" records a
1266
+ point-in-time call (surfaced only as the latest). Every other field is optional.
1267
+
1268
+ DEPRECATED: omitting "kind" still writes the item, as a decision, but warns --
1269
+ and becomes an ERROR in the next release. It has no default because getting it
1270
+ wrong fails silently: a track filed as a decision simply never comes back.
1266
1271
  All decisions are written into one ad-hoc session timestamped now, so
1267
1272
  orientation surfaces them as the latest decisions. Run from a workspace-view
1268
1273
  directory and it resolves to the planning repo, like 'basou orient' /
@@ -1270,7 +1275,7 @@ directory and it resolves to the planning repo, like 'basou orient' /
1270
1275
 
1271
1276
  Example (heredoc on stdin):
1272
1277
  basou decision capture <<'JSON'
1273
- [{ "title": "Ship the capture command", "rationale": "Close the why-capture gap" }]
1278
+ [{ "title": "Ship the capture command", "rationale": "Close the why-capture gap", "kind": "decision" }]
1274
1279
  JSON
1275
1280
  `;
1276
1281
  async function runDecisionRecord(options, ctx = {}) {
@@ -1306,12 +1311,11 @@ async function warnLinkedFilesOutsideRoots(input) {
1306
1311
  } catch {
1307
1312
  }
1308
1313
  }
1309
- var TRACK_MARKER_IN_TITLE = /^\s*track\s*[:\uff1a]|[[(\uff08\uff3b\u3010]\s*track\s*[\])\uff09\uff3d\u3011]/i;
1310
- function warnTrackMarkerWithoutKind(decisions, markerWithoutKind) {
1311
- for (const index of markerWithoutKind) {
1314
+ function warnMissingKind(decisions, indices) {
1315
+ for (const index of indices) {
1312
1316
  const title = (decisions[index]?.title ?? "").trim();
1313
1317
  console.error(
1314
- `basou: decision[${index}] title carries a track marker (${title.slice(0, 40)}) but "kind" is absent \u2014 it is recorded as a point-in-time decision and will NOT resurface in orient. Set "kind": "track" to open a track, or "kind": "decision" to say the marker is part of the title.`
1318
+ `basou: decision[${index}] ("${title.slice(0, 40)}") declares no "kind" \u2014 it is recorded as a point-in-time decision and will NOT resurface in orient. Set "kind": "track" for an unfinished direction, or "kind": "decision" to say it is settled. Omitting it becomes an ERROR in the next release.`
1315
1319
  );
1316
1320
  }
1317
1321
  }
@@ -1412,8 +1416,8 @@ async function doRunDecisionCapture(options, ctx) {
1412
1416
  const paths = basouPaths4(repositoryRoot);
1413
1417
  await assertWorkspaceInitialized2(paths.root);
1414
1418
  const raw = await readCaptureInput(options, ctx);
1415
- const { decisions, markerWithoutKind } = parseCaptureInput(raw);
1416
- warnTrackMarkerWithoutKind(decisions, markerWithoutKind);
1419
+ const { decisions, missingKind } = parseCaptureInput(raw);
1420
+ warnMissingKind(decisions, missingKind);
1417
1421
  await warnLinkedFilesOutsideRoots({
1418
1422
  linkedFiles: decisions.flatMap((d) => d.linked_files ?? []),
1419
1423
  cwd,
@@ -1675,13 +1679,13 @@ function parseCaptureInput(raw) {
1675
1679
  throw new Error("Input array must contain at least one decision.");
1676
1680
  }
1677
1681
  const decisions = [];
1678
- const markerWithoutKind = [];
1682
+ const missingKind = [];
1679
1683
  parsed.forEach((item, index) => {
1680
- const { input, kindWasPresent } = validateCaptureItem(item, index);
1684
+ const input = validateCaptureItem(item, index);
1681
1685
  decisions.push(input);
1682
- if (!kindWasPresent && TRACK_MARKER_IN_TITLE.test(input.title)) markerWithoutKind.push(index);
1686
+ if (input.kind === void 0) missingKind.push(index);
1683
1687
  });
1684
- return { decisions, markerWithoutKind };
1688
+ return { decisions, missingKind };
1685
1689
  }
1686
1690
  function validateCaptureItem(item, index) {
1687
1691
  if (typeof item !== "object" || item === null || Array.isArray(item)) {
@@ -1703,7 +1707,7 @@ function validateCaptureItem(item, index) {
1703
1707
  if (obj.kind !== "decision" && obj.kind !== "track") {
1704
1708
  throw new Error(`decision[${index}].kind must be "decision" or "track", got '${obj.kind}'.`);
1705
1709
  }
1706
- if (obj.kind === "track") out.kind = "track";
1710
+ out.kind = obj.kind;
1707
1711
  }
1708
1712
  if (obj.rationale !== void 0) {
1709
1713
  out.rationale = requireNonEmptyString(obj.rationale, index, "rationale");
@@ -1742,7 +1746,7 @@ function validateCaptureItem(item, index) {
1742
1746
  }
1743
1747
  });
1744
1748
  }
1745
- return { input: out, kindWasPresent: obj.kind !== void 0 };
1749
+ return out;
1746
1750
  }
1747
1751
  function requireNonEmptyString(value, index, field) {
1748
1752
  if (typeof value !== "string" || isBlank(value)) {
@@ -1793,8 +1797,13 @@ function captureItemToPayload(item) {
1793
1797
  if (item.input.kind !== void 0) payload.kind = item.input.kind;
1794
1798
  return payload;
1795
1799
  }
1796
- function kindMarker(kind) {
1797
- return kind === "track" ? " [TRACK]" : " [DECISION]";
1800
+ function previewKindMarker(kind) {
1801
+ if (kind === "track") return " [TRACK]";
1802
+ return kind === "decision" ? " [DECISION]" : " [NO KIND]";
1803
+ }
1804
+ function recordedKindMarker(kind) {
1805
+ if (kind === "track") return " [TRACK]";
1806
+ return kind === "decision" ? "" : " [NO KIND]";
1798
1807
  }
1799
1808
  function printCapturePreview(options, decisions) {
1800
1809
  if (options.json === true) {
@@ -1805,7 +1814,7 @@ function printCapturePreview(options, decisions) {
1805
1814
  `Would capture ${decisions.length} decision${decisions.length === 1 ? "" : "s"} (dry run; nothing written):`
1806
1815
  );
1807
1816
  for (const decision of decisions) {
1808
- console.log(`- ${decision.title}${kindMarker(decision.kind)}`);
1817
+ console.log(`- ${decision.title}${previewKindMarker(decision.kind)}`);
1809
1818
  }
1810
1819
  }
1811
1820
  function printCaptureResult(options, result) {
@@ -1826,7 +1835,7 @@ function printCaptureResult(options, result) {
1826
1835
  `Captured ${result.items.length} decision${result.items.length === 1 ? "" : "s"} in ad-hoc session ${sid}:`
1827
1836
  );
1828
1837
  for (const item of result.items) {
1829
- console.log(`- ${item.decisionId}: ${item.input.title}${kindMarker(item.input.kind)}`);
1838
+ console.log(`- ${item.decisionId}: ${item.input.title}${recordedKindMarker(item.input.kind)}`);
1830
1839
  }
1831
1840
  }
1832
1841
  function pickRichFields(options) {
@@ -2443,8 +2452,8 @@ async function assertWorkspaceInitialized4(basouRoot) {
2443
2452
  // src/commands/hook.ts
2444
2453
  import { execFile } from "child_process";
2445
2454
  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";
2455
+ import { homedir as homedir8 } from "os";
2456
+ import { join as join10 } from "path";
2448
2457
  import { fileURLToPath } from "url";
2449
2458
  import { promisify } from "util";
2450
2459
  import {
@@ -2454,12 +2463,20 @@ import {
2454
2463
  evaluateStopHook,
2455
2464
  findBasouSessionStartHook,
2456
2465
  findBasouStopHookCommand,
2466
+ isProtocolUpdateDue,
2457
2467
  ORIENTATION_END as ORIENTATION_END2,
2458
2468
  ORIENTATION_START as ORIENTATION_START2,
2469
+ PROTOCOL_END,
2470
+ PROTOCOL_START,
2459
2471
  parseMarkers as parseMarkers2,
2472
+ parseProtocolStamp,
2473
+ protocolSectionsFrom,
2474
+ protocolUpdateToken,
2460
2475
  readMarkdownFile as readMarkdownFile6,
2461
2476
  removeSessionStartHook,
2462
2477
  removeStopHook,
2478
+ renderProtocolUpdate,
2479
+ transcriptStartedAt,
2463
2480
  upsertSessionStartHook,
2464
2481
  upsertStopHook
2465
2482
  } from "@basou/core";
@@ -2714,6 +2731,83 @@ async function warnIfPositionNamesOtherWorkspaces(args) {
2714
2731
  }
2715
2732
  }
2716
2733
 
2734
+ // src/lib/protocols-config.ts
2735
+ import { homedir as homedir5 } from "os";
2736
+ import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
2737
+ import { readYamlFile as readYamlFile4 } from "@basou/core";
2738
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join7(homedir5(), ".basou", "protocols.yaml");
2739
+ var DEFAULT_TARGET_PATH = join7(homedir5(), ".claude", "CLAUDE.md");
2740
+ var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
2741
+ var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
2742
+ function expandTilde2(p) {
2743
+ if (p === "~") return homedir5();
2744
+ if (p.startsWith("~/")) return join7(homedir5(), p.slice(2));
2745
+ return p;
2746
+ }
2747
+ function isRecord2(value) {
2748
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2749
+ }
2750
+ async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
2751
+ let raw;
2752
+ try {
2753
+ raw = await readYamlFile4(configPath);
2754
+ } catch (error) {
2755
+ if (error instanceof Error && error.message === "YAML file not found") {
2756
+ throw new Error(
2757
+ "No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
2758
+ );
2759
+ }
2760
+ if (error instanceof Error && error.message === "Failed to parse YAML content") {
2761
+ throw new Error("~/.basou/protocols.yaml is not valid YAML.");
2762
+ }
2763
+ throw error;
2764
+ }
2765
+ if (!isRecord2(raw) || !Array.isArray(raw.protocols)) {
2766
+ throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
2767
+ }
2768
+ for (const key of Object.keys(raw)) {
2769
+ if (!ALLOWED_TOP_KEYS.has(key)) {
2770
+ throw new Error(
2771
+ `~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
2772
+ );
2773
+ }
2774
+ }
2775
+ const seen = /* @__PURE__ */ new Set();
2776
+ const result = [];
2777
+ for (const entry of raw.protocols) {
2778
+ if (!isRecord2(entry)) {
2779
+ throw new Error("Each protocol entry must be a mapping with a 'source' key.");
2780
+ }
2781
+ for (const key of Object.keys(entry)) {
2782
+ if (!ALLOWED_ENTRY_KEYS.has(key)) {
2783
+ throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
2784
+ }
2785
+ }
2786
+ if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
2787
+ throw new Error("Each protocol entry needs a non-empty string 'source'.");
2788
+ }
2789
+ if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
2790
+ throw new Error("A protocol entry 'title' must be a non-empty string when present.");
2791
+ }
2792
+ const expanded = expandTilde2(entry.source.trim());
2793
+ if (!isAbsolute2(expanded)) {
2794
+ throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
2795
+ }
2796
+ const abs = resolve4(expanded);
2797
+ if (seen.has(abs)) {
2798
+ throw new Error("Duplicate protocol source (each source path may appear only once).");
2799
+ }
2800
+ seen.add(abs);
2801
+ result.push(
2802
+ entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
2803
+ );
2804
+ }
2805
+ if (result.length === 0) {
2806
+ throw new Error("~/.basou/protocols.yaml has no protocols.");
2807
+ }
2808
+ return result;
2809
+ }
2810
+
2717
2811
  // src/commands/orient.ts
2718
2812
  import {
2719
2813
  assertBasouRootSafe as assertBasouRootSafe7,
@@ -2724,22 +2818,22 @@ import {
2724
2818
  } from "@basou/core";
2725
2819
 
2726
2820
  // 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));
2821
+ import { homedir as homedir6 } from "os";
2822
+ import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "path";
2823
+ import { readYamlFile as readYamlFile5 } from "@basou/core";
2824
+ var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
2825
+ function expandTilde3(p) {
2826
+ if (p === "~") return homedir6();
2827
+ if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
2734
2828
  return p;
2735
2829
  }
2736
- function isRecord2(value) {
2830
+ function isRecord3(value) {
2737
2831
  return typeof value === "object" && value !== null && !Array.isArray(value);
2738
2832
  }
2739
2833
  async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
2740
2834
  let raw;
2741
2835
  try {
2742
- raw = await readYamlFile4(configPath);
2836
+ raw = await readYamlFile5(configPath);
2743
2837
  } catch (error) {
2744
2838
  if (error instanceof Error && error.message === "YAML file not found") {
2745
2839
  return null;
@@ -2749,25 +2843,25 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
2749
2843
  }
2750
2844
  throw error;
2751
2845
  }
2752
- if (!isRecord2(raw) || !Array.isArray(raw.hosts)) {
2846
+ if (!isRecord3(raw) || !Array.isArray(raw.hosts)) {
2753
2847
  throw new Error("~/.basou/hosts.yaml must contain a 'hosts:' list.");
2754
2848
  }
2755
2849
  const seenPaths = /* @__PURE__ */ new Set();
2756
2850
  const seenLabels = /* @__PURE__ */ new Set();
2757
2851
  const result = [];
2758
2852
  for (const entry of raw.hosts) {
2759
- if (!isRecord2(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
2853
+ if (!isRecord3(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
2760
2854
  throw new Error("Each host needs a non-empty string 'label'.");
2761
2855
  }
2762
2856
  const label = entry.label.trim();
2763
2857
  if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
2764
2858
  throw new Error("Each host needs a non-empty string 'path'.");
2765
2859
  }
2766
- const expanded = expandTilde2(entry.path.trim());
2767
- if (!isAbsolute2(expanded)) {
2860
+ const expanded = expandTilde3(entry.path.trim());
2861
+ if (!isAbsolute3(expanded)) {
2768
2862
  throw new Error("Host paths must be absolute (or start with '~').");
2769
2863
  }
2770
- const abs = resolve4(expanded);
2864
+ const abs = resolve5(expanded);
2771
2865
  if (seenPaths.has(abs)) continue;
2772
2866
  if (seenLabels.has(label)) {
2773
2867
  throw new Error(`Duplicate host label '${label}'; each host needs a distinct label.`);
@@ -2792,8 +2886,8 @@ import {
2792
2886
  // src/commands/import.ts
2793
2887
  import { createReadStream } from "fs";
2794
2888
  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";
2889
+ import { homedir as homedir7 } from "os";
2890
+ import { basename as basename4, dirname as dirname3, join as join9, resolve as resolve6 } from "path";
2797
2891
  import { createInterface } from "readline";
2798
2892
  import {
2799
2893
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
@@ -2864,10 +2958,10 @@ function resolveSourceRoots(args) {
2864
2958
  const { projectFlags, manifest, repoRoot, cwd } = args;
2865
2959
  let resolved;
2866
2960
  if (projectFlags.length > 0) {
2867
- resolved = projectFlags.map((p) => resolve5(cwd, p));
2961
+ resolved = projectFlags.map((p) => resolve6(cwd, p));
2868
2962
  } else {
2869
2963
  const roots = manifest.import?.source_roots;
2870
- resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve5(repoRoot, r)) : [repoRoot];
2964
+ resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve6(repoRoot, r)) : [repoRoot];
2871
2965
  }
2872
2966
  return [...new Set(resolved)];
2873
2967
  }
@@ -2880,7 +2974,7 @@ async function doRunImportClaudeCode(options, ctx) {
2880
2974
  repoRoot: repositoryRoot,
2881
2975
  cwd: ctx.cwd ?? process.cwd()
2882
2976
  });
2883
- const projectsRoot = ctx.claudeProjectsDir ?? join8(homedir6(), ".claude", "projects");
2977
+ const projectsRoot = ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects");
2884
2978
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
2885
2979
  const projectSet = new Set(projectPaths);
2886
2980
  const candidates = files.map((file) => {
@@ -2919,7 +3013,7 @@ async function doRunImportCodex(options, ctx) {
2919
3013
  repoRoot: repositoryRoot,
2920
3014
  cwd: ctx.cwd ?? process.cwd()
2921
3015
  });
2922
- const sessionsRoot = ctx.codexSessionsDir ?? join8(homedir6(), ".codex", "sessions");
3016
+ const sessionsRoot = ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions");
2923
3017
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
2924
3018
  const candidates = rollouts.map(({ file, externalId }) => ({
2925
3019
  externalId,
@@ -3050,7 +3144,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
3050
3144
  if (priors.length > 0 && options.force === true) {
3051
3145
  if (options.dryRun !== true) {
3052
3146
  for (const { sessionId } of priors) {
3053
- await rm(join8(paths.sessions, sessionId), { recursive: true, force: true });
3147
+ await rm(join9(paths.sessions, sessionId), { recursive: true, force: true });
3054
3148
  }
3055
3149
  }
3056
3150
  counts.replaced++;
@@ -3161,7 +3255,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3161
3255
  if (options.session !== void 0) {
3162
3256
  const matches = [];
3163
3257
  for (const projectPath of projectPaths) {
3164
- const file = join8(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
3258
+ const file = join9(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
3165
3259
  if (await pathExists(file)) matches.push(file);
3166
3260
  }
3167
3261
  if (matches.length === 0) {
@@ -3172,7 +3266,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3172
3266
  const files = [];
3173
3267
  let anyDirFound = false;
3174
3268
  for (const projectPath of projectPaths) {
3175
- const transcriptDir = join8(projectsRoot, encodeProjectDir(projectPath));
3269
+ const transcriptDir = join9(projectsRoot, encodeProjectDir(projectPath));
3176
3270
  let entries;
3177
3271
  try {
3178
3272
  entries = await readdir(transcriptDir);
@@ -3182,7 +3276,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3182
3276
  }
3183
3277
  anyDirFound = true;
3184
3278
  for (const name of entries) {
3185
- if (name.endsWith(".jsonl")) files.push(join8(transcriptDir, name));
3279
+ if (name.endsWith(".jsonl")) files.push(join9(transcriptDir, name));
3186
3280
  }
3187
3281
  }
3188
3282
  if (!anyDirFound) {
@@ -3239,7 +3333,7 @@ async function findRolloutFiles(sessionsRoot) {
3239
3333
  throw new Error("Failed to read Codex sessions directory", { cause: error });
3240
3334
  }
3241
3335
  for (const entry of entries) {
3242
- const full = join8(dir, entry.name);
3336
+ const full = join9(dir, entry.name);
3243
3337
  if (entry.isDirectory()) {
3244
3338
  await walk(full, false);
3245
3339
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -3696,6 +3790,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
3696
3790
 
3697
3791
  // src/commands/hook.ts
3698
3792
  var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
3793
+ var MAX_TRANSCRIPT_HEAD_BYTES = 256 * 1024;
3794
+ var TOKEN_SCAN_CHUNK_BYTES = 1024 * 1024;
3699
3795
  var execFileAsync = promisify(execFile);
3700
3796
  function registerHookCommand(program2) {
3701
3797
  const hook = program2.command("hook").description(
@@ -3707,13 +3803,13 @@ function registerHookCommand(program2) {
3707
3803
  await runHookSessionStart();
3708
3804
  });
3709
3805
  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."
3806
+ "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
3807
  ).option(
3712
3808
  "--min-edits <n>",
3713
3809
  `Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
3714
3810
  ).option(
3715
3811
  "--block",
3716
- "Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking reminder"
3812
+ "Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking message"
3717
3813
  ).option(
3718
3814
  "--require-review",
3719
3815
  "Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
@@ -3826,12 +3922,25 @@ gh pr create|merge) without recording a review ('basou review record'). This
3826
3922
  gate is off by default; when on, its reminder is composed into the same
3827
3923
  envelope as the capture reminder.
3828
3924
 
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.
3925
+ It also hands a RUNNING session the standing protocols when they changed after
3926
+ that session started. The protocol block in ~/.claude/CLAUDE.md is read at
3927
+ session start, so an update made mid-session never reaches the session it was
3928
+ meant to correct; this is the only channel that does. The complete current set
3929
+ is delivered, not a diff, so what it supersedes -- including a protocol that is
3930
+ no longer there -- is unambiguous. It reads the rendered block and nothing else,
3931
+ so an edit not yet published by 'basou protocol sync' cannot reach a session,
3932
+ and it delivers once per block state: a second update in the same session still
3933
+ lands, the same one twice does not. This part is always on, needs no flag, and
3934
+ says nothing at all unless the block actually changed.
3935
+
3936
+ By default every message here is non-blocking: Claude sees it and may act on it
3937
+ or stop. With --block (opt-in enforcement, 'basou hook install --block') it
3938
+ instead returns decision:block, holding the agent in-turn; the 'stop_hook_active'
3939
+ flag and Claude Code's own loop prevention bound it to a single turn. Note that
3940
+ this covers the protocol delivery too, which carries no action to take -- with
3941
+ --block the turn is held so the new text lands before more work is done on the
3942
+ old. Either way the hook fails open: a bad payload or unreadable transcript
3943
+ exits cleanly with no output.
3835
3944
  `;
3836
3945
  async function runHookStop(options, ctx = {}) {
3837
3946
  try {
@@ -3869,7 +3978,12 @@ async function doRunHookStop(options, ctx) {
3869
3978
  stopHookActive: false,
3870
3979
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
3871
3980
  });
3981
+ const protocolUpdate = await evaluateProtocolUpdateGate({
3982
+ transcriptPath,
3983
+ target: ctx.protocolTargetPath ?? DEFAULT_TARGET_PATH
3984
+ });
3872
3985
  const parts = [];
3986
+ if (protocolUpdate !== null) parts.push(protocolUpdate);
3873
3987
  if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
3874
3988
  if (options.requireReview === true && evaluation.review.fires) {
3875
3989
  parts.push(evaluation.review.additionalContext);
@@ -3885,6 +3999,56 @@ async function doRunHookStop(options, ctx) {
3885
3999
  write(`${payloadJson}
3886
4000
  `);
3887
4001
  }
4002
+ async function evaluateProtocolUpdateGate(input) {
4003
+ try {
4004
+ const head = await readTranscriptHead(input.transcriptPath);
4005
+ const sessionStartedAt = transcriptStartedAt(parseTranscript(head));
4006
+ if (sessionStartedAt === void 0) return null;
4007
+ const touchedAt = await targetModifiedAt(input.target);
4008
+ if (touchedAt !== null && touchedAt <= Date.parse(sessionStartedAt)) return null;
4009
+ const existing = await readMarkdownFile6(input.target);
4010
+ if (existing === null) return null;
4011
+ const section = parseMarkers2(existing, { start: PROTOCOL_START, end: PROTOCOL_END });
4012
+ if (section.kind !== "ok") return null;
4013
+ const stamp = parseProtocolStamp(section.generated);
4014
+ if (stamp === null) return null;
4015
+ if (!isProtocolUpdateDue({ stamp, sessionStartedAt })) return null;
4016
+ const sections = protocolSectionsFrom(section.generated);
4017
+ if (sections === null || sections.trim().length === 0) return null;
4018
+ if (await transcriptCarries(input.transcriptPath, protocolUpdateToken(stamp.contentHash))) {
4019
+ return null;
4020
+ }
4021
+ return renderProtocolUpdate(sections, stamp);
4022
+ } catch {
4023
+ return null;
4024
+ }
4025
+ }
4026
+ async function targetModifiedAt(target) {
4027
+ try {
4028
+ return (await stat4(target)).mtimeMs;
4029
+ } catch {
4030
+ return null;
4031
+ }
4032
+ }
4033
+ async function transcriptCarries(path, token) {
4034
+ const handle = await open2(path, "r");
4035
+ try {
4036
+ const overlap = Math.max(token.length - 1, 0);
4037
+ const chunk = Buffer.alloc(TOKEN_SCAN_CHUNK_BYTES);
4038
+ let carry = "";
4039
+ let position = 0;
4040
+ for (; ; ) {
4041
+ const { bytesRead } = await handle.read(chunk, 0, TOKEN_SCAN_CHUNK_BYTES, position);
4042
+ if (bytesRead === 0) return false;
4043
+ position += bytesRead;
4044
+ const text = carry + chunk.subarray(0, bytesRead).toString("utf8");
4045
+ if (text.includes(token)) return true;
4046
+ carry = overlap > 0 ? text.slice(-overlap) : "";
4047
+ }
4048
+ } finally {
4049
+ await handle.close();
4050
+ }
4051
+ }
3888
4052
  async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
3889
4053
  const root = await resolveBasouRootForCommand(cwd, "hook session-start", {
3890
4054
  portfolioConfigPath
@@ -3984,11 +4148,25 @@ async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
3984
4148
  await handle.close();
3985
4149
  }
3986
4150
  }
4151
+ async function readTranscriptHead(path, maxBytes = MAX_TRANSCRIPT_HEAD_BYTES) {
4152
+ const { size } = await stat4(path);
4153
+ if (size <= maxBytes) return readFile3(path, "utf8");
4154
+ const handle = await open2(path, "r");
4155
+ try {
4156
+ const buffer = Buffer.alloc(maxBytes);
4157
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
4158
+ const text = buffer.subarray(0, bytesRead).toString("utf8");
4159
+ const lastNewline = text.lastIndexOf("\n");
4160
+ return lastNewline >= 0 ? text.slice(0, lastNewline + 1) : text;
4161
+ } finally {
4162
+ await handle.close();
4163
+ }
4164
+ }
3987
4165
  function parseMinEdits(raw) {
3988
4166
  if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
3989
4167
  return Number(raw);
3990
4168
  }
3991
- var DEFAULT_CLAUDE_SETTINGS_PATH = join9(homedir7(), ".claude", "settings.json");
4169
+ var DEFAULT_CLAUDE_SETTINGS_PATH = join10(homedir8(), ".claude", "settings.json");
3992
4170
  function resolveCliEntry() {
3993
4171
  return fileURLToPath(import.meta.url);
3994
4172
  }
@@ -4236,7 +4414,7 @@ function describeHookMode(tiers) {
4236
4414
  const gates = tiers.review ? "capture + review" : "capture";
4237
4415
  return `${enforcement}, ${gates}`;
4238
4416
  }
4239
- var DEFAULT_CODEX_FACE_PATH = join9(homedir7(), ".codex", "AGENTS.md");
4417
+ var DEFAULT_CODEX_FACE_PATH = join10(homedir8(), ".codex", "AGENTS.md");
4240
4418
  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
4419
  async function faceHasLeftoverOrientationBlock(facePath) {
4242
4420
  try {
@@ -4248,8 +4426,8 @@ async function faceHasLeftoverOrientationBlock(facePath) {
4248
4426
  return false;
4249
4427
  }
4250
4428
  }
4251
- var DEFAULT_CODEX_HOOKS_PATH = join9(homedir7(), ".codex", "hooks.json");
4252
- var DEFAULT_CODEX_CONFIG_PATH = join9(homedir7(), ".codex", "config.toml");
4429
+ var DEFAULT_CODEX_HOOKS_PATH = join10(homedir8(), ".codex", "hooks.json");
4430
+ var DEFAULT_CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
4253
4431
  async function readHooksFile(path) {
4254
4432
  let raw;
4255
4433
  try {
@@ -4421,7 +4599,7 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
4421
4599
  }
4422
4600
 
4423
4601
  // src/commands/init.ts
4424
- import { basename as basename5, relative, resolve as resolve6 } from "path";
4602
+ import { basename as basename5, relative, resolve as resolve7 } from "path";
4425
4603
  import {
4426
4604
  appendBasouGitignore,
4427
4605
  createManifest,
@@ -4466,7 +4644,7 @@ async function doRunInit(options, ctx) {
4466
4644
  );
4467
4645
  }
4468
4646
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
4469
- const rel = relative(repositoryRoot, resolve6(cwd, p));
4647
+ const rel = relative(repositoryRoot, resolve7(cwd, p));
4470
4648
  return rel === "" ? "." : rel;
4471
4649
  });
4472
4650
  const paths = await ensureBasouDirectory(repositoryRoot);
@@ -4676,7 +4854,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
4676
4854
 
4677
4855
  // src/commands/portfolio.ts
4678
4856
  import { existsSync, statSync } from "fs";
4679
- import { join as join10 } from "path";
4857
+ import { join as join11 } from "path";
4680
4858
  function registerPortfolioCommand(program2) {
4681
4859
  program2.command("portfolio").description(
4682
4860
  "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 +4900,7 @@ async function runPortfolioList(options, ctx = {}) {
4722
4900
  async function doRunPortfolioList(options, ctx) {
4723
4901
  const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
4724
4902
  const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
4725
- const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join10(p, ".basou")));
4903
+ const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join11(p, ".basou")));
4726
4904
  const workspaces = await loadPortfolioConfig(configPath);
4727
4905
  const result = {
4728
4906
  configPath,
@@ -4787,7 +4965,7 @@ import {
4787
4965
  writeFileSync,
4788
4966
  writeSync
4789
4967
  } from "fs";
4790
- import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve7 } from "path";
4968
+ import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute4, join as join12, relative as relative2, resolve as resolve8 } from "path";
4791
4969
  import {
4792
4970
  appendBasouGitignore as appendBasouGitignore2,
4793
4971
  basouPaths as basouPaths11,
@@ -5162,14 +5340,14 @@ async function runProjectAdopt(options, ctx = {}) {
5162
5340
  }
5163
5341
  }
5164
5342
  function classifySourceRoot(repositoryRoot, declaredPath) {
5165
- const absolute = resolve7(repositoryRoot, declaredPath);
5343
+ const absolute = resolve8(repositoryRoot, declaredPath);
5166
5344
  let real;
5167
5345
  try {
5168
5346
  real = realpathSync(absolute);
5169
5347
  } catch {
5170
5348
  return { path: declaredPath, kind: "unresolved" };
5171
5349
  }
5172
- return { path: declaredPath, kind: existsSync2(join11(real, ".git")) ? "repo" : "non-repo" };
5350
+ return { path: declaredPath, kind: existsSync2(join12(real, ".git")) ? "repo" : "non-repo" };
5173
5351
  }
5174
5352
  async function doRunProjectAdopt(options, ctx) {
5175
5353
  const cwd = ctx.cwd ?? process.cwd();
@@ -5269,11 +5447,11 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5269
5447
  };
5270
5448
  let real;
5271
5449
  try {
5272
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5450
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5273
5451
  } catch {
5274
5452
  return { ...base, reachable: false, instructionFiles: [] };
5275
5453
  }
5276
- if (!existsSync2(join11(real, ".git"))) {
5454
+ if (!existsSync2(join12(real, ".git"))) {
5277
5455
  return { ...base, reachable: false, instructionFiles: [] };
5278
5456
  }
5279
5457
  try {
@@ -5281,7 +5459,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5281
5459
  for (const name of INSTRUCTION_FILES) {
5282
5460
  let present = true;
5283
5461
  try {
5284
- lstatSync(join11(real, name));
5462
+ lstatSync(join12(real, name));
5285
5463
  } catch {
5286
5464
  present = false;
5287
5465
  }
@@ -5388,14 +5566,14 @@ function gatherRepoGitignore(repositoryRoot, entry) {
5388
5566
  };
5389
5567
  let real;
5390
5568
  try {
5391
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5569
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5392
5570
  } catch {
5393
5571
  return { ...base, reachable: false, currentLines: [] };
5394
5572
  }
5395
- if (!existsSync2(join11(real, ".git"))) {
5573
+ if (!existsSync2(join12(real, ".git"))) {
5396
5574
  return { ...base, reachable: false, currentLines: [] };
5397
5575
  }
5398
- return { ...base, reachable: true, currentLines: readGitignoreLines(join11(real, ".gitignore")) };
5576
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join12(real, ".gitignore")) };
5399
5577
  }
5400
5578
  function hasErrorCode(error) {
5401
5579
  return error instanceof Error && typeof error.code === "string";
@@ -5409,7 +5587,7 @@ function readGitignoreLines(file) {
5409
5587
  }
5410
5588
  }
5411
5589
  function applyGitignorePlan(repositoryRoot, plan) {
5412
- const file = join11(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
5590
+ const file = join12(realpathSync(resolve8(repositoryRoot, plan.path)), ".gitignore");
5413
5591
  let existing = "";
5414
5592
  try {
5415
5593
  existing = readFileSync(file, "utf8");
@@ -5544,12 +5722,12 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5544
5722
  const base = { path: entry.path, ...isSelf ? { self: true } : {} };
5545
5723
  let real;
5546
5724
  try {
5547
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5725
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5548
5726
  } catch {
5549
5727
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
5550
5728
  }
5551
5729
  if (real === anchorReal) {
5552
- const anchorCanonical = join11(real, CANONICAL_FILE);
5730
+ const anchorCanonical = join12(real, CANONICAL_FILE);
5553
5731
  const anchorState = anchorCanonicalState(anchorCanonical);
5554
5732
  if (anchorState === "absent") {
5555
5733
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
@@ -5569,7 +5747,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5569
5747
  anchorCanonical,
5570
5748
  "self"
5571
5749
  ).map((spec) => {
5572
- const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
5750
+ const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
5573
5751
  return {
5574
5752
  name: spec.name,
5575
5753
  expectedTarget: spec.target,
@@ -5586,16 +5764,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5586
5764
  files: anchorFiles
5587
5765
  };
5588
5766
  }
5589
- if (!existsSync2(join11(real, ".git"))) {
5767
+ if (!existsSync2(join12(real, ".git"))) {
5590
5768
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
5591
5769
  }
5592
- const canonicalFile = isSelf ? join11(real, CANONICAL_FILE) : join11(anchorReal, "agents", basename6(real), CANONICAL_FILE);
5770
+ const canonicalFile = isSelf ? join12(real, CANONICAL_FILE) : join12(anchorReal, "agents", basename6(real), CANONICAL_FILE);
5593
5771
  if (!existsSync2(canonicalFile)) {
5594
5772
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
5595
5773
  }
5596
5774
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
5597
5775
  (spec) => {
5598
- const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
5776
+ const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
5599
5777
  return {
5600
5778
  name: spec.name,
5601
5779
  expectedTarget: spec.target,
@@ -5616,7 +5794,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5616
5794
  function applySymlinkPlan(repositoryRoot, plan) {
5617
5795
  let real;
5618
5796
  try {
5619
- real = realpathSync(resolve7(repositoryRoot, plan.path));
5797
+ real = realpathSync(resolve8(repositoryRoot, plan.path));
5620
5798
  } catch (error) {
5621
5799
  const message = failureReason(error);
5622
5800
  return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
@@ -5624,7 +5802,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
5624
5802
  const created = [];
5625
5803
  const failed = [];
5626
5804
  for (const { name, target } of plan.toCreate) {
5627
- const filePath = join11(real, name);
5805
+ const filePath = join12(real, name);
5628
5806
  try {
5629
5807
  mkdirSync(dirname4(filePath), { recursive: true });
5630
5808
  symlinkSync(target, filePath);
@@ -5642,9 +5820,9 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
5642
5820
  for (const entry of roster) {
5643
5821
  let name;
5644
5822
  try {
5645
- name = basename6(realpathSync(resolve7(repositoryRoot, entry.path)));
5823
+ name = basename6(realpathSync(resolve8(repositoryRoot, entry.path)));
5646
5824
  } catch {
5647
- name = basename6(resolve7(repositoryRoot, entry.path));
5825
+ name = basename6(resolve8(repositoryRoot, entry.path));
5648
5826
  }
5649
5827
  if (name === viewName) return entry.path;
5650
5828
  }
@@ -5658,7 +5836,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
5658
5836
  if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
5659
5837
  const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
5660
5838
  (spec) => {
5661
- const { state, actualTarget } = inspectSymlink(join11(viewDir, spec.name), spec.target);
5839
+ const { state, actualTarget } = inspectSymlink(join12(viewDir, spec.name), spec.target);
5662
5840
  return {
5663
5841
  name: spec.name,
5664
5842
  expectedTarget: spec.target,
@@ -5674,7 +5852,7 @@ function applyViewSymlinks(viewDir, files) {
5674
5852
  const failed = [];
5675
5853
  for (const f of files) {
5676
5854
  if (f.state !== "missing") continue;
5677
- const filePath = join11(viewDir, f.name);
5855
+ const filePath = join12(viewDir, f.name);
5678
5856
  try {
5679
5857
  mkdirSync(dirname4(filePath), { recursive: true });
5680
5858
  symlinkSync(f.expectedTarget, filePath);
@@ -5891,12 +6069,12 @@ async function runProjectWorkspace(options, ctx = {}) {
5891
6069
  }
5892
6070
  }
5893
6071
  function resolveViewDir(repositoryRoot, viewPath) {
5894
- const abs = resolve7(repositoryRoot, viewPath);
6072
+ const abs = resolve8(repositoryRoot, viewPath);
5895
6073
  try {
5896
6074
  return realpathSync(abs);
5897
6075
  } catch {
5898
6076
  try {
5899
- return join11(realpathSync(dirname4(abs)), basename6(abs));
6077
+ return join12(realpathSync(dirname4(abs)), basename6(abs));
5900
6078
  } catch {
5901
6079
  return abs;
5902
6080
  }
@@ -5905,7 +6083,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
5905
6083
  function gatherViewRepo(repositoryRoot, viewDir, entry) {
5906
6084
  let repoReal;
5907
6085
  try {
5908
- repoReal = realpathSync(resolve7(repositoryRoot, entry.path));
6086
+ repoReal = realpathSync(resolve8(repositoryRoot, entry.path));
5909
6087
  } catch {
5910
6088
  return { path: entry.path, reachable: false };
5911
6089
  }
@@ -5914,7 +6092,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
5914
6092
  return { path: entry.path, reachable: false };
5915
6093
  }
5916
6094
  const linkName = basename6(repoReal);
5917
- const { state, actualTarget } = inspectSymlink(join11(viewDir, linkName), expectedTarget);
6095
+ const { state, actualTarget } = inspectSymlink(join12(viewDir, linkName), expectedTarget);
5918
6096
  return {
5919
6097
  path: entry.path,
5920
6098
  reachable: true,
@@ -5928,7 +6106,7 @@ function applyViewPlan(viewDir, toCreate) {
5928
6106
  const created = [];
5929
6107
  const failed = [];
5930
6108
  for (const { name, target } of toCreate) {
5931
- const filePath = join11(viewDir, name);
6109
+ const filePath = join12(viewDir, name);
5932
6110
  try {
5933
6111
  mkdirSync(dirname4(filePath), { recursive: true });
5934
6112
  symlinkSync(target, filePath);
@@ -5943,7 +6121,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
5943
6121
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
5944
6122
  );
5945
6123
  function classifyViewLink(viewDir, name, rosterRealpaths) {
5946
- const filePath = join11(viewDir, name);
6124
+ const filePath = join12(viewDir, name);
5947
6125
  let isLink;
5948
6126
  try {
5949
6127
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -5957,12 +6135,12 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
5957
6135
  } catch {
5958
6136
  return null;
5959
6137
  }
5960
- const resolved = isAbsolute3(target) ? target : resolve7(viewDir, target);
6138
+ const resolved = isAbsolute4(target) ? target : resolve8(viewDir, target);
5961
6139
  try {
5962
6140
  if (rosterRealpaths.has(realpathSync(resolved))) return null;
5963
6141
  } catch {
5964
6142
  }
5965
- if (isAbsolute3(target)) return { target, kind: "absolute" };
6143
+ if (isAbsolute4(target)) return { target, kind: "absolute" };
5966
6144
  let isDir = false;
5967
6145
  try {
5968
6146
  isDir = statSync2(resolved).isDirectory();
@@ -5972,7 +6150,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
5972
6150
  if (!isDir) {
5973
6151
  return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
5974
6152
  }
5975
- return { target, kind: existsSync2(join11(resolved, ".git")) ? "repo" : "non-repo" };
6153
+ return { target, kind: existsSync2(join12(resolved, ".git")) ? "repo" : "non-repo" };
5976
6154
  }
5977
6155
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
5978
6156
  let names;
@@ -5997,7 +6175,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
5997
6175
  const pruned = [];
5998
6176
  const failed = [];
5999
6177
  for (const { name } of toPrune) {
6000
- const filePath = join11(viewDir, name);
6178
+ const filePath = join12(viewDir, name);
6001
6179
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
6002
6180
  if (c === null || c.kind !== "repo") {
6003
6181
  failed.push({
@@ -6043,11 +6221,11 @@ async function doRunProjectWorkspace(options, ctx) {
6043
6221
  } else {
6044
6222
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6045
6223
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
6046
- const rosterNames = roster.map((entry) => basename6(resolve7(repositoryRoot, entry.path)));
6224
+ const rosterNames = roster.map((entry) => basename6(resolve8(repositoryRoot, entry.path)));
6047
6225
  const rosterRealpaths = /* @__PURE__ */ new Set();
6048
6226
  for (const entry of roster) {
6049
6227
  try {
6050
- rosterRealpaths.add(realpathSync(resolve7(repositoryRoot, entry.path)));
6228
+ rosterRealpaths.add(realpathSync(resolve8(repositoryRoot, entry.path)));
6051
6229
  } catch {
6052
6230
  }
6053
6231
  }
@@ -6210,10 +6388,10 @@ async function runProjectPreset(options, ctx = {}) {
6210
6388
  }
6211
6389
  }
6212
6390
  function canonicalFileFor(anchorReal, canonicalName) {
6213
- return join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6391
+ return join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6214
6392
  }
6215
6393
  function canonicalLabelFor(canonicalName) {
6216
- return join11("agents", canonicalName, CANONICAL_FILE);
6394
+ return join12("agents", canonicalName, CANONICAL_FILE);
6217
6395
  }
6218
6396
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
6219
6397
  const declared = {
@@ -6227,14 +6405,14 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
6227
6405
  }
6228
6406
  let real;
6229
6407
  try {
6230
- real = realpathSync(resolve7(repositoryRoot, entry.path));
6408
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
6231
6409
  } catch {
6232
6410
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
6233
6411
  }
6234
6412
  if (real === anchorReal) {
6235
6413
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
6236
6414
  }
6237
- if (!existsSync2(join11(real, ".git"))) {
6415
+ if (!existsSync2(join12(real, ".git"))) {
6238
6416
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
6239
6417
  }
6240
6418
  const canonicalName = basename6(real);
@@ -6283,7 +6461,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
6283
6461
  anchorReal = void 0;
6284
6462
  }
6285
6463
  return roster.map((entry) => {
6286
- const abs = resolve7(repositoryRoot, entry.path);
6464
+ const abs = resolve8(repositoryRoot, entry.path);
6287
6465
  let real;
6288
6466
  try {
6289
6467
  real = realpathSync(abs);
@@ -6607,7 +6785,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6607
6785
  };
6608
6786
  let real;
6609
6787
  try {
6610
- real = realpathSync(resolve7(repositoryRoot, target));
6788
+ real = realpathSync(resolve8(repositoryRoot, target));
6611
6789
  } catch {
6612
6790
  return empty;
6613
6791
  }
@@ -6616,24 +6794,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6616
6794
  const instructionFiles = [];
6617
6795
  for (const name of INSTRUCTION_FILES) {
6618
6796
  try {
6619
- lstatSync(join11(real, name));
6797
+ lstatSync(join12(real, name));
6620
6798
  instructionFiles.push(name);
6621
6799
  } catch {
6622
6800
  }
6623
6801
  }
6624
6802
  let ignored;
6625
6803
  try {
6626
- ignored = new Set(readGitignoreLines(join11(real, ".gitignore")).map((l) => l.trim()));
6804
+ ignored = new Set(readGitignoreLines(join12(real, ".gitignore")).map((l) => l.trim()));
6627
6805
  } catch {
6628
6806
  ignored = /* @__PURE__ */ new Set();
6629
6807
  }
6630
6808
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
6631
- const canonical2 = existsSync2(join11(anchorReal, "agents", canonicalName, CANONICAL_FILE));
6809
+ const canonical2 = existsSync2(join12(anchorReal, "agents", canonicalName, CANONICAL_FILE));
6632
6810
  let viewLink = false;
6633
6811
  const viewPath = manifest.workspace.view;
6634
6812
  if (viewPath !== void 0) {
6635
6813
  try {
6636
- lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), canonicalName));
6814
+ lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), canonicalName));
6637
6815
  viewLink = true;
6638
6816
  } catch {
6639
6817
  }
@@ -6647,27 +6825,27 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6647
6825
  };
6648
6826
  }
6649
6827
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
6650
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6828
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6651
6829
  return expectedSymlinkTargets(repoReal, canonicalFile);
6652
6830
  }
6653
6831
  function viewLinkPointsAt(viewDir, name, repoReal) {
6654
- const filePath = join11(viewDir, name);
6832
+ const filePath = join12(viewDir, name);
6655
6833
  try {
6656
6834
  if (!lstatSync(filePath).isSymbolicLink()) return false;
6657
6835
  const target = readlinkSync(filePath);
6658
- if (isAbsolute3(target)) return false;
6659
- return realpathSync(resolve7(viewDir, target)) === repoReal;
6836
+ if (isAbsolute4(target)) return false;
6837
+ return realpathSync(resolve8(viewDir, target)) === repoReal;
6660
6838
  } catch {
6661
6839
  return false;
6662
6840
  }
6663
6841
  }
6664
6842
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
6665
- const filePath = join11(viewDir, name);
6843
+ const filePath = join12(viewDir, name);
6666
6844
  try {
6667
6845
  if (!lstatSync(filePath).isSymbolicLink()) return false;
6668
6846
  const target = readlinkSync(filePath);
6669
- if (isAbsolute3(target)) return false;
6670
- return resolve7(viewDir, target) === expectedRepoPath;
6847
+ if (isAbsolute4(target)) return false;
6848
+ return resolve8(viewDir, target) === expectedRepoPath;
6671
6849
  } catch {
6672
6850
  return false;
6673
6851
  }
@@ -6676,19 +6854,19 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6676
6854
  const anchorReal = realpathSync(repositoryRoot);
6677
6855
  let repoReal;
6678
6856
  try {
6679
- repoReal = realpathSync(resolve7(repositoryRoot, target));
6857
+ repoReal = realpathSync(resolve8(repositoryRoot, target));
6680
6858
  } catch {
6681
6859
  repoReal = void 0;
6682
6860
  }
6683
6861
  const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
6684
- const targetAbs = resolve7(repositoryRoot, target);
6862
+ const targetAbs = resolve8(repositoryRoot, target);
6685
6863
  const canonicalName = basename6(repoReal ?? targetAbs);
6686
6864
  const roster = manifest.repos ?? [];
6687
6865
  const declaredEntry = roster.find((r) => {
6688
6866
  try {
6689
- return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
6867
+ return realpathSync(resolve8(repositoryRoot, r.path)) === (repoReal ?? "\0");
6690
6868
  } catch {
6691
- return resolve7(repositoryRoot, r.path) === targetAbs;
6869
+ return resolve8(repositoryRoot, r.path) === targetAbs;
6692
6870
  }
6693
6871
  });
6694
6872
  const inRoster = declaredEntry !== void 0;
@@ -6697,7 +6875,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6697
6875
  const canonicalShared = roster.some((r) => {
6698
6876
  let rReal = null;
6699
6877
  try {
6700
- rReal = realpathSync(resolve7(repositoryRoot, r.path));
6878
+ rReal = realpathSync(resolve8(repositoryRoot, r.path));
6701
6879
  } catch {
6702
6880
  rReal = null;
6703
6881
  }
@@ -6705,15 +6883,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6705
6883
  if (repoReal !== void 0 && rReal === repoReal) return false;
6706
6884
  return basename6(rReal).toLowerCase() === cnFold;
6707
6885
  }
6708
- if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
6709
- return basename6(resolve7(repositoryRoot, r.path)).toLowerCase() === cnFold;
6886
+ if (resolve8(repositoryRoot, r.path) === targetAbs) return false;
6887
+ return basename6(resolve8(repositoryRoot, r.path)).toLowerCase() === cnFold;
6710
6888
  });
6711
6889
  const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
6712
6890
  const items = [];
6713
6891
  if (!isAnchor) {
6714
6892
  if (repoReal !== void 0) {
6715
6893
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
6716
- const { state, actualTarget } = inspectSymlink(join11(repoReal, spec.name), spec.target);
6894
+ const { state, actualTarget } = inspectSymlink(join12(repoReal, spec.name), spec.target);
6717
6895
  if (isSelf) {
6718
6896
  if (state !== "missing")
6719
6897
  items.push({
@@ -6750,7 +6928,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6750
6928
  }
6751
6929
  let ignored;
6752
6930
  try {
6753
- ignored = new Set(readGitignoreLines(join11(repoReal, ".gitignore")).map((l) => l.trim()));
6931
+ ignored = new Set(readGitignoreLines(join12(repoReal, ".gitignore")).map((l) => l.trim()));
6754
6932
  for (const p of INSTRUCTION_FILES) {
6755
6933
  if (ignored.has(p) || ignored.has(`/${p}`)) {
6756
6934
  items.push({
@@ -6773,7 +6951,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6773
6951
  const viewPath = manifest.workspace.view;
6774
6952
  if (viewPath !== void 0) {
6775
6953
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6776
- const linkPath = join11(viewDir, canonicalName);
6954
+ const linkPath = join12(viewDir, canonicalName);
6777
6955
  let isLink = false;
6778
6956
  try {
6779
6957
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -6799,8 +6977,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6799
6977
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
6800
6978
  }
6801
6979
  }
6802
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6803
- const canonicalLabel = join11("agents", canonicalName, CANONICAL_FILE);
6980
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6981
+ const canonicalLabel = join12("agents", canonicalName, CANONICAL_FILE);
6804
6982
  let canonicalIsLink = false;
6805
6983
  try {
6806
6984
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -6887,7 +7065,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6887
7065
  const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
6888
7066
  let currentRepoReal = null;
6889
7067
  try {
6890
- currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
7068
+ currentRepoReal = realpathSync(resolve8(repositoryRoot, plan.target));
6891
7069
  } catch {
6892
7070
  currentRepoReal = null;
6893
7071
  }
@@ -6904,12 +7082,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6904
7082
  );
6905
7083
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
6906
7084
  const expected = expectedByName.get(item.label);
6907
- if (repoReal === null || expected === void 0 || inspectSymlink(join11(repoReal, item.label), expected).state !== "correct") {
7085
+ if (repoReal === null || expected === void 0 || inspectSymlink(join12(repoReal, item.label), expected).state !== "correct") {
6908
7086
  changed(item.label);
6909
7087
  continue;
6910
7088
  }
6911
7089
  try {
6912
- unlinkSync(join11(repoReal, item.label));
7090
+ unlinkSync(join12(repoReal, item.label));
6913
7091
  removed.push(item.label);
6914
7092
  } catch (error) {
6915
7093
  failed.push({ label: item.label, message: failureReason(error) });
@@ -6922,13 +7100,13 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6922
7100
  continue;
6923
7101
  }
6924
7102
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6925
- const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve7(repositoryRoot, plan.target));
7103
+ const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve8(repositoryRoot, plan.target));
6926
7104
  if (!owned) {
6927
7105
  changed(item.label);
6928
7106
  continue;
6929
7107
  }
6930
7108
  try {
6931
- unlinkSync(join11(viewDir, item.label));
7109
+ unlinkSync(join12(viewDir, item.label));
6932
7110
  removed.push(`view/${item.label}`);
6933
7111
  } catch (error) {
6934
7112
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -6936,7 +7114,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6936
7114
  }
6937
7115
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
6938
7116
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
6939
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7117
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6940
7118
  try {
6941
7119
  if (lstatSync(canonicalFile).isSymbolicLink()) {
6942
7120
  changed(item.label);
@@ -7084,7 +7262,7 @@ async function doRunProjectArchive(target, options, ctx) {
7084
7262
  const roster = manifest.repos ?? [];
7085
7263
  let targetIsAnchor = false;
7086
7264
  try {
7087
- targetIsAnchor = realpathSync(resolve7(repositoryRoot, target)) === realpathSync(repositoryRoot);
7265
+ targetIsAnchor = realpathSync(resolve8(repositoryRoot, target)) === realpathSync(repositoryRoot);
7088
7266
  } catch {
7089
7267
  targetIsAnchor = false;
7090
7268
  }
@@ -7210,12 +7388,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
7210
7388
  } catch {
7211
7389
  return { canonicalDirOld: false, viewLinkOld: false };
7212
7390
  }
7213
- const canonicalDirOld = existsSync2(join11(anchorReal, "agents", oldBasename));
7391
+ const canonicalDirOld = existsSync2(join12(anchorReal, "agents", oldBasename));
7214
7392
  let viewLinkOld = false;
7215
7393
  const viewPath = manifest.workspace.view;
7216
7394
  if (viewPath !== void 0) {
7217
7395
  try {
7218
- lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), oldBasename));
7396
+ lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), oldBasename));
7219
7397
  viewLinkOld = true;
7220
7398
  } catch {
7221
7399
  }
@@ -7241,7 +7419,7 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
7241
7419
  const roster = manifest.repos ?? [];
7242
7420
  let oldIsAnchor = false;
7243
7421
  try {
7244
- oldIsAnchor = realpathSync(resolve7(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
7422
+ oldIsAnchor = realpathSync(resolve8(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
7245
7423
  } catch {
7246
7424
  oldIsAnchor = false;
7247
7425
  }
@@ -7387,7 +7565,7 @@ async function doRunProjectNew(repos, options, ctx) {
7387
7565
  const viewStem = productName ?? workspaceName;
7388
7566
  const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
7389
7567
  const declared = repos.map((p) => {
7390
- const abs = resolve7(cwd, p);
7568
+ const abs = resolve8(cwd, p);
7391
7569
  let real;
7392
7570
  try {
7393
7571
  real = realpathSync(abs);
@@ -7573,7 +7751,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
7573
7751
  console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
7574
7752
  return;
7575
7753
  }
7576
- const anchorDoc = join11(repositoryRoot, CANONICAL_FILE);
7754
+ const anchorDoc = join12(repositoryRoot, CANONICAL_FILE);
7577
7755
  if (pathPresent(anchorDoc)) {
7578
7756
  console.log(
7579
7757
  `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
@@ -7635,7 +7813,7 @@ function regularFileSpokes(repoReal) {
7635
7813
  const out = [];
7636
7814
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
7637
7815
  try {
7638
- const st = lstatSync(join11(repoReal, spoke));
7816
+ const st = lstatSync(join12(repoReal, spoke));
7639
7817
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
7640
7818
  } catch {
7641
7819
  }
@@ -7652,7 +7830,7 @@ function pathPresent(p) {
7652
7830
  }
7653
7831
  function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, viewCanonicalName) {
7654
7832
  const declaredEntry = roster.find((entry) => {
7655
- const entryAbs = resolve7(repositoryRoot, entry.path);
7833
+ const entryAbs = resolve8(repositoryRoot, entry.path);
7656
7834
  if (argReal !== void 0) {
7657
7835
  try {
7658
7836
  if (realpathSync(entryAbs) === argReal) return true;
@@ -7681,8 +7859,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
7681
7859
  };
7682
7860
  }
7683
7861
  const isAnchor = argReal === anchorReal;
7684
- const reachable = existsSync2(join11(argReal, ".git"));
7685
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7862
+ const reachable = existsSync2(join12(argReal, ".git"));
7863
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7686
7864
  return {
7687
7865
  path,
7688
7866
  declared,
@@ -7691,13 +7869,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
7691
7869
  reachable,
7692
7870
  canonicalName,
7693
7871
  ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
7694
- agentsState: inspectAgentsState(join11(argReal, CANONICAL_FILE)),
7872
+ agentsState: inspectAgentsState(join12(argReal, CANONICAL_FILE)),
7695
7873
  canonicalExists: pathPresent(canonicalFile),
7696
7874
  regularSpokes: regularFileSpokes(argReal)
7697
7875
  };
7698
7876
  }
7699
7877
  function relocateAgentsFile(repoReal, canonicalFile) {
7700
- const agentsFile = join11(repoReal, CANONICAL_FILE);
7878
+ const agentsFile = join12(repoReal, CANONICAL_FILE);
7701
7879
  try {
7702
7880
  mkdirSync(dirname4(canonicalFile), { recursive: true });
7703
7881
  } catch (error) {
@@ -7792,7 +7970,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7792
7970
  }
7793
7971
  return result2;
7794
7972
  }
7795
- const argAbs = resolve7(repositoryRoot, repo);
7973
+ const argAbs = resolve8(repositoryRoot, repo);
7796
7974
  let argReal;
7797
7975
  try {
7798
7976
  argReal = realpathSync(argAbs);
@@ -7813,7 +7991,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7813
7991
  let failure;
7814
7992
  let partial = false;
7815
7993
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
7816
- const canonicalFile = join11(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
7994
+ const canonicalFile = join12(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
7817
7995
  const res = relocateAgentsFile(argReal, canonicalFile);
7818
7996
  if (res.ok) {
7819
7997
  applied = true;
@@ -8013,88 +8191,19 @@ function renderProjectRetrofit(result) {
8013
8191
  }
8014
8192
 
8015
8193
  // 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 };
8194
+ import { readFile as readFile4, stat as stat5 } from "fs/promises";
8195
+ import {
8196
+ carryForwardProtocolStamp,
8197
+ PROTOCOL_END as PROTOCOL_END2,
8198
+ PROTOCOL_START as PROTOCOL_START2,
8199
+ parseMarkers as parseMarkers4,
8200
+ parseProtocolStamp as parseProtocolStamp2,
8201
+ protocolBlockHash,
8202
+ readMarkdownFile as readMarkdownFile8,
8203
+ renderProtocolStamp,
8204
+ unstampedProtocolSectionsFrom
8205
+ } from "@basou/core";
8206
+ var PROTOCOL_MARKERS = { start: PROTOCOL_START2, end: PROTOCOL_END2 };
8098
8207
  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
8208
  function registerProtocolCommand(program2) {
8100
8209
  const protocol = program2.command("protocol").description("Manage the basou-managed standing-protocol block in the global CLAUDE.md");
@@ -8154,24 +8263,54 @@ async function readProtocolSources(entries) {
8154
8263
  }
8155
8264
  return out;
8156
8265
  }
8157
- function buildBlock(sources) {
8158
- const sections = sources.map(({ entry, content }) => {
8266
+ function buildSections(sources) {
8267
+ return sources.map(({ entry, content }) => {
8159
8268
  const body = content.replace(/\s+$/, "");
8160
8269
  return entry.title !== void 0 ? `## ${entry.title}
8161
8270
 
8162
8271
  ${body}` : body;
8163
- });
8272
+ }).join("\n\n");
8273
+ }
8274
+ function buildBlock(sections, stamp) {
8164
8275
  return `${MANAGED_NOTE}
8276
+ ${renderProtocolStamp(stamp)}
8165
8277
 
8166
- ${sections.join("\n\n")}
8278
+ ${sections}
8167
8279
  `;
8168
8280
  }
8281
+ async function readPreviousStamp(target) {
8282
+ const existing = await readMarkdownFile8(target);
8283
+ if (existing === null) return null;
8284
+ const section = parseMarkers4(existing, PROTOCOL_MARKERS);
8285
+ if (section.kind !== "ok") return null;
8286
+ const stamp = parseProtocolStamp2(section.generated);
8287
+ if (stamp !== null) return stamp;
8288
+ const writtenAt = await lastWrittenAt(target);
8289
+ if (writtenAt === null) return null;
8290
+ return {
8291
+ changedAt: writtenAt,
8292
+ contentHash: protocolBlockHash(unstampedProtocolSectionsFrom(section.generated))
8293
+ };
8294
+ }
8295
+ async function lastWrittenAt(target) {
8296
+ try {
8297
+ return new Date((await stat5(target)).mtimeMs).toISOString();
8298
+ } catch {
8299
+ return null;
8300
+ }
8301
+ }
8169
8302
  async function doRunProtocolSync(options, ctx = {}) {
8170
8303
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
8171
8304
  const target = options.target ?? DEFAULT_TARGET_PATH;
8172
8305
  const entries = await loadProtocolsConfig(configPath);
8173
8306
  const sources = await readProtocolSources(entries);
8174
- const block = buildBlock(sources);
8307
+ const sections = buildSections(sources);
8308
+ const stamp = carryForwardProtocolStamp({
8309
+ sections,
8310
+ previous: await readPreviousStamp(target),
8311
+ now: (/* @__PURE__ */ new Date()).toISOString()
8312
+ });
8313
+ const block = buildBlock(sections, stamp);
8175
8314
  const foreign = await findForeignWorkspaceNames({
8176
8315
  text: block,
8177
8316
  configPath: ctx.portfolioConfigPath
@@ -8241,7 +8380,7 @@ import {
8241
8380
  import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
8242
8381
 
8243
8382
  // src/commands/refresh-watch.ts
8244
- import { readdir as readdir2, stat as stat5 } from "fs/promises";
8383
+ import { readdir as readdir2, stat as stat6 } from "fs/promises";
8245
8384
  import { homedir as homedir9 } from "os";
8246
8385
  import { join as join13 } from "path";
8247
8386
  import { findErrorCode as findErrorCode8 } from "@basou/core";
@@ -8270,7 +8409,7 @@ async function scanSourceLogs(roots) {
8270
8409
  await walk(full);
8271
8410
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
8272
8411
  try {
8273
- const info = await stat5(full);
8412
+ const info = await stat6(full);
8274
8413
  out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
8275
8414
  } catch (error) {
8276
8415
  if (findErrorCode8(error, "ENOENT")) continue;
@@ -11730,7 +11869,7 @@ import { InvalidArgumentError as InvalidArgumentError8 } from "commander";
11730
11869
 
11731
11870
  // src/lib/portfolio-coverage.ts
11732
11871
  import { createReadStream as createReadStream2 } from "fs";
11733
- import { readdir as readdir3, stat as stat6 } from "fs/promises";
11872
+ import { readdir as readdir3, stat as stat7 } from "fs/promises";
11734
11873
  import { homedir as homedir12 } from "os";
11735
11874
  import { basename as basename8, dirname as dirname5, join as join17 } from "path";
11736
11875
  import { createInterface as createInterface2 } from "readline";
@@ -11877,7 +12016,7 @@ async function isDirEntry(parent, entry) {
11877
12016
  if (entry.isDirectory()) return true;
11878
12017
  if (!entry.isSymbolicLink()) return false;
11879
12018
  try {
11880
- return (await stat6(join17(parent, entry.name))).isDirectory();
12019
+ return (await stat7(join17(parent, entry.name))).isDirectory();
11881
12020
  } catch {
11882
12021
  return false;
11883
12022
  }
@@ -12575,6 +12714,18 @@ var VIEW_HTML = `<!doctype html>
12575
12714
  return el('span', { class: 'badge ok', text: 'up to date' });
12576
12715
  }
12577
12716
 
12717
+ // Three states hide behind a bare 'in-flight 0': nothing ever recorded,
12718
+ // everything finished, and a task store that cannot be read -- where the
12719
+ // count is not zero but UNKNOWN. Say which one it is, as orient does.
12720
+ // Kept as a named top-level function with no free variables so the test suite
12721
+ // can lift it out of this template and run it; inlined in the card it was
12722
+ // unreachable from any test.
12723
+ function taskFlightLabel(w) {
12724
+ if (w.unreadableTaskCount > 0) return 'in-flight unknown (' + w.unreadableTaskCount + ' unreadable)';
12725
+ if (w.anyTaskEverRecorded === false) return 'no tasks recorded';
12726
+ return 'in-flight ' + w.inFlightCount;
12727
+ }
12728
+
12578
12729
  function portfolioCard(w, generatedAt) {
12579
12730
  if (!w.initialized) {
12580
12731
  return el('div', { class: 'card pcard muted' }, [
@@ -12590,6 +12741,7 @@ var VIEW_HTML = `<!doctype html>
12590
12741
  }
12591
12742
  var pend = w.pendingApprovals || [];
12592
12743
  var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
12744
+ var flightText = taskFlightLabel(w);
12593
12745
  var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
12594
12746
  var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
12595
12747
  var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
@@ -12602,7 +12754,7 @@ var VIEW_HTML = `<!doctype html>
12602
12754
  ]),
12603
12755
  el('div', { class: 'f', text: 'now: ' + now }),
12604
12756
  el('div', { class: 'f', text: 'latest: ' + dec }),
12605
- el('div', { class: 'f', text: 'in-flight ' + w.inFlightCount + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
12757
+ el('div', { class: 'f', text: flightText + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
12606
12758
  el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
12607
12759
  ]);
12608
12760
  }
@@ -13179,6 +13331,13 @@ async function portfolioCard(ws, nowIso) {
13179
13331
  sessionCount: s.sessionCount,
13180
13332
  suspectCount: s.suspects.length,
13181
13333
  inFlightCount: s.inFlightTasks.length,
13334
+ // Three states hide behind `inFlightCount: 0`: nothing ever recorded,
13335
+ // everything finished, and a store whose task files cannot be read (where
13336
+ // the count is not 0 but UNKNOWN). `orient` branches all three ways; the
13337
+ // card carries the two extra facts so it can too, instead of reporting a
13338
+ // loader blind spot as an empty record.
13339
+ anyTaskEverRecorded: s.anyTaskEverRecorded,
13340
+ unreadableTaskCount: s.unreadableTaskCount,
13182
13341
  pendingApprovals: s.pendingApprovals.map((a) => ({
13183
13342
  risk: a.risk,
13184
13343
  kind: a.kind,
@@ -13646,7 +13805,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
13646
13805
  function readBuildStamp() {
13647
13806
  if (false) return void 0;
13648
13807
  try {
13649
- return JSON.parse('{"version":"0.45.0","commit":"6b04208","committedAt":"2026-09-19T01:07:28+09:00"}');
13808
+ return JSON.parse('{"version":"0.47.0","commit":"831e0cb","committedAt":"2026-09-20T22:28:13+09:00"}');
13650
13809
  } catch {
13651
13810
  return void 0;
13652
13811
  }