@basou/cli 0.44.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/index.js CHANGED
@@ -119,6 +119,7 @@ function printTaskSkip(taskId, reason) {
119
119
 
120
120
  // src/program.ts
121
121
  import { createRequire } from "module";
122
+ import * as basouCore from "@basou/core";
122
123
  import { Command } from "commander";
123
124
 
124
125
  // src/commands/approval.ts
@@ -135,6 +136,7 @@ import {
135
136
  enumerateApprovals,
136
137
  findErrorCode,
137
138
  isLazyExpired,
139
+ LOCAL_CLI_EVENT_SOURCE,
138
140
  linkYamlFile,
139
141
  loadApproval,
140
142
  prefixedUlid,
@@ -369,7 +371,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
369
371
  id: eventId,
370
372
  session_id: approval.session_id,
371
373
  occurred_at: occurredAt,
372
- source: "local-cli",
374
+ source: LOCAL_CLI_EVENT_SOURCE,
373
375
  type: "approval_approved",
374
376
  approval_id: approval.id,
375
377
  resolver: "local-cli",
@@ -382,7 +384,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
382
384
  id: eventId,
383
385
  session_id: approval.session_id,
384
386
  occurred_at: occurredAt,
385
- source: "local-cli",
387
+ source: LOCAL_CLI_EVENT_SOURCE,
386
388
  type: "approval_rejected",
387
389
  approval_id: approval.id,
388
390
  resolver: "local-cli",
@@ -861,12 +863,13 @@ import {
861
863
  acquireLock as acquireLock2,
862
864
  appendEventToExistingSession,
863
865
  assertBasouRootSafe as assertBasouRootSafe2,
864
- basouPaths as basouPaths3,
866
+ basouPaths as basouPaths4,
865
867
  classifyFilesBySourceRoot,
866
868
  createAdHocSessionWithEvent,
867
869
  EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION2,
868
870
  findErrorCode as findErrorCode2,
869
871
  isValidPrefixedId,
872
+ LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE2,
870
873
  loadSessionEntries,
871
874
  prefixedUlid as prefixedUlid2,
872
875
  readManifest as readManifest2,
@@ -875,7 +878,7 @@ import {
875
878
  resolveSessionId,
876
879
  sanitizePath
877
880
  } from "@basou/core";
878
- import { InvalidArgumentError } from "commander";
881
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
879
882
 
880
883
  // src/lib/repo-root.ts
881
884
  import { realpath, stat as stat2 } from "fs/promises";
@@ -1035,11 +1038,165 @@ async function realpathOrNull(p) {
1035
1038
  }
1036
1039
  }
1037
1040
 
1041
+ // src/commands/decision-gaps.ts
1042
+ import {
1043
+ basouPaths as basouPaths3,
1044
+ findDecisionGaps
1045
+ } from "@basou/core";
1046
+ import { InvalidArgumentError } from "commander";
1047
+ var DEFAULT_GAP_LIMIT = 20;
1048
+ function parseLimit(value) {
1049
+ const n = /^\d+$/u.test(value.trim()) ? Number(value.trim()) : Number.NaN;
1050
+ if (!Number.isInteger(n) || n < 0) {
1051
+ throw new InvalidArgumentError("--limit must be a non-negative integer.");
1052
+ }
1053
+ return n;
1054
+ }
1055
+ function parseSince(value, now = /* @__PURE__ */ new Date()) {
1056
+ const duration = /^(\d+)([dhm])$/u.exec(value.trim());
1057
+ if (duration !== null) {
1058
+ const n = Number(duration[1]);
1059
+ const ms = { d: 864e5, h: 36e5, m: 6e4 }[duration[2]];
1060
+ return new Date(now.getTime() - n * ms).toISOString();
1061
+ }
1062
+ if (/^\d+$/u.test(value.trim())) {
1063
+ throw new InvalidArgumentError("--since needs a unit (7d, 36h, 90m) or a full ISO timestamp.");
1064
+ }
1065
+ const parsed = Date.parse(value);
1066
+ if (Number.isNaN(parsed)) {
1067
+ throw new InvalidArgumentError(
1068
+ "--since must be an ISO timestamp or a duration (7d, 36h, 90m)."
1069
+ );
1070
+ }
1071
+ return new Date(parsed).toISOString();
1072
+ }
1073
+ function registerDecisionGapsCommand(decision) {
1074
+ decision.command("gaps").description("List recorded decisions that no task carries (read-only, advisory)").option(
1075
+ "--since <when>",
1076
+ "Only decisions recorded at or after this ISO instant or duration back (7d, 36h)"
1077
+ ).option(
1078
+ "--limit <n>",
1079
+ `Maximum entries to list; the rest are counted (default ${DEFAULT_GAP_LIMIT}, 0 = no limit)`,
1080
+ parseLimit
1081
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
1082
+ await runDecisionGaps(opts);
1083
+ });
1084
+ }
1085
+ async function runDecisionGaps(options, ctx = {}) {
1086
+ try {
1087
+ await doRunDecisionGaps(options, ctx);
1088
+ } catch (error) {
1089
+ renderCliError(error, { verbose: isVerbose(options) });
1090
+ process.exitCode = 1;
1091
+ }
1092
+ }
1093
+ async function doRunDecisionGaps(options, ctx) {
1094
+ const cwd = ctx.cwd ?? process.cwd();
1095
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision gaps");
1096
+ const paths = basouPaths3(repositoryRoot);
1097
+ const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
1098
+ const limit = options.limit ?? DEFAULT_GAP_LIMIT;
1099
+ const summary = await findDecisionGaps({
1100
+ paths,
1101
+ nowIso: now.toISOString(),
1102
+ ...options.since !== void 0 ? { start: parseSince(options.since, now) } : {},
1103
+ ...limit > 0 ? { limit } : {},
1104
+ onWarning: (w, sid) => printReplayWarning(w, sid),
1105
+ onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
1106
+ });
1107
+ if (options.json === true) {
1108
+ console.log(JSON.stringify(summary));
1109
+ } else {
1110
+ console.log(renderDecisionGaps(summary));
1111
+ }
1112
+ return summary;
1113
+ }
1114
+ function relAge(iso, now) {
1115
+ const ms = now.getTime() - Date.parse(iso);
1116
+ if (!Number.isFinite(ms)) return "(unknown)";
1117
+ if (ms < 0) return "clock ahead";
1118
+ const days = Math.floor(ms / 864e5);
1119
+ if (days >= 1) return `${days}d ago`;
1120
+ const hours = Math.floor(ms / 36e5);
1121
+ if (hours >= 1) return `${hours}h ago`;
1122
+ return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
1123
+ }
1124
+ var GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1125
+ function oneLine(value, max) {
1126
+ const flat = [...GRAPHEMES.segment(value.replace(/\s+/gu, " ").trim())].map((s) => s.segment);
1127
+ return flat.length > max ? `${flat.slice(0, max - 1).join("")}\u2026` : flat.join("");
1128
+ }
1129
+ function gapLine(d, now) {
1130
+ return `- ${oneLine(d.title, 100)}
1131
+ ${relAge(d.recordedAt, now)} \xB7 ${d.decisionId}`;
1132
+ }
1133
+ function incompleteLine(summary) {
1134
+ const { sessions, tasks, unknownReferences } = summary.incomplete;
1135
+ const parts = [];
1136
+ if (sessions > 0) {
1137
+ parts.push(
1138
+ `${sessions} session${sessions === 1 ? "" : "s"} could not be read in full, so a decision recorded there may be missing from these numbers, and a \`decision void\` recorded there may not have been applied`
1139
+ );
1140
+ }
1141
+ if (tasks > 0) {
1142
+ parts.push(
1143
+ `${tasks} task file${tasks === 1 ? "" : "s"} could not be read, so a decision one of them carries is listed above as if nothing did`
1144
+ );
1145
+ }
1146
+ if (unknownReferences > 0) {
1147
+ parts.push(
1148
+ `${unknownReferences} decision id${unknownReferences === 1 ? "" : "s"} named by a task ${unknownReferences === 1 ? "matches" : "match"} no decision in this store, and ${unknownReferences === 1 ? "was" : "were"} not counted as carrying anything`
1149
+ );
1150
+ }
1151
+ return parts.length === 0 ? null : `\u26A0\uFE0F ${parts.join("; ")}.`;
1152
+ }
1153
+ function renderDecisionGaps(summary) {
1154
+ const now = new Date(summary.generatedAt);
1155
+ const { excluded, scope } = summary;
1156
+ const lines = ["# Decision gaps", ""];
1157
+ if (summary.populationCount === 0) {
1158
+ lines.push(
1159
+ `No decision recorded since ${scope.start} by \`basou decision capture\` or \`basou decision record\` is still open, so there is nothing to check yet. This list fills as decisions are recorded.`
1160
+ );
1161
+ } else if (summary.gaps.length === 0) {
1162
+ lines.push(
1163
+ `\u2705 Within what was checked, each of the ${summary.populationCount} open decision${summary.populationCount === 1 ? "" : "s"} in scope has a task carrying it.`
1164
+ );
1165
+ } else {
1166
+ const total = summary.gaps.length + summary.truncated;
1167
+ lines.push(`\u26A0\uFE0F Open decisions no task carries: ${total} of ${summary.populationCount} in scope`);
1168
+ lines.push("");
1169
+ for (const d of summary.gaps) lines.push(gapLine(d, now));
1170
+ if (summary.truncated > 0) {
1171
+ lines.push(` ... +${summary.truncated} more (--limit 0 to list them all, or --json)`);
1172
+ }
1173
+ }
1174
+ lines.push("");
1175
+ lines.push("## Scope");
1176
+ lines.push(
1177
+ `- Checked: decisions recorded at or after ${scope.start} with source \`${scope.source}\` (i.e. recorded by running basou, not derived from a transcript by an importer), still open, and not a track.`
1178
+ );
1179
+ lines.push(
1180
+ `- Not checked: ${excluded.byStart} recorded earlier, then ${excluded.bySource} recorded by something other than basou itself, then ${excluded.track} tracks (already shown every session by \`basou orient\` until closed), then ${excluded.voided} closed with \`basou decision void\`.`
1181
+ );
1182
+ lines.push(
1183
+ `- Task files read: ${summary.tasksScanned} (live and archived); ${summary.carried} of the decisions in scope are carried by one.`
1184
+ );
1185
+ const incomplete = incompleteLine(summary);
1186
+ if (incomplete !== null) lines.push(`- ${incomplete}`);
1187
+ lines.push("");
1188
+ lines.push(
1189
+ "Note: read-only advisory. A task carries a decision when it names that decision's full id; an abbreviated id names no one decision, because a captured batch shares a millisecond and its ids differ only at the end. It does not read what a decision means, and it does not enforce."
1190
+ );
1191
+ return lines.join("\n");
1192
+ }
1193
+
1038
1194
  // src/commands/decision.ts
1039
1195
  var LABEL_TITLE_MAX = 80;
1040
1196
  var LABEL_TRUNCATE_HEAD = LABEL_TITLE_MAX - 3;
1041
1197
  function registerDecisionCommand(program2) {
1042
1198
  const decision = program2.command("decision").description("Record human-authored decisions as events");
1199
+ registerDecisionGapsCommand(decision);
1043
1200
  decision.command("record").description("Record a decision_recorded event").requiredOption("--title <text>", "Decision title", parseTitle).option("--rationale <text>", "Rationale for the decision", parseRationale).option(
1044
1201
  "--rejected-reason <text>",
1045
1202
  "Reason rejected alternatives were not chosen",
@@ -1161,7 +1318,7 @@ function warnTrackMarkerWithoutKind(decisions, markerWithoutKind) {
1161
1318
  async function doRunDecisionRecord(options, ctx) {
1162
1319
  const cwd = ctx.cwd ?? process.cwd();
1163
1320
  const repositoryRoot = await resolveRepositoryRootForDecision(cwd);
1164
- const paths = basouPaths3(repositoryRoot);
1321
+ const paths = basouPaths4(repositoryRoot);
1165
1322
  await assertWorkspaceInitialized2(paths.root);
1166
1323
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
1167
1324
  const occurredAt = now.toISOString();
@@ -1252,7 +1409,7 @@ async function runDecisionCapture(options, ctx = {}) {
1252
1409
  async function doRunDecisionCapture(options, ctx) {
1253
1410
  const cwd = ctx.cwd ?? process.cwd();
1254
1411
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision capture");
1255
- const paths = basouPaths3(repositoryRoot);
1412
+ const paths = basouPaths4(repositoryRoot);
1256
1413
  await assertWorkspaceInitialized2(paths.root);
1257
1414
  const raw = await readCaptureInput(options, ctx);
1258
1415
  const { decisions, markerWithoutKind } = parseCaptureInput(raw);
@@ -1334,7 +1491,7 @@ async function doRunDecisionVoid(decisionId, options, ctx) {
1334
1491
  }
1335
1492
  const cwd = ctx.cwd ?? process.cwd();
1336
1493
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision void");
1337
- const paths = basouPaths3(repositoryRoot);
1494
+ const paths = basouPaths4(repositoryRoot);
1338
1495
  await assertWorkspaceInitialized2(paths.root);
1339
1496
  if (!await decisionExists(paths, decisionId)) {
1340
1497
  throw new Error(
@@ -1428,7 +1585,7 @@ function buildDecisionVoidedEvent(input) {
1428
1585
  id: input.eventId,
1429
1586
  session_id: input.sessionId,
1430
1587
  occurred_at: input.occurredAt,
1431
- source: "local-cli",
1588
+ source: LOCAL_CLI_EVENT_SOURCE2,
1432
1589
  type: "decision_voided",
1433
1590
  decision_id: input.decisionId,
1434
1591
  ...input.reason !== void 0 ? { reason: input.reason } : {},
@@ -1460,7 +1617,7 @@ function printVoidResult(options, result) {
1460
1617
  }
1461
1618
  function parseReason(raw) {
1462
1619
  if (raw.trim().length === 0) {
1463
- throw new InvalidArgumentError("--reason must not be empty");
1620
+ throw new InvalidArgumentError2("--reason must not be empty");
1464
1621
  }
1465
1622
  return raw;
1466
1623
  }
@@ -1694,7 +1851,7 @@ function buildDecisionEvent(input) {
1694
1851
  id: input.eventId,
1695
1852
  session_id: input.sessionId,
1696
1853
  occurred_at: input.occurredAt,
1697
- source: "local-cli",
1854
+ source: LOCAL_CLI_EVENT_SOURCE2,
1698
1855
  type: "decision_recorded",
1699
1856
  decision_id: input.decisionId,
1700
1857
  title: input.title,
@@ -1712,25 +1869,25 @@ function buildAdHocLabel(title) {
1712
1869
  }
1713
1870
  function parseTitle(raw) {
1714
1871
  if (isBlank(raw)) {
1715
- throw new InvalidArgumentError("Title must not be empty");
1872
+ throw new InvalidArgumentError2("Title must not be empty");
1716
1873
  }
1717
1874
  return raw;
1718
1875
  }
1719
1876
  function parseRationale(raw) {
1720
1877
  if (isBlank(raw)) {
1721
- throw new InvalidArgumentError("Rationale must not be empty");
1878
+ throw new InvalidArgumentError2("Rationale must not be empty");
1722
1879
  }
1723
1880
  return raw;
1724
1881
  }
1725
1882
  function parseRejectedReason(raw) {
1726
1883
  if (isBlank(raw)) {
1727
- throw new InvalidArgumentError("Rejected reason must not be empty");
1884
+ throw new InvalidArgumentError2("Rejected reason must not be empty");
1728
1885
  }
1729
1886
  return raw;
1730
1887
  }
1731
1888
  function collectAlternative(value, prev) {
1732
1889
  if (isBlank(value)) {
1733
- throw new InvalidArgumentError("Alternative must not be empty");
1890
+ throw new InvalidArgumentError2("Alternative must not be empty");
1734
1891
  }
1735
1892
  return prev.concat(value);
1736
1893
  }
@@ -1739,16 +1896,16 @@ function isValidEventId(value) {
1739
1896
  }
1740
1897
  function collectLinkedEvent(value, prev) {
1741
1898
  if (!isValidEventId(value)) {
1742
- throw new InvalidArgumentError(`Linked event id must match evt_<ULID>, got '${value}'`);
1899
+ throw new InvalidArgumentError2(`Linked event id must match evt_<ULID>, got '${value}'`);
1743
1900
  }
1744
1901
  return prev.concat(value);
1745
1902
  }
1746
1903
  function collectLinkedFile(value, prev) {
1747
1904
  if (isBlank(value)) {
1748
- throw new InvalidArgumentError("Linked file path must not be empty");
1905
+ throw new InvalidArgumentError2("Linked file path must not be empty");
1749
1906
  }
1750
1907
  if (value.length > 4096) {
1751
- throw new InvalidArgumentError("Linked file path exceeds 4096 chars");
1908
+ throw new InvalidArgumentError2("Linked file path exceeds 4096 chars");
1752
1909
  }
1753
1910
  return prev.concat(value);
1754
1911
  }
@@ -1813,7 +1970,7 @@ async function assertWorkspaceInitialized2(basouRoot) {
1813
1970
  // src/commands/decisions.ts
1814
1971
  import {
1815
1972
  assertBasouRootSafe as assertBasouRootSafe3,
1816
- basouPaths as basouPaths4,
1973
+ basouPaths as basouPaths5,
1817
1974
  findErrorCode as findErrorCode3,
1818
1975
  readMarkdownFile as readMarkdownFile2,
1819
1976
  renderDecisions,
@@ -1839,7 +1996,7 @@ async function doRunDecisionsGenerate(options, ctx) {
1839
1996
  void options;
1840
1997
  const cwd = ctx.cwd ?? process.cwd();
1841
1998
  const repositoryRoot = await resolveRepositoryRootForDecisions(cwd);
1842
- const paths = basouPaths4(repositoryRoot);
1999
+ const paths = basouPaths5(repositoryRoot);
1843
2000
  await assertWorkspaceInitialized3(paths.root);
1844
2001
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
1845
2002
  const result = await renderDecisions({
@@ -1884,7 +2041,7 @@ import { join as join6 } from "path";
1884
2041
  import {
1885
2042
  acquireLock as acquireLock3,
1886
2043
  assertBasouRootSafe as assertBasouRootSafe4,
1887
- basouPaths as basouPaths5,
2044
+ basouPaths as basouPaths6,
1888
2045
  ChildProcessRunner,
1889
2046
  appendChainedEvent as coreAppendChainedEvent,
1890
2047
  EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION3,
@@ -1896,6 +2053,7 @@ import {
1896
2053
  readManifest as readManifest3,
1897
2054
  readYamlFile as readYamlFile3,
1898
2055
  resolveRepositoryRoot as resolveRepositoryRoot4,
2056
+ SESSION_SCHEMA_VERSION,
1899
2057
  SessionSchema,
1900
2058
  sanitizeWorkingDirectory,
1901
2059
  writeObservedDuration,
@@ -1918,7 +2076,7 @@ async function runExec(command, args, options, ctx = {}) {
1918
2076
  const cwd = options.cwd ?? process.cwd();
1919
2077
  const timeout_ms = options.timeout !== void 0 ? parseDuration(options.timeout) : void 0;
1920
2078
  const repoRoot = await resolveRepositoryRootForExec(cwd);
1921
- const paths = basouPaths5(repoRoot);
2079
+ const paths = basouPaths6(repoRoot);
1922
2080
  await assertBasouRootSafe4(paths.root);
1923
2081
  const manifest = await readManifest3(paths);
1924
2082
  const sessionId = prefixedUlid3("ses");
@@ -2131,7 +2289,7 @@ function normalizeGitSnapshotSkipMessage(error) {
2131
2289
  function buildInitialSession(input) {
2132
2290
  const cmdline = [input.command, ...input.args].join(" ");
2133
2291
  return {
2134
- schema_version: "0.1.0",
2292
+ schema_version: SESSION_SCHEMA_VERSION,
2135
2293
  session: {
2136
2294
  id: input.id,
2137
2295
  label: `basou exec ${cmdline} (${input.startedAt})`,
@@ -2216,12 +2374,11 @@ async function resolveRepositoryRootForExec(cwd) {
2216
2374
  // src/commands/handoff.ts
2217
2375
  import {
2218
2376
  assertBasouRootSafe as assertBasouRootSafe5,
2219
- basouPaths as basouPaths6,
2377
+ basouPaths as basouPaths7,
2220
2378
  findErrorCode as findErrorCode4,
2221
2379
  readMarkdownFile as readMarkdownFile3,
2222
2380
  renderHandoff,
2223
2381
  renderWithMarkers as renderWithMarkers2,
2224
- resolveRepositoryRoot as resolveRepositoryRoot5,
2225
2382
  writeMarkdownFile as writeMarkdownFile2
2226
2383
  } from "@basou/core";
2227
2384
  function registerHandoffCommand(program2) {
@@ -2242,7 +2399,7 @@ async function doRunHandoffGenerate(options, ctx) {
2242
2399
  void options;
2243
2400
  const cwd = ctx.cwd ?? process.cwd();
2244
2401
  const repositoryRoot = await resolveRepositoryRootForHandoff(cwd);
2245
- const paths = basouPaths6(repositoryRoot);
2402
+ const paths = basouPaths7(repositoryRoot);
2246
2403
  await assertWorkspaceInitialized4(paths.root);
2247
2404
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
2248
2405
  const result = await renderHandoff({
@@ -2261,7 +2418,7 @@ async function doRunHandoffGenerate(options, ctx) {
2261
2418
  }
2262
2419
  async function resolveRepositoryRootForHandoff(cwd) {
2263
2420
  try {
2264
- return await resolveRepositoryRoot5(cwd);
2421
+ return await resolveBasouRootForCommand(cwd, "handoff generate");
2265
2422
  } catch (error) {
2266
2423
  if (error instanceof Error && error.message === "Not a git repository") {
2267
2424
  throw new Error(
@@ -2284,10 +2441,12 @@ async function assertWorkspaceInitialized4(basouRoot) {
2284
2441
  }
2285
2442
 
2286
2443
  // src/commands/hook.ts
2444
+ import { execFile } from "child_process";
2287
2445
  import { open as open2, readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
2288
- import { homedir as homedir7 } from "os";
2289
- import { join as join9 } from "path";
2446
+ import { homedir as homedir8 } from "os";
2447
+ import { join as join10 } from "path";
2290
2448
  import { fileURLToPath } from "url";
2449
+ import { promisify } from "util";
2291
2450
  import {
2292
2451
  buildSessionStartHookCommand,
2293
2452
  buildStopHookCommand,
@@ -2295,12 +2454,20 @@ import {
2295
2454
  evaluateStopHook,
2296
2455
  findBasouSessionStartHook,
2297
2456
  findBasouStopHookCommand,
2457
+ isProtocolUpdateDue,
2298
2458
  ORIENTATION_END as ORIENTATION_END2,
2299
2459
  ORIENTATION_START as ORIENTATION_START2,
2460
+ PROTOCOL_END,
2461
+ PROTOCOL_START,
2300
2462
  parseMarkers as parseMarkers2,
2463
+ parseProtocolStamp,
2464
+ protocolSectionsFrom,
2465
+ protocolUpdateToken,
2301
2466
  readMarkdownFile as readMarkdownFile6,
2302
2467
  removeSessionStartHook,
2303
2468
  removeStopHook,
2469
+ renderProtocolUpdate,
2470
+ transcriptStartedAt,
2304
2471
  upsertSessionStartHook,
2305
2472
  upsertStopHook
2306
2473
  } from "@basou/core";
@@ -2555,32 +2722,109 @@ async function warnIfPositionNamesOtherWorkspaces(args) {
2555
2722
  }
2556
2723
  }
2557
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
+
2558
2802
  // src/commands/orient.ts
2559
2803
  import {
2560
2804
  assertBasouRootSafe as assertBasouRootSafe7,
2561
- basouPaths as basouPaths8,
2805
+ basouPaths as basouPaths9,
2562
2806
  findErrorCode as findErrorCode6,
2563
2807
  renderOrientation as renderOrientation2,
2564
2808
  writeMarkdownFile as writeMarkdownFile4
2565
2809
  } from "@basou/core";
2566
2810
 
2567
2811
  // src/lib/hosts-config.ts
2568
- import { homedir as homedir5 } from "os";
2569
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
2570
- import { readYamlFile as readYamlFile4 } from "@basou/core";
2571
- var DEFAULT_HOSTS_CONFIG_PATH = join7(homedir5(), ".basou", "hosts.yaml");
2572
- function expandTilde2(p) {
2573
- if (p === "~") return homedir5();
2574
- 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));
2575
2819
  return p;
2576
2820
  }
2577
- function isRecord2(value) {
2821
+ function isRecord3(value) {
2578
2822
  return typeof value === "object" && value !== null && !Array.isArray(value);
2579
2823
  }
2580
2824
  async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
2581
2825
  let raw;
2582
2826
  try {
2583
- raw = await readYamlFile4(configPath);
2827
+ raw = await readYamlFile5(configPath);
2584
2828
  } catch (error) {
2585
2829
  if (error instanceof Error && error.message === "YAML file not found") {
2586
2830
  return null;
@@ -2590,25 +2834,25 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
2590
2834
  }
2591
2835
  throw error;
2592
2836
  }
2593
- if (!isRecord2(raw) || !Array.isArray(raw.hosts)) {
2837
+ if (!isRecord3(raw) || !Array.isArray(raw.hosts)) {
2594
2838
  throw new Error("~/.basou/hosts.yaml must contain a 'hosts:' list.");
2595
2839
  }
2596
2840
  const seenPaths = /* @__PURE__ */ new Set();
2597
2841
  const seenLabels = /* @__PURE__ */ new Set();
2598
2842
  const result = [];
2599
2843
  for (const entry of raw.hosts) {
2600
- 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) {
2601
2845
  throw new Error("Each host needs a non-empty string 'label'.");
2602
2846
  }
2603
2847
  const label = entry.label.trim();
2604
2848
  if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
2605
2849
  throw new Error("Each host needs a non-empty string 'path'.");
2606
2850
  }
2607
- const expanded = expandTilde2(entry.path.trim());
2608
- if (!isAbsolute2(expanded)) {
2851
+ const expanded = expandTilde3(entry.path.trim());
2852
+ if (!isAbsolute3(expanded)) {
2609
2853
  throw new Error("Host paths must be absolute (or start with '~').");
2610
2854
  }
2611
- const abs = resolve4(expanded);
2855
+ const abs = resolve5(expanded);
2612
2856
  if (seenPaths.has(abs)) continue;
2613
2857
  if (seenLabels.has(label)) {
2614
2858
  throw new Error(`Duplicate host label '${label}'; each host needs a distinct label.`);
@@ -2633,13 +2877,13 @@ import {
2633
2877
  // src/commands/import.ts
2634
2878
  import { createReadStream } from "fs";
2635
2879
  import { readdir, readFile as readFile2, rm, stat as stat3 } from "fs/promises";
2636
- import { homedir as homedir6 } from "os";
2637
- 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";
2638
2882
  import { createInterface } from "readline";
2639
2883
  import {
2640
2884
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
2641
2885
  assertBasouRootSafe as assertBasouRootSafe6,
2642
- basouPaths as basouPaths7,
2886
+ basouPaths as basouPaths8,
2643
2887
  CLAUDE_IMPORT_SOURCE,
2644
2888
  CODEX_IMPORT_SOURCE,
2645
2889
  classifyFilesBySourceRoot as classifyFilesBySourceRoot2,
@@ -2651,7 +2895,7 @@ import {
2651
2895
  readManifest as readManifest4,
2652
2896
  readSessionYaml as readSessionYaml2,
2653
2897
  reimportPreservingId,
2654
- resolveRepositoryRoot as resolveRepositoryRoot6,
2898
+ resolveRepositoryRoot as resolveRepositoryRoot5,
2655
2899
  SESSION_IMPORT_SCHEMA_VERSION,
2656
2900
  SessionImportPayloadSchema
2657
2901
  } from "@basou/core";
@@ -2705,10 +2949,10 @@ function resolveSourceRoots(args) {
2705
2949
  const { projectFlags, manifest, repoRoot, cwd } = args;
2706
2950
  let resolved;
2707
2951
  if (projectFlags.length > 0) {
2708
- resolved = projectFlags.map((p) => resolve5(cwd, p));
2952
+ resolved = projectFlags.map((p) => resolve6(cwd, p));
2709
2953
  } else {
2710
2954
  const roots = manifest.import?.source_roots;
2711
- 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];
2712
2956
  }
2713
2957
  return [...new Set(resolved)];
2714
2958
  }
@@ -2721,7 +2965,7 @@ async function doRunImportClaudeCode(options, ctx) {
2721
2965
  repoRoot: repositoryRoot,
2722
2966
  cwd: ctx.cwd ?? process.cwd()
2723
2967
  });
2724
- const projectsRoot = ctx.claudeProjectsDir ?? join8(homedir6(), ".claude", "projects");
2968
+ const projectsRoot = ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects");
2725
2969
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
2726
2970
  const projectSet = new Set(projectPaths);
2727
2971
  const candidates = files.map((file) => {
@@ -2760,7 +3004,7 @@ async function doRunImportCodex(options, ctx) {
2760
3004
  repoRoot: repositoryRoot,
2761
3005
  cwd: ctx.cwd ?? process.cwd()
2762
3006
  });
2763
- const sessionsRoot = ctx.codexSessionsDir ?? join8(homedir6(), ".codex", "sessions");
3007
+ const sessionsRoot = ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions");
2764
3008
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
2765
3009
  const candidates = rollouts.map(({ file, externalId }) => ({
2766
3010
  externalId,
@@ -2798,7 +3042,7 @@ function assertSelector(options) {
2798
3042
  async function resolveImportTarget(ctx) {
2799
3043
  const cwd = ctx.cwd ?? process.cwd();
2800
3044
  const repositoryRoot = await resolveRepositoryRootForImport(cwd);
2801
- const paths = basouPaths7(repositoryRoot);
3045
+ const paths = basouPaths8(repositoryRoot);
2802
3046
  await assertWorkspaceInitialized5(paths.root);
2803
3047
  const manifest = await readManifest4(paths);
2804
3048
  return { repositoryRoot, paths, manifest };
@@ -2841,7 +3085,9 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2841
3085
  throw new Error("Invalid import payload", { cause: parsed.error });
2842
3086
  }
2843
3087
  if (parsed.data.schema_version !== SESSION_IMPORT_SCHEMA_VERSION) {
2844
- throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
3088
+ throw new Error(
3089
+ `Unsupported import schema_version: ${parsed.data.schema_version} (expected ${SESSION_IMPORT_SCHEMA_VERSION}). The envelope's shape is published as @basou/core/schemas/session-import.schema.json; this version may move at a minor release, so a producer should read it from the installed @basou/core rather than pinning it.`
3090
+ );
2845
3091
  }
2846
3092
  return parsed.data;
2847
3093
  };
@@ -2889,7 +3135,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2889
3135
  if (priors.length > 0 && options.force === true) {
2890
3136
  if (options.dryRun !== true) {
2891
3137
  for (const { sessionId } of priors) {
2892
- await rm(join8(paths.sessions, sessionId), { recursive: true, force: true });
3138
+ await rm(join9(paths.sessions, sessionId), { recursive: true, force: true });
2893
3139
  }
2894
3140
  }
2895
3141
  counts.replaced++;
@@ -3000,7 +3246,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3000
3246
  if (options.session !== void 0) {
3001
3247
  const matches = [];
3002
3248
  for (const projectPath of projectPaths) {
3003
- const file = join8(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
3249
+ const file = join9(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
3004
3250
  if (await pathExists(file)) matches.push(file);
3005
3251
  }
3006
3252
  if (matches.length === 0) {
@@ -3011,7 +3257,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3011
3257
  const files = [];
3012
3258
  let anyDirFound = false;
3013
3259
  for (const projectPath of projectPaths) {
3014
- const transcriptDir = join8(projectsRoot, encodeProjectDir(projectPath));
3260
+ const transcriptDir = join9(projectsRoot, encodeProjectDir(projectPath));
3015
3261
  let entries;
3016
3262
  try {
3017
3263
  entries = await readdir(transcriptDir);
@@ -3021,7 +3267,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3021
3267
  }
3022
3268
  anyDirFound = true;
3023
3269
  for (const name of entries) {
3024
- if (name.endsWith(".jsonl")) files.push(join8(transcriptDir, name));
3270
+ if (name.endsWith(".jsonl")) files.push(join9(transcriptDir, name));
3025
3271
  }
3026
3272
  }
3027
3273
  if (!anyDirFound) {
@@ -3078,7 +3324,7 @@ async function findRolloutFiles(sessionsRoot) {
3078
3324
  throw new Error("Failed to read Codex sessions directory", { cause: error });
3079
3325
  }
3080
3326
  for (const entry of entries) {
3081
- const full = join8(dir, entry.name);
3327
+ const full = join9(dir, entry.name);
3082
3328
  if (entry.isDirectory()) {
3083
3329
  await walk(full, false);
3084
3330
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -3234,7 +3480,7 @@ function shortId2(id) {
3234
3480
  }
3235
3481
  async function resolveRepositoryRootForImport(cwd) {
3236
3482
  try {
3237
- return await resolveRepositoryRoot6(cwd);
3483
+ return await resolveRepositoryRoot5(cwd);
3238
3484
  } catch (error) {
3239
3485
  if (error instanceof Error && error.message === "Not a git repository") {
3240
3486
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou import'.", {
@@ -3467,7 +3713,7 @@ async function warnIfPositionNamesOtherWorkspaces2(result, ctx) {
3467
3713
  });
3468
3714
  if (report !== null) {
3469
3715
  console.error(
3470
- positionForeignWorkspaceWarning(report, basouPaths8(result.workspaceRoot).files.orientation)
3716
+ positionForeignWorkspaceWarning(report, basouPaths9(result.workspaceRoot).files.orientation)
3471
3717
  );
3472
3718
  }
3473
3719
  }
@@ -3477,7 +3723,7 @@ async function renderOrientationForCwd(options, ctx) {
3477
3723
  return renderOrientationForRoot(repositoryRoot, options, ctx, { write: true });
3478
3724
  }
3479
3725
  async function renderOrientationForRoot(repositoryRoot, options, ctx, behaviour) {
3480
- const paths = basouPaths8(repositoryRoot);
3726
+ const paths = basouPaths9(repositoryRoot);
3481
3727
  await assertWorkspaceInitialized6(paths.root);
3482
3728
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
3483
3729
  const probeCtx = { cwd: repositoryRoot };
@@ -3491,7 +3737,7 @@ async function renderOrientationForRoot(repositoryRoot, options, ctx, behaviour)
3491
3737
  try {
3492
3738
  const hosts = await loadHostsConfig(ctx.hostsConfigPath);
3493
3739
  if (hosts !== null) {
3494
- federatedRoots = hosts.map((h) => ({ paths: basouPaths8(h.path), host: h.label }));
3740
+ federatedRoots = hosts.map((h) => ({ paths: basouPaths9(h.path), host: h.label }));
3495
3741
  }
3496
3742
  } catch (error) {
3497
3743
  console.error(
@@ -3535,6 +3781,9 @@ async function assertWorkspaceInitialized6(basouRoot) {
3535
3781
 
3536
3782
  // src/commands/hook.ts
3537
3783
  var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
3784
+ var MAX_TRANSCRIPT_HEAD_BYTES = 256 * 1024;
3785
+ var TOKEN_SCAN_CHUNK_BYTES = 1024 * 1024;
3786
+ var execFileAsync = promisify(execFile);
3538
3787
  function registerHookCommand(program2) {
3539
3788
  const hook = program2.command("hook").description(
3540
3789
  "Hook handlers for AI coding tools (Claude Code, Codex): read a hook payload on stdin, emit the tool's hook output on stdout"
@@ -3545,13 +3794,13 @@ function registerHookCommand(program2) {
3545
3794
  await runHookSessionStart();
3546
3795
  });
3547
3796
  hook.command("stop").description(
3548
- "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."
3549
3798
  ).option(
3550
3799
  "--min-edits <n>",
3551
3800
  `Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
3552
3801
  ).option(
3553
3802
  "--block",
3554
- "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"
3555
3804
  ).option(
3556
3805
  "--require-review",
3557
3806
  "Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
@@ -3664,12 +3913,25 @@ gh pr create|merge) without recording a review ('basou review record'). This
3664
3913
  gate is off by default; when on, its reminder is composed into the same
3665
3914
  envelope as the capture reminder.
3666
3915
 
3667
- By default the reminder is non-blocking: Claude sees it and may act on it or
3668
- stop. With --block (opt-in enforcement, 'basou hook install --block') it instead
3669
- returns decision:block, holding the agent in-turn to act on the reminder; the
3670
- 'stop_hook_active' flag and Claude Code's own loop prevention bound it to a
3671
- single turn. Either way the hook fails open: a bad payload or unreadable
3672
- 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.
3673
3935
  `;
3674
3936
  async function runHookStop(options, ctx = {}) {
3675
3937
  try {
@@ -3707,7 +3969,12 @@ async function doRunHookStop(options, ctx) {
3707
3969
  stopHookActive: false,
3708
3970
  ...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
3709
3971
  });
3972
+ const protocolUpdate = await evaluateProtocolUpdateGate({
3973
+ transcriptPath,
3974
+ target: ctx.protocolTargetPath ?? DEFAULT_TARGET_PATH
3975
+ });
3710
3976
  const parts = [];
3977
+ if (protocolUpdate !== null) parts.push(protocolUpdate);
3711
3978
  if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
3712
3979
  if (options.requireReview === true && evaluation.review.fires) {
3713
3980
  parts.push(evaluation.review.additionalContext);
@@ -3723,6 +3990,56 @@ async function doRunHookStop(options, ctx) {
3723
3990
  write(`${payloadJson}
3724
3991
  `);
3725
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
+ }
3726
4043
  async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
3727
4044
  const root = await resolveBasouRootForCommand(cwd, "hook session-start", {
3728
4045
  portfolioConfigPath
@@ -3822,11 +4139,25 @@ async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
3822
4139
  await handle.close();
3823
4140
  }
3824
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
+ }
3825
4156
  function parseMinEdits(raw) {
3826
4157
  if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
3827
4158
  return Number(raw);
3828
4159
  }
3829
- var DEFAULT_CLAUDE_SETTINGS_PATH = join9(homedir7(), ".claude", "settings.json");
4160
+ var DEFAULT_CLAUDE_SETTINGS_PATH = join10(homedir8(), ".claude", "settings.json");
3830
4161
  function resolveCliEntry() {
3831
4162
  return fileURLToPath(import.meta.url);
3832
4163
  }
@@ -3984,13 +4315,97 @@ async function doRunHookStatus(options) {
3984
4315
  review: / --require-review\b/.test(command)
3985
4316
  });
3986
4317
  console.log(`basou Stop hook: registered, ${mode}.`);
4318
+ await reportHookEntryBuild(command);
4319
+ }
4320
+ async function reportHookEntryBuild(command) {
4321
+ console.log(` this basou is: ${BASOU_VERSION_LINE}`);
4322
+ const entry = extractHookEntryPath(command);
4323
+ if (entry === void 0) {
4324
+ console.log(
4325
+ " runs: (registered by alias, not by path) \u2014 which build that resolves to depends on the hook's PATH, so this cannot tell you."
4326
+ );
4327
+ return;
4328
+ }
4329
+ try {
4330
+ const { stdout } = await execFileAsync(process.execPath, [entry, "--version"], {
4331
+ timeout: 1e4
4332
+ });
4333
+ const reported = stdout.trim();
4334
+ console.log(` the hook runs: ${entry}`);
4335
+ console.log(` that build is: ${reported}`);
4336
+ if (!reported.includes("(build ")) {
4337
+ console.log(
4338
+ " note: that build predates build stamping, so what it reports is its package.json rather than itself \u2014 it cannot tell you which build it is. Update it (rebuild a source checkout, or reinstall the package) to find out."
4339
+ );
4340
+ }
4341
+ } catch {
4342
+ console.log(` the hook runs: ${entry}`);
4343
+ console.log(
4344
+ " that build is: could not be executed \u2014 the hook's wrapper fails open, so it is silently doing nothing."
4345
+ );
4346
+ }
4347
+ }
4348
+ function tokenizeShellCommand(command) {
4349
+ const tokens = [];
4350
+ let current = "";
4351
+ let started = false;
4352
+ let quote;
4353
+ for (let i = 0; i < command.length; i++) {
4354
+ const ch = command[i];
4355
+ if (quote === "'") {
4356
+ if (ch === "'") quote = void 0;
4357
+ else current += ch;
4358
+ continue;
4359
+ }
4360
+ if (quote === '"') {
4361
+ if (ch === '"') quote = void 0;
4362
+ else if (ch === "\\" && i + 1 < command.length) current += command[++i];
4363
+ else current += ch;
4364
+ continue;
4365
+ }
4366
+ if (ch === "'" || ch === '"') {
4367
+ quote = ch;
4368
+ started = true;
4369
+ continue;
4370
+ }
4371
+ if (ch === "\\" && i + 1 < command.length) {
4372
+ current += command[++i];
4373
+ started = true;
4374
+ continue;
4375
+ }
4376
+ if (/\s/.test(ch)) {
4377
+ if (started) tokens.push(current);
4378
+ current = "";
4379
+ started = false;
4380
+ continue;
4381
+ }
4382
+ current += ch;
4383
+ started = true;
4384
+ }
4385
+ if (quote !== void 0) return void 0;
4386
+ if (started) tokens.push(current);
4387
+ return tokens;
4388
+ }
4389
+ function extractHookEntryPath(command) {
4390
+ const tokens = tokenizeShellCommand(command);
4391
+ if (tokens === void 0) return void 0;
4392
+ let index = 0;
4393
+ while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) index++;
4394
+ const interpreter = tokens[index];
4395
+ if (interpreter === void 0) return void 0;
4396
+ const base = interpreter.replace(/\\/g, "/").split("/").pop() ?? "";
4397
+ if (base !== "node" && base !== "node.exe") return void 0;
4398
+ index++;
4399
+ while (index < tokens.length && tokens[index].startsWith("-")) index++;
4400
+ const entry = tokens[index];
4401
+ return entry === void 0 || entry === "" ? void 0 : entry;
3987
4402
  }
3988
4403
  function describeHookMode(tiers) {
3989
4404
  const enforcement = tiers.block ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
3990
4405
  const gates = tiers.review ? "capture + review" : "capture";
3991
4406
  return `${enforcement}, ${gates}`;
3992
4407
  }
3993
- var DEFAULT_CODEX_FACE_PATH = join9(homedir7(), ".codex", "AGENTS.md");
4408
+ var DEFAULT_CODEX_FACE_PATH = join10(homedir8(), ".codex", "AGENTS.md");
3994
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.`;
3995
4410
  async function faceHasLeftoverOrientationBlock(facePath) {
3996
4411
  try {
@@ -4002,8 +4417,8 @@ async function faceHasLeftoverOrientationBlock(facePath) {
4002
4417
  return false;
4003
4418
  }
4004
4419
  }
4005
- var DEFAULT_CODEX_HOOKS_PATH = join9(homedir7(), ".codex", "hooks.json");
4006
- 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");
4007
4422
  async function readHooksFile(path) {
4008
4423
  let raw;
4009
4424
  try {
@@ -4140,6 +4555,7 @@ async function doRunCodexHookStatus(options) {
4140
4555
  console.log(
4141
4556
  `basou Codex SessionStart hook: registered in ${hooksPath} (matcher: ${matcher}); speaks only for workspaces registered in ~/.basou/portfolio.yaml.`
4142
4557
  );
4558
+ await reportHookEntryBuild(location.command);
4143
4559
  await reportCodexHookState(hooksPath, parsed, options);
4144
4560
  }
4145
4561
  async function codexHookTrustFor(hooksPath, location, configPath) {
@@ -4174,12 +4590,12 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
4174
4590
  }
4175
4591
 
4176
4592
  // src/commands/init.ts
4177
- import { basename as basename5, relative, resolve as resolve6 } from "path";
4593
+ import { basename as basename5, relative, resolve as resolve7 } from "path";
4178
4594
  import {
4179
4595
  appendBasouGitignore,
4180
4596
  createManifest,
4181
4597
  ensureBasouDirectory,
4182
- resolveRepositoryRoot as resolveRepositoryRoot7,
4598
+ resolveRepositoryRoot as resolveRepositoryRoot6,
4183
4599
  writeManifest
4184
4600
  } from "@basou/core";
4185
4601
  function collectValue(value, previous) {
@@ -4219,7 +4635,7 @@ async function doRunInit(options, ctx) {
4219
4635
  );
4220
4636
  }
4221
4637
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
4222
- const rel = relative(repositoryRoot, resolve6(cwd, p));
4638
+ const rel = relative(repositoryRoot, resolve7(cwd, p));
4223
4639
  return rel === "" ? "." : rel;
4224
4640
  });
4225
4641
  const paths = await ensureBasouDirectory(repositoryRoot);
@@ -4249,7 +4665,7 @@ function renderGitignoreWarning(error, verbose) {
4249
4665
  }
4250
4666
  async function resolveRepositoryRootForInit(cwd) {
4251
4667
  try {
4252
- return await resolveRepositoryRoot7(cwd);
4668
+ return await resolveRepositoryRoot6(cwd);
4253
4669
  } catch (error) {
4254
4670
  if (error instanceof Error && error.message === "Not a git repository") {
4255
4671
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou init'.", {
@@ -4265,14 +4681,15 @@ import {
4265
4681
  acquireLock as acquireLock4,
4266
4682
  appendEventToExistingSession as appendEventToExistingSession2,
4267
4683
  assertBasouRootSafe as assertBasouRootSafe8,
4268
- basouPaths as basouPaths9,
4684
+ basouPaths as basouPaths10,
4269
4685
  createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
4270
4686
  EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION4,
4271
4687
  findErrorCode as findErrorCode7,
4688
+ LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE3,
4272
4689
  readManifest as readManifest5,
4273
4690
  resolveSessionId as resolveSessionId2
4274
4691
  } from "@basou/core";
4275
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
4692
+ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
4276
4693
  var NOTE_SUBCOMMAND_LOOKALIKES = /* @__PURE__ */ new Set([
4277
4694
  "list",
4278
4695
  "ls",
@@ -4319,7 +4736,7 @@ async function doRunNote(body, options, ctx) {
4319
4736
  }
4320
4737
  const cwd = ctx.cwd ?? process.cwd();
4321
4738
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "note");
4322
- const paths = basouPaths9(repositoryRoot);
4739
+ const paths = basouPaths10(repositoryRoot);
4323
4740
  await assertWorkspaceInitialized7(paths.root);
4324
4741
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
4325
4742
  const occurredAt = now.toISOString();
@@ -4376,7 +4793,7 @@ function buildNoteEvent(input) {
4376
4793
  id: input.eventId,
4377
4794
  session_id: input.sessionId,
4378
4795
  occurred_at: input.occurredAt,
4379
- source: "local-cli",
4796
+ source: LOCAL_CLI_EVENT_SOURCE3,
4380
4797
  type: "note_added",
4381
4798
  body: input.body,
4382
4799
  // `basou note` is the resume-hint command; mark it so orientation surfaces
@@ -4385,13 +4802,13 @@ function buildNoteEvent(input) {
4385
4802
  };
4386
4803
  }
4387
4804
  function buildAdHocLabel2(body) {
4388
- const oneLine2 = body.replace(/\s+/g, " ").trim();
4389
- const truncated = oneLine2.length > LABEL_BODY_MAX ? `${oneLine2.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine2;
4805
+ const oneLine3 = body.replace(/\s+/g, " ").trim();
4806
+ const truncated = oneLine3.length > LABEL_BODY_MAX ? `${oneLine3.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine3;
4390
4807
  return `Ad-hoc note: ${truncated}`;
4391
4808
  }
4392
4809
  function parseBody(raw) {
4393
4810
  if (raw.trim().length === 0) {
4394
- throw new InvalidArgumentError2("Note body must not be empty");
4811
+ throw new InvalidArgumentError3("Note body must not be empty");
4395
4812
  }
4396
4813
  return raw;
4397
4814
  }
@@ -4428,7 +4845,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
4428
4845
 
4429
4846
  // src/commands/portfolio.ts
4430
4847
  import { existsSync, statSync } from "fs";
4431
- import { join as join10 } from "path";
4848
+ import { join as join11 } from "path";
4432
4849
  function registerPortfolioCommand(program2) {
4433
4850
  program2.command("portfolio").description(
4434
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"
@@ -4474,7 +4891,7 @@ async function runPortfolioList(options, ctx = {}) {
4474
4891
  async function doRunPortfolioList(options, ctx) {
4475
4892
  const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
4476
4893
  const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
4477
- const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join10(p, ".basou")));
4894
+ const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join11(p, ".basou")));
4478
4895
  const workspaces = await loadPortfolioConfig(configPath);
4479
4896
  const result = {
4480
4897
  configPath,
@@ -4539,10 +4956,10 @@ import {
4539
4956
  writeFileSync,
4540
4957
  writeSync
4541
4958
  } from "fs";
4542
- 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";
4543
4960
  import {
4544
4961
  appendBasouGitignore as appendBasouGitignore2,
4545
- basouPaths as basouPaths10,
4962
+ basouPaths as basouPaths11,
4546
4963
  classifyRetrofit,
4547
4964
  createManifest as createManifest2,
4548
4965
  ensureBasouDirectory as ensureBasouDirectory2,
@@ -4564,7 +4981,7 @@ import {
4564
4981
  renderAnchorStarter,
4565
4982
  renderViewPresetBlock,
4566
4983
  renderWithMarkers as renderWithMarkers4,
4567
- resolveRepositoryRoot as resolveRepositoryRoot8,
4984
+ resolveRepositoryRoot as resolveRepositoryRoot7,
4568
4985
  safeSimpleGit,
4569
4986
  seedMarkers,
4570
4987
  summarizePresetPlan,
@@ -4705,7 +5122,7 @@ function preservedUnknownLines(fields) {
4705
5122
  async function doRunProjectCheck(options, ctx) {
4706
5123
  const cwd = ctx.cwd ?? process.cwd();
4707
5124
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
4708
- const paths = basouPaths10(repositoryRoot);
5125
+ const paths = basouPaths11(repositoryRoot);
4709
5126
  const manifest = await readManifest6(paths);
4710
5127
  const roster = summarizeRosterDrift({
4711
5128
  ...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
@@ -4840,7 +5257,7 @@ async function runProjectSync(options, ctx = {}) {
4840
5257
  async function doRunProjectSync(options, ctx) {
4841
5258
  const cwd = ctx.cwd ?? process.cwd();
4842
5259
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project sync");
4843
- const paths = basouPaths10(repositoryRoot);
5260
+ const paths = basouPaths11(repositoryRoot);
4844
5261
  const manifest = await readManifest6(paths);
4845
5262
  const hasRoster = manifest.repos !== void 0 && manifest.repos.length > 0;
4846
5263
  const reconcile = reconcileSourceRoots({
@@ -4914,19 +5331,19 @@ async function runProjectAdopt(options, ctx = {}) {
4914
5331
  }
4915
5332
  }
4916
5333
  function classifySourceRoot(repositoryRoot, declaredPath) {
4917
- const absolute = resolve7(repositoryRoot, declaredPath);
5334
+ const absolute = resolve8(repositoryRoot, declaredPath);
4918
5335
  let real;
4919
5336
  try {
4920
5337
  real = realpathSync(absolute);
4921
5338
  } catch {
4922
5339
  return { path: declaredPath, kind: "unresolved" };
4923
5340
  }
4924
- return { path: declaredPath, kind: existsSync2(join11(real, ".git")) ? "repo" : "non-repo" };
5341
+ return { path: declaredPath, kind: existsSync2(join12(real, ".git")) ? "repo" : "non-repo" };
4925
5342
  }
4926
5343
  async function doRunProjectAdopt(options, ctx) {
4927
5344
  const cwd = ctx.cwd ?? process.cwd();
4928
5345
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project adopt");
4929
- const paths = basouPaths10(repositoryRoot);
5346
+ const paths = basouPaths11(repositoryRoot);
4930
5347
  const manifest = await readManifest6(paths);
4931
5348
  const alreadyDeclared = manifest.repos !== void 0 && manifest.repos.length > 0;
4932
5349
  const candidates = effectiveSourceRoots(manifest).map(
@@ -5021,11 +5438,11 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5021
5438
  };
5022
5439
  let real;
5023
5440
  try {
5024
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5441
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5025
5442
  } catch {
5026
5443
  return { ...base, reachable: false, instructionFiles: [] };
5027
5444
  }
5028
- if (!existsSync2(join11(real, ".git"))) {
5445
+ if (!existsSync2(join12(real, ".git"))) {
5029
5446
  return { ...base, reachable: false, instructionFiles: [] };
5030
5447
  }
5031
5448
  try {
@@ -5033,7 +5450,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5033
5450
  for (const name of INSTRUCTION_FILES) {
5034
5451
  let present = true;
5035
5452
  try {
5036
- lstatSync(join11(real, name));
5453
+ lstatSync(join12(real, name));
5037
5454
  } catch {
5038
5455
  present = false;
5039
5456
  }
@@ -5048,7 +5465,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
5048
5465
  async function doRunProjectWiring(options, ctx) {
5049
5466
  const cwd = ctx.cwd ?? process.cwd();
5050
5467
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project wiring");
5051
- const paths = basouPaths10(repositoryRoot);
5468
+ const paths = basouPaths11(repositoryRoot);
5052
5469
  const manifest = await readManifest6(paths);
5053
5470
  const roster = manifest.repos ?? [];
5054
5471
  const facts = [];
@@ -5140,14 +5557,14 @@ function gatherRepoGitignore(repositoryRoot, entry) {
5140
5557
  };
5141
5558
  let real;
5142
5559
  try {
5143
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5560
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5144
5561
  } catch {
5145
5562
  return { ...base, reachable: false, currentLines: [] };
5146
5563
  }
5147
- if (!existsSync2(join11(real, ".git"))) {
5564
+ if (!existsSync2(join12(real, ".git"))) {
5148
5565
  return { ...base, reachable: false, currentLines: [] };
5149
5566
  }
5150
- return { ...base, reachable: true, currentLines: readGitignoreLines(join11(real, ".gitignore")) };
5567
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join12(real, ".gitignore")) };
5151
5568
  }
5152
5569
  function hasErrorCode(error) {
5153
5570
  return error instanceof Error && typeof error.code === "string";
@@ -5161,7 +5578,7 @@ function readGitignoreLines(file) {
5161
5578
  }
5162
5579
  }
5163
5580
  function applyGitignorePlan(repositoryRoot, plan) {
5164
- const file = join11(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
5581
+ const file = join12(realpathSync(resolve8(repositoryRoot, plan.path)), ".gitignore");
5165
5582
  let existing = "";
5166
5583
  try {
5167
5584
  existing = readFileSync(file, "utf8");
@@ -5181,7 +5598,7 @@ function applyGitignorePlan(repositoryRoot, plan) {
5181
5598
  async function doRunProjectGitignore(options, ctx) {
5182
5599
  const cwd = ctx.cwd ?? process.cwd();
5183
5600
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project gitignore");
5184
- const paths = basouPaths10(repositoryRoot);
5601
+ const paths = basouPaths11(repositoryRoot);
5185
5602
  const manifest = await readManifest6(paths);
5186
5603
  const roster = manifest.repos ?? [];
5187
5604
  const facts = roster.map((entry) => gatherRepoGitignore(repositoryRoot, entry));
@@ -5296,12 +5713,12 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5296
5713
  const base = { path: entry.path, ...isSelf ? { self: true } : {} };
5297
5714
  let real;
5298
5715
  try {
5299
- real = realpathSync(resolve7(repositoryRoot, entry.path));
5716
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5300
5717
  } catch {
5301
5718
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
5302
5719
  }
5303
5720
  if (real === anchorReal) {
5304
- const anchorCanonical = join11(real, CANONICAL_FILE);
5721
+ const anchorCanonical = join12(real, CANONICAL_FILE);
5305
5722
  const anchorState = anchorCanonicalState(anchorCanonical);
5306
5723
  if (anchorState === "absent") {
5307
5724
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
@@ -5321,7 +5738,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5321
5738
  anchorCanonical,
5322
5739
  "self"
5323
5740
  ).map((spec) => {
5324
- const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
5741
+ const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
5325
5742
  return {
5326
5743
  name: spec.name,
5327
5744
  expectedTarget: spec.target,
@@ -5338,16 +5755,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5338
5755
  files: anchorFiles
5339
5756
  };
5340
5757
  }
5341
- if (!existsSync2(join11(real, ".git"))) {
5758
+ if (!existsSync2(join12(real, ".git"))) {
5342
5759
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
5343
5760
  }
5344
- 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);
5345
5762
  if (!existsSync2(canonicalFile)) {
5346
5763
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
5347
5764
  }
5348
5765
  const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
5349
5766
  (spec) => {
5350
- const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
5767
+ const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
5351
5768
  return {
5352
5769
  name: spec.name,
5353
5770
  expectedTarget: spec.target,
@@ -5368,7 +5785,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
5368
5785
  function applySymlinkPlan(repositoryRoot, plan) {
5369
5786
  let real;
5370
5787
  try {
5371
- real = realpathSync(resolve7(repositoryRoot, plan.path));
5788
+ real = realpathSync(resolve8(repositoryRoot, plan.path));
5372
5789
  } catch (error) {
5373
5790
  const message = failureReason(error);
5374
5791
  return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
@@ -5376,7 +5793,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
5376
5793
  const created = [];
5377
5794
  const failed = [];
5378
5795
  for (const { name, target } of plan.toCreate) {
5379
- const filePath = join11(real, name);
5796
+ const filePath = join12(real, name);
5380
5797
  try {
5381
5798
  mkdirSync(dirname4(filePath), { recursive: true });
5382
5799
  symlinkSync(target, filePath);
@@ -5394,9 +5811,9 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
5394
5811
  for (const entry of roster) {
5395
5812
  let name;
5396
5813
  try {
5397
- name = basename6(realpathSync(resolve7(repositoryRoot, entry.path)));
5814
+ name = basename6(realpathSync(resolve8(repositoryRoot, entry.path)));
5398
5815
  } catch {
5399
- name = basename6(resolve7(repositoryRoot, entry.path));
5816
+ name = basename6(resolve8(repositoryRoot, entry.path));
5400
5817
  }
5401
5818
  if (name === viewName) return entry.path;
5402
5819
  }
@@ -5410,7 +5827,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
5410
5827
  if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
5411
5828
  const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
5412
5829
  (spec) => {
5413
- const { state, actualTarget } = inspectSymlink(join11(viewDir, spec.name), spec.target);
5830
+ const { state, actualTarget } = inspectSymlink(join12(viewDir, spec.name), spec.target);
5414
5831
  return {
5415
5832
  name: spec.name,
5416
5833
  expectedTarget: spec.target,
@@ -5426,7 +5843,7 @@ function applyViewSymlinks(viewDir, files) {
5426
5843
  const failed = [];
5427
5844
  for (const f of files) {
5428
5845
  if (f.state !== "missing") continue;
5429
- const filePath = join11(viewDir, f.name);
5846
+ const filePath = join12(viewDir, f.name);
5430
5847
  try {
5431
5848
  mkdirSync(dirname4(filePath), { recursive: true });
5432
5849
  symlinkSync(f.expectedTarget, filePath);
@@ -5440,7 +5857,7 @@ function applyViewSymlinks(viewDir, files) {
5440
5857
  async function doRunProjectSymlinks(options, ctx) {
5441
5858
  const cwd = ctx.cwd ?? process.cwd();
5442
5859
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
5443
- const paths = basouPaths10(repositoryRoot);
5860
+ const paths = basouPaths11(repositoryRoot);
5444
5861
  const manifest = await readManifest6(paths);
5445
5862
  const roster = manifest.repos ?? [];
5446
5863
  const anchorReal = realpathSync(repositoryRoot);
@@ -5643,12 +6060,12 @@ async function runProjectWorkspace(options, ctx = {}) {
5643
6060
  }
5644
6061
  }
5645
6062
  function resolveViewDir(repositoryRoot, viewPath) {
5646
- const abs = resolve7(repositoryRoot, viewPath);
6063
+ const abs = resolve8(repositoryRoot, viewPath);
5647
6064
  try {
5648
6065
  return realpathSync(abs);
5649
6066
  } catch {
5650
6067
  try {
5651
- return join11(realpathSync(dirname4(abs)), basename6(abs));
6068
+ return join12(realpathSync(dirname4(abs)), basename6(abs));
5652
6069
  } catch {
5653
6070
  return abs;
5654
6071
  }
@@ -5657,7 +6074,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
5657
6074
  function gatherViewRepo(repositoryRoot, viewDir, entry) {
5658
6075
  let repoReal;
5659
6076
  try {
5660
- repoReal = realpathSync(resolve7(repositoryRoot, entry.path));
6077
+ repoReal = realpathSync(resolve8(repositoryRoot, entry.path));
5661
6078
  } catch {
5662
6079
  return { path: entry.path, reachable: false };
5663
6080
  }
@@ -5666,7 +6083,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
5666
6083
  return { path: entry.path, reachable: false };
5667
6084
  }
5668
6085
  const linkName = basename6(repoReal);
5669
- const { state, actualTarget } = inspectSymlink(join11(viewDir, linkName), expectedTarget);
6086
+ const { state, actualTarget } = inspectSymlink(join12(viewDir, linkName), expectedTarget);
5670
6087
  return {
5671
6088
  path: entry.path,
5672
6089
  reachable: true,
@@ -5680,7 +6097,7 @@ function applyViewPlan(viewDir, toCreate) {
5680
6097
  const created = [];
5681
6098
  const failed = [];
5682
6099
  for (const { name, target } of toCreate) {
5683
- const filePath = join11(viewDir, name);
6100
+ const filePath = join12(viewDir, name);
5684
6101
  try {
5685
6102
  mkdirSync(dirname4(filePath), { recursive: true });
5686
6103
  symlinkSync(target, filePath);
@@ -5695,7 +6112,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
5695
6112
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
5696
6113
  );
5697
6114
  function classifyViewLink(viewDir, name, rosterRealpaths) {
5698
- const filePath = join11(viewDir, name);
6115
+ const filePath = join12(viewDir, name);
5699
6116
  let isLink;
5700
6117
  try {
5701
6118
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -5709,12 +6126,12 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
5709
6126
  } catch {
5710
6127
  return null;
5711
6128
  }
5712
- const resolved = isAbsolute3(target) ? target : resolve7(viewDir, target);
6129
+ const resolved = isAbsolute4(target) ? target : resolve8(viewDir, target);
5713
6130
  try {
5714
6131
  if (rosterRealpaths.has(realpathSync(resolved))) return null;
5715
6132
  } catch {
5716
6133
  }
5717
- if (isAbsolute3(target)) return { target, kind: "absolute" };
6134
+ if (isAbsolute4(target)) return { target, kind: "absolute" };
5718
6135
  let isDir = false;
5719
6136
  try {
5720
6137
  isDir = statSync2(resolved).isDirectory();
@@ -5724,7 +6141,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
5724
6141
  if (!isDir) {
5725
6142
  return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
5726
6143
  }
5727
- return { target, kind: existsSync2(join11(resolved, ".git")) ? "repo" : "non-repo" };
6144
+ return { target, kind: existsSync2(join12(resolved, ".git")) ? "repo" : "non-repo" };
5728
6145
  }
5729
6146
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
5730
6147
  let names;
@@ -5749,7 +6166,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
5749
6166
  const pruned = [];
5750
6167
  const failed = [];
5751
6168
  for (const { name } of toPrune) {
5752
- const filePath = join11(viewDir, name);
6169
+ const filePath = join12(viewDir, name);
5753
6170
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
5754
6171
  if (c === null || c.kind !== "repo") {
5755
6172
  failed.push({
@@ -5770,7 +6187,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
5770
6187
  async function doRunProjectWorkspace(options, ctx) {
5771
6188
  const cwd = ctx.cwd ?? process.cwd();
5772
6189
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project workspace");
5773
- const paths = basouPaths10(repositoryRoot);
6190
+ const paths = basouPaths11(repositoryRoot);
5774
6191
  const manifest = await readManifest6(paths);
5775
6192
  const viewPath = manifest.workspace.view;
5776
6193
  const roster = manifest.repos ?? [];
@@ -5795,11 +6212,11 @@ async function doRunProjectWorkspace(options, ctx) {
5795
6212
  } else {
5796
6213
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
5797
6214
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
5798
- const rosterNames = roster.map((entry) => basename6(resolve7(repositoryRoot, entry.path)));
6215
+ const rosterNames = roster.map((entry) => basename6(resolve8(repositoryRoot, entry.path)));
5799
6216
  const rosterRealpaths = /* @__PURE__ */ new Set();
5800
6217
  for (const entry of roster) {
5801
6218
  try {
5802
- rosterRealpaths.add(realpathSync(resolve7(repositoryRoot, entry.path)));
6219
+ rosterRealpaths.add(realpathSync(resolve8(repositoryRoot, entry.path)));
5803
6220
  } catch {
5804
6221
  }
5805
6222
  }
@@ -5962,10 +6379,10 @@ async function runProjectPreset(options, ctx = {}) {
5962
6379
  }
5963
6380
  }
5964
6381
  function canonicalFileFor(anchorReal, canonicalName) {
5965
- return join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6382
+ return join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5966
6383
  }
5967
6384
  function canonicalLabelFor(canonicalName) {
5968
- return join11("agents", canonicalName, CANONICAL_FILE);
6385
+ return join12("agents", canonicalName, CANONICAL_FILE);
5969
6386
  }
5970
6387
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5971
6388
  const declared = {
@@ -5979,14 +6396,14 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
5979
6396
  }
5980
6397
  let real;
5981
6398
  try {
5982
- real = realpathSync(resolve7(repositoryRoot, entry.path));
6399
+ real = realpathSync(resolve8(repositoryRoot, entry.path));
5983
6400
  } catch {
5984
6401
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
5985
6402
  }
5986
6403
  if (real === anchorReal) {
5987
6404
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
5988
6405
  }
5989
- if (!existsSync2(join11(real, ".git"))) {
6406
+ if (!existsSync2(join12(real, ".git"))) {
5990
6407
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
5991
6408
  }
5992
6409
  const canonicalName = basename6(real);
@@ -6035,7 +6452,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
6035
6452
  anchorReal = void 0;
6036
6453
  }
6037
6454
  return roster.map((entry) => {
6038
- const abs = resolve7(repositoryRoot, entry.path);
6455
+ const abs = resolve8(repositoryRoot, entry.path);
6039
6456
  let real;
6040
6457
  try {
6041
6458
  real = realpathSync(abs);
@@ -6130,7 +6547,7 @@ function presetFailureReason(error) {
6130
6547
  async function doRunProjectPreset(options, ctx) {
6131
6548
  const cwd = ctx.cwd ?? process.cwd();
6132
6549
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project preset");
6133
- const paths = basouPaths10(repositoryRoot);
6550
+ const paths = basouPaths11(repositoryRoot);
6134
6551
  const manifest = await readManifest6(paths);
6135
6552
  const roster = manifest.repos ?? [];
6136
6553
  const anchorReal = realpathSync(repositoryRoot);
@@ -6359,7 +6776,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6359
6776
  };
6360
6777
  let real;
6361
6778
  try {
6362
- real = realpathSync(resolve7(repositoryRoot, target));
6779
+ real = realpathSync(resolve8(repositoryRoot, target));
6363
6780
  } catch {
6364
6781
  return empty;
6365
6782
  }
@@ -6368,24 +6785,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6368
6785
  const instructionFiles = [];
6369
6786
  for (const name of INSTRUCTION_FILES) {
6370
6787
  try {
6371
- lstatSync(join11(real, name));
6788
+ lstatSync(join12(real, name));
6372
6789
  instructionFiles.push(name);
6373
6790
  } catch {
6374
6791
  }
6375
6792
  }
6376
6793
  let ignored;
6377
6794
  try {
6378
- ignored = new Set(readGitignoreLines(join11(real, ".gitignore")).map((l) => l.trim()));
6795
+ ignored = new Set(readGitignoreLines(join12(real, ".gitignore")).map((l) => l.trim()));
6379
6796
  } catch {
6380
6797
  ignored = /* @__PURE__ */ new Set();
6381
6798
  }
6382
6799
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
6383
- const canonical2 = existsSync2(join11(anchorReal, "agents", canonicalName, CANONICAL_FILE));
6800
+ const canonical2 = existsSync2(join12(anchorReal, "agents", canonicalName, CANONICAL_FILE));
6384
6801
  let viewLink = false;
6385
6802
  const viewPath = manifest.workspace.view;
6386
6803
  if (viewPath !== void 0) {
6387
6804
  try {
6388
- lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), canonicalName));
6805
+ lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), canonicalName));
6389
6806
  viewLink = true;
6390
6807
  } catch {
6391
6808
  }
@@ -6399,27 +6816,27 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
6399
6816
  };
6400
6817
  }
6401
6818
  function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
6402
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6819
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6403
6820
  return expectedSymlinkTargets(repoReal, canonicalFile);
6404
6821
  }
6405
6822
  function viewLinkPointsAt(viewDir, name, repoReal) {
6406
- const filePath = join11(viewDir, name);
6823
+ const filePath = join12(viewDir, name);
6407
6824
  try {
6408
6825
  if (!lstatSync(filePath).isSymbolicLink()) return false;
6409
6826
  const target = readlinkSync(filePath);
6410
- if (isAbsolute3(target)) return false;
6411
- return realpathSync(resolve7(viewDir, target)) === repoReal;
6827
+ if (isAbsolute4(target)) return false;
6828
+ return realpathSync(resolve8(viewDir, target)) === repoReal;
6412
6829
  } catch {
6413
6830
  return false;
6414
6831
  }
6415
6832
  }
6416
6833
  function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
6417
- const filePath = join11(viewDir, name);
6834
+ const filePath = join12(viewDir, name);
6418
6835
  try {
6419
6836
  if (!lstatSync(filePath).isSymbolicLink()) return false;
6420
6837
  const target = readlinkSync(filePath);
6421
- if (isAbsolute3(target)) return false;
6422
- return resolve7(viewDir, target) === expectedRepoPath;
6838
+ if (isAbsolute4(target)) return false;
6839
+ return resolve8(viewDir, target) === expectedRepoPath;
6423
6840
  } catch {
6424
6841
  return false;
6425
6842
  }
@@ -6428,19 +6845,19 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6428
6845
  const anchorReal = realpathSync(repositoryRoot);
6429
6846
  let repoReal;
6430
6847
  try {
6431
- repoReal = realpathSync(resolve7(repositoryRoot, target));
6848
+ repoReal = realpathSync(resolve8(repositoryRoot, target));
6432
6849
  } catch {
6433
6850
  repoReal = void 0;
6434
6851
  }
6435
6852
  const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
6436
- const targetAbs = resolve7(repositoryRoot, target);
6853
+ const targetAbs = resolve8(repositoryRoot, target);
6437
6854
  const canonicalName = basename6(repoReal ?? targetAbs);
6438
6855
  const roster = manifest.repos ?? [];
6439
6856
  const declaredEntry = roster.find((r) => {
6440
6857
  try {
6441
- return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
6858
+ return realpathSync(resolve8(repositoryRoot, r.path)) === (repoReal ?? "\0");
6442
6859
  } catch {
6443
- return resolve7(repositoryRoot, r.path) === targetAbs;
6860
+ return resolve8(repositoryRoot, r.path) === targetAbs;
6444
6861
  }
6445
6862
  });
6446
6863
  const inRoster = declaredEntry !== void 0;
@@ -6449,7 +6866,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6449
6866
  const canonicalShared = roster.some((r) => {
6450
6867
  let rReal = null;
6451
6868
  try {
6452
- rReal = realpathSync(resolve7(repositoryRoot, r.path));
6869
+ rReal = realpathSync(resolve8(repositoryRoot, r.path));
6453
6870
  } catch {
6454
6871
  rReal = null;
6455
6872
  }
@@ -6457,15 +6874,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6457
6874
  if (repoReal !== void 0 && rReal === repoReal) return false;
6458
6875
  return basename6(rReal).toLowerCase() === cnFold;
6459
6876
  }
6460
- if (resolve7(repositoryRoot, r.path) === targetAbs) return false;
6461
- 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;
6462
6879
  });
6463
6880
  const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
6464
6881
  const items = [];
6465
6882
  if (!isAnchor) {
6466
6883
  if (repoReal !== void 0) {
6467
6884
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
6468
- const { state, actualTarget } = inspectSymlink(join11(repoReal, spec.name), spec.target);
6885
+ const { state, actualTarget } = inspectSymlink(join12(repoReal, spec.name), spec.target);
6469
6886
  if (isSelf) {
6470
6887
  if (state !== "missing")
6471
6888
  items.push({
@@ -6502,7 +6919,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6502
6919
  }
6503
6920
  let ignored;
6504
6921
  try {
6505
- ignored = new Set(readGitignoreLines(join11(repoReal, ".gitignore")).map((l) => l.trim()));
6922
+ ignored = new Set(readGitignoreLines(join12(repoReal, ".gitignore")).map((l) => l.trim()));
6506
6923
  for (const p of INSTRUCTION_FILES) {
6507
6924
  if (ignored.has(p) || ignored.has(`/${p}`)) {
6508
6925
  items.push({
@@ -6525,7 +6942,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6525
6942
  const viewPath = manifest.workspace.view;
6526
6943
  if (viewPath !== void 0) {
6527
6944
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6528
- const linkPath = join11(viewDir, canonicalName);
6945
+ const linkPath = join12(viewDir, canonicalName);
6529
6946
  let isLink = false;
6530
6947
  try {
6531
6948
  isLink = lstatSync(linkPath).isSymbolicLink();
@@ -6551,8 +6968,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
6551
6968
  else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
6552
6969
  }
6553
6970
  }
6554
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6555
- const canonicalLabel = join11("agents", canonicalName, CANONICAL_FILE);
6971
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6972
+ const canonicalLabel = join12("agents", canonicalName, CANONICAL_FILE);
6556
6973
  let canonicalIsLink = false;
6557
6974
  try {
6558
6975
  canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
@@ -6639,7 +7056,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6639
7056
  const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
6640
7057
  let currentRepoReal = null;
6641
7058
  try {
6642
- currentRepoReal = realpathSync(resolve7(repositoryRoot, plan.target));
7059
+ currentRepoReal = realpathSync(resolve8(repositoryRoot, plan.target));
6643
7060
  } catch {
6644
7061
  currentRepoReal = null;
6645
7062
  }
@@ -6656,12 +7073,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6656
7073
  );
6657
7074
  for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
6658
7075
  const expected = expectedByName.get(item.label);
6659
- 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") {
6660
7077
  changed(item.label);
6661
7078
  continue;
6662
7079
  }
6663
7080
  try {
6664
- unlinkSync(join11(repoReal, item.label));
7081
+ unlinkSync(join12(repoReal, item.label));
6665
7082
  removed.push(item.label);
6666
7083
  } catch (error) {
6667
7084
  failed.push({ label: item.label, message: failureReason(error) });
@@ -6674,13 +7091,13 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6674
7091
  continue;
6675
7092
  }
6676
7093
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
6677
- 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));
6678
7095
  if (!owned) {
6679
7096
  changed(item.label);
6680
7097
  continue;
6681
7098
  }
6682
7099
  try {
6683
- unlinkSync(join11(viewDir, item.label));
7100
+ unlinkSync(join12(viewDir, item.label));
6684
7101
  removed.push(`view/${item.label}`);
6685
7102
  } catch (error) {
6686
7103
  failed.push({ label: `view/${item.label}`, message: failureReason(error) });
@@ -6688,7 +7105,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
6688
7105
  }
6689
7106
  const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
6690
7107
  for (const item of removable.filter((i) => i.kind === "canonical-block")) {
6691
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7108
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
6692
7109
  try {
6693
7110
  if (lstatSync(canonicalFile).isSymbolicLink()) {
6694
7111
  changed(item.label);
@@ -6789,7 +7206,7 @@ function renderProjectTeardown(result) {
6789
7206
  async function doRunProjectTeardown(target, options, ctx = {}) {
6790
7207
  const cwd = ctx.cwd ?? process.cwd();
6791
7208
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project teardown");
6792
- const paths = basouPaths10(repositoryRoot);
7209
+ const paths = basouPaths11(repositoryRoot);
6793
7210
  const manifest = await readManifest6(paths);
6794
7211
  const plan = gatherRepoTeardown(repositoryRoot, manifest, target);
6795
7212
  const willApply = options.apply === true && !plan.isAnchor && plan.removableCount > 0;
@@ -6831,12 +7248,12 @@ function buildArchivedManifest(manifest, plan, updatedAt) {
6831
7248
  async function doRunProjectArchive(target, options, ctx) {
6832
7249
  const cwd = ctx.cwd ?? process.cwd();
6833
7250
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project archive");
6834
- const paths = basouPaths10(repositoryRoot);
7251
+ const paths = basouPaths11(repositoryRoot);
6835
7252
  const manifest = await readManifest6(paths);
6836
7253
  const roster = manifest.repos ?? [];
6837
7254
  let targetIsAnchor = false;
6838
7255
  try {
6839
- targetIsAnchor = realpathSync(resolve7(repositoryRoot, target)) === realpathSync(repositoryRoot);
7256
+ targetIsAnchor = realpathSync(resolve8(repositoryRoot, target)) === realpathSync(repositoryRoot);
6840
7257
  } catch {
6841
7258
  targetIsAnchor = false;
6842
7259
  }
@@ -6962,12 +7379,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
6962
7379
  } catch {
6963
7380
  return { canonicalDirOld: false, viewLinkOld: false };
6964
7381
  }
6965
- const canonicalDirOld = existsSync2(join11(anchorReal, "agents", oldBasename));
7382
+ const canonicalDirOld = existsSync2(join12(anchorReal, "agents", oldBasename));
6966
7383
  let viewLinkOld = false;
6967
7384
  const viewPath = manifest.workspace.view;
6968
7385
  if (viewPath !== void 0) {
6969
7386
  try {
6970
- lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), oldBasename));
7387
+ lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), oldBasename));
6971
7388
  viewLinkOld = true;
6972
7389
  } catch {
6973
7390
  }
@@ -6988,12 +7405,12 @@ function buildRenamedManifest(manifest, plan, updatedAt) {
6988
7405
  async function doRunProjectRename(oldPath, newPath, options, ctx) {
6989
7406
  const cwd = ctx.cwd ?? process.cwd();
6990
7407
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project rename");
6991
- const paths = basouPaths10(repositoryRoot);
7408
+ const paths = basouPaths11(repositoryRoot);
6992
7409
  const manifest = await readManifest6(paths);
6993
7410
  const roster = manifest.repos ?? [];
6994
7411
  let oldIsAnchor = false;
6995
7412
  try {
6996
- oldIsAnchor = realpathSync(resolve7(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
7413
+ oldIsAnchor = realpathSync(resolve8(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
6997
7414
  } catch {
6998
7415
  oldIsAnchor = false;
6999
7416
  }
@@ -7112,7 +7529,7 @@ async function runProjectNew(repos, options, ctx = {}) {
7112
7529
  }
7113
7530
  async function resolveRepositoryRootForNew(cwd) {
7114
7531
  try {
7115
- return await resolveRepositoryRoot8(cwd);
7532
+ return await resolveRepositoryRoot7(cwd);
7116
7533
  } catch (error) {
7117
7534
  if (error instanceof Error && error.message === "Not a git repository") {
7118
7535
  throw new Error(
@@ -7139,7 +7556,7 @@ async function doRunProjectNew(repos, options, ctx) {
7139
7556
  const viewStem = productName ?? workspaceName;
7140
7557
  const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
7141
7558
  const declared = repos.map((p) => {
7142
- const abs = resolve7(cwd, p);
7559
+ const abs = resolve8(cwd, p);
7143
7560
  let real;
7144
7561
  try {
7145
7562
  real = realpathSync(abs);
@@ -7164,7 +7581,7 @@ async function doRunProjectNew(repos, options, ctx) {
7164
7581
  const roster = rosterPaths.map((path) => ({ path }));
7165
7582
  const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
7166
7583
  const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
7167
- const paths = basouPaths10(repositoryRoot);
7584
+ const paths = basouPaths11(repositoryRoot);
7168
7585
  const existed = existsSync2(paths.files.manifest);
7169
7586
  const manifest = createManifest2({
7170
7587
  workspaceName,
@@ -7275,7 +7692,7 @@ async function runProjectDerive(options, ctx = {}) {
7275
7692
  async function doRunProjectDerive(options, ctx) {
7276
7693
  const cwd = ctx.cwd ?? process.cwd();
7277
7694
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
7278
- const paths = basouPaths10(repositoryRoot);
7695
+ const paths = basouPaths11(repositoryRoot);
7279
7696
  const manifest = await readManifest6(paths);
7280
7697
  if (manifest.repos === void 0 || manifest.repos.length === 0) {
7281
7698
  console.log(
@@ -7316,7 +7733,7 @@ async function doRunProjectDerive(options, ctx) {
7316
7733
  async function doRunProjectSeedAnchor(options, ctx) {
7317
7734
  const cwd = ctx.cwd ?? process.cwd();
7318
7735
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
7319
- const paths = basouPaths10(repositoryRoot);
7736
+ const paths = basouPaths11(repositoryRoot);
7320
7737
  const manifest = await readManifest6(paths);
7321
7738
  const roster = manifest.repos ?? [];
7322
7739
  console.log("# Anchor instruction-file seed (the planning master's own AGENTS.md)");
@@ -7325,7 +7742,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
7325
7742
  console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
7326
7743
  return;
7327
7744
  }
7328
- const anchorDoc = join11(repositoryRoot, CANONICAL_FILE);
7745
+ const anchorDoc = join12(repositoryRoot, CANONICAL_FILE);
7329
7746
  if (pathPresent(anchorDoc)) {
7330
7747
  console.log(
7331
7748
  `\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
@@ -7387,7 +7804,7 @@ function regularFileSpokes(repoReal) {
7387
7804
  const out = [];
7388
7805
  for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
7389
7806
  try {
7390
- const st = lstatSync(join11(repoReal, spoke));
7807
+ const st = lstatSync(join12(repoReal, spoke));
7391
7808
  if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
7392
7809
  } catch {
7393
7810
  }
@@ -7402,9 +7819,9 @@ function pathPresent(p) {
7402
7819
  return false;
7403
7820
  }
7404
7821
  }
7405
- function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal, viewCanonicalName) {
7822
+ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, viewCanonicalName) {
7406
7823
  const declaredEntry = roster.find((entry) => {
7407
- const entryAbs = resolve7(repositoryRoot, entry.path);
7824
+ const entryAbs = resolve8(repositoryRoot, entry.path);
7408
7825
  if (argReal !== void 0) {
7409
7826
  try {
7410
7827
  if (realpathSync(entryAbs) === argReal) return true;
@@ -7433,8 +7850,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
7433
7850
  };
7434
7851
  }
7435
7852
  const isAnchor = argReal === anchorReal;
7436
- const reachable = existsSync2(join11(argReal, ".git"));
7437
- const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7853
+ const reachable = existsSync2(join12(argReal, ".git"));
7854
+ const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
7438
7855
  return {
7439
7856
  path,
7440
7857
  declared,
@@ -7443,13 +7860,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
7443
7860
  reachable,
7444
7861
  canonicalName,
7445
7862
  ...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
7446
- agentsState: inspectAgentsState(join11(argReal, CANONICAL_FILE)),
7863
+ agentsState: inspectAgentsState(join12(argReal, CANONICAL_FILE)),
7447
7864
  canonicalExists: pathPresent(canonicalFile),
7448
7865
  regularSpokes: regularFileSpokes(argReal)
7449
7866
  };
7450
7867
  }
7451
7868
  function relocateAgentsFile(repoReal, canonicalFile) {
7452
- const agentsFile = join11(repoReal, CANONICAL_FILE);
7869
+ const agentsFile = join12(repoReal, CANONICAL_FILE);
7453
7870
  try {
7454
7871
  mkdirSync(dirname4(canonicalFile), { recursive: true });
7455
7872
  } catch (error) {
@@ -7510,7 +7927,7 @@ async function applyViewRetrofit(anchorReal, outcome) {
7510
7927
  async function doRunProjectRetrofit(repo, options, ctx) {
7511
7928
  const cwd = ctx.cwd ?? process.cwd();
7512
7929
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
7513
- const paths = basouPaths10(repositoryRoot);
7930
+ const paths = basouPaths11(repositoryRoot);
7514
7931
  const manifest = await readManifest6(paths);
7515
7932
  const roster = manifest.repos ?? [];
7516
7933
  const anchorReal = realpathSync(repositoryRoot);
@@ -7544,7 +7961,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7544
7961
  }
7545
7962
  return result2;
7546
7963
  }
7547
- const argAbs = resolve7(repositoryRoot, repo);
7964
+ const argAbs = resolve8(repositoryRoot, repo);
7548
7965
  let argReal;
7549
7966
  try {
7550
7967
  argReal = realpathSync(argAbs);
@@ -7556,7 +7973,6 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7556
7973
  repositoryRoot,
7557
7974
  anchorReal,
7558
7975
  roster,
7559
- repo,
7560
7976
  argAbs,
7561
7977
  argReal,
7562
7978
  viewCanonicalName
@@ -7566,7 +7982,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
7566
7982
  let failure;
7567
7983
  let partial = false;
7568
7984
  if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
7569
- const canonicalFile = join11(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
7985
+ const canonicalFile = join12(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
7570
7986
  const res = relocateAgentsFile(argReal, canonicalFile);
7571
7987
  if (res.ok) {
7572
7988
  applied = true;
@@ -7766,88 +8182,19 @@ function renderProjectRetrofit(result) {
7766
8182
  }
7767
8183
 
7768
8184
  // src/commands/protocol.ts
7769
- import { readFile as readFile4 } from "fs/promises";
7770
- import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers4, readMarkdownFile as readMarkdownFile8 } from "@basou/core";
7771
-
7772
- // src/lib/protocols-config.ts
7773
- import { homedir as homedir8 } from "os";
7774
- import { isAbsolute as isAbsolute4, join as join12, resolve as resolve8 } from "path";
7775
- import { readYamlFile as readYamlFile5 } from "@basou/core";
7776
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join12(homedir8(), ".basou", "protocols.yaml");
7777
- var DEFAULT_TARGET_PATH = join12(homedir8(), ".claude", "CLAUDE.md");
7778
- var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
7779
- var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
7780
- function expandTilde3(p) {
7781
- if (p === "~") return homedir8();
7782
- if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
7783
- return p;
7784
- }
7785
- function isRecord3(value) {
7786
- return typeof value === "object" && value !== null && !Array.isArray(value);
7787
- }
7788
- async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
7789
- let raw;
7790
- try {
7791
- raw = await readYamlFile5(configPath);
7792
- } catch (error) {
7793
- if (error instanceof Error && error.message === "YAML file not found") {
7794
- throw new Error(
7795
- "No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
7796
- );
7797
- }
7798
- if (error instanceof Error && error.message === "Failed to parse YAML content") {
7799
- throw new Error("~/.basou/protocols.yaml is not valid YAML.");
7800
- }
7801
- throw error;
7802
- }
7803
- if (!isRecord3(raw) || !Array.isArray(raw.protocols)) {
7804
- throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
7805
- }
7806
- for (const key of Object.keys(raw)) {
7807
- if (!ALLOWED_TOP_KEYS.has(key)) {
7808
- throw new Error(
7809
- `~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
7810
- );
7811
- }
7812
- }
7813
- const seen = /* @__PURE__ */ new Set();
7814
- const result = [];
7815
- for (const entry of raw.protocols) {
7816
- if (!isRecord3(entry)) {
7817
- throw new Error("Each protocol entry must be a mapping with a 'source' key.");
7818
- }
7819
- for (const key of Object.keys(entry)) {
7820
- if (!ALLOWED_ENTRY_KEYS.has(key)) {
7821
- throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
7822
- }
7823
- }
7824
- if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
7825
- throw new Error("Each protocol entry needs a non-empty string 'source'.");
7826
- }
7827
- if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
7828
- throw new Error("A protocol entry 'title' must be a non-empty string when present.");
7829
- }
7830
- const expanded = expandTilde3(entry.source.trim());
7831
- if (!isAbsolute4(expanded)) {
7832
- throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
7833
- }
7834
- const abs = resolve8(expanded);
7835
- if (seen.has(abs)) {
7836
- throw new Error("Duplicate protocol source (each source path may appear only once).");
7837
- }
7838
- seen.add(abs);
7839
- result.push(
7840
- entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
7841
- );
7842
- }
7843
- if (result.length === 0) {
7844
- throw new Error("~/.basou/protocols.yaml has no protocols.");
7845
- }
7846
- return result;
7847
- }
7848
-
7849
- // src/commands/protocol.ts
7850
- 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 };
7851
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. -->";
7852
8199
  function registerProtocolCommand(program2) {
7853
8200
  const protocol = program2.command("protocol").description("Manage the basou-managed standing-protocol block in the global CLAUDE.md");
@@ -7907,24 +8254,54 @@ async function readProtocolSources(entries) {
7907
8254
  }
7908
8255
  return out;
7909
8256
  }
7910
- function buildBlock(sources) {
7911
- const sections = sources.map(({ entry, content }) => {
8257
+ function buildSections(sources) {
8258
+ return sources.map(({ entry, content }) => {
7912
8259
  const body = content.replace(/\s+$/, "");
7913
8260
  return entry.title !== void 0 ? `## ${entry.title}
7914
8261
 
7915
8262
  ${body}` : body;
7916
- });
8263
+ }).join("\n\n");
8264
+ }
8265
+ function buildBlock(sections, stamp) {
7917
8266
  return `${MANAGED_NOTE}
8267
+ ${renderProtocolStamp(stamp)}
7918
8268
 
7919
- ${sections.join("\n\n")}
8269
+ ${sections}
7920
8270
  `;
7921
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
+ }
7922
8293
  async function doRunProtocolSync(options, ctx = {}) {
7923
8294
  const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
7924
8295
  const target = options.target ?? DEFAULT_TARGET_PATH;
7925
8296
  const entries = await loadProtocolsConfig(configPath);
7926
8297
  const sources = await readProtocolSources(entries);
7927
- 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);
7928
8305
  const foreign = await findForeignWorkspaceNames({
7929
8306
  text: block,
7930
8307
  configPath: ctx.portfolioConfigPath
@@ -7987,14 +8364,14 @@ async function doRunProtocolUnsync(options) {
7987
8364
  // src/commands/refresh.ts
7988
8365
  import {
7989
8366
  assertBasouRootSafe as assertBasouRootSafe9,
7990
- basouPaths as basouPaths11,
8367
+ basouPaths as basouPaths12,
7991
8368
  findErrorCode as findErrorCode9,
7992
8369
  readManifest as readManifest7
7993
8370
  } from "@basou/core";
7994
- import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
8371
+ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
7995
8372
 
7996
8373
  // src/commands/refresh-watch.ts
7997
- import { readdir as readdir2, stat as stat5 } from "fs/promises";
8374
+ import { readdir as readdir2, stat as stat6 } from "fs/promises";
7998
8375
  import { homedir as homedir9 } from "os";
7999
8376
  import { join as join13 } from "path";
8000
8377
  import { findErrorCode as findErrorCode8 } from "@basou/core";
@@ -8023,7 +8400,7 @@ async function scanSourceLogs(roots) {
8023
8400
  await walk(full);
8024
8401
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
8025
8402
  try {
8026
- const info = await stat5(full);
8403
+ const info = await stat6(full);
8027
8404
  out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
8028
8405
  } catch (error) {
8029
8406
  if (findErrorCode8(error, "ENOENT")) continue;
@@ -8118,7 +8495,7 @@ function collectPath2(value, previous) {
8118
8495
  function parseInterval(value) {
8119
8496
  const seconds = Number(value);
8120
8497
  if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
8121
- throw new InvalidArgumentError3(
8498
+ throw new InvalidArgumentError4(
8122
8499
  `--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
8123
8500
  );
8124
8501
  }
@@ -8229,7 +8606,7 @@ async function doRunRefreshWatch(options, ctx) {
8229
8606
  if (options.force === true) throw new Error("--watch cannot be combined with --force.");
8230
8607
  const cwd = ctx.cwd ?? process.cwd();
8231
8608
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
8232
- const paths = basouPaths11(repositoryRoot);
8609
+ const paths = basouPaths12(repositoryRoot);
8233
8610
  await assertWorkspaceInitialized8(paths.root);
8234
8611
  const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
8235
8612
  const controller = new AbortController();
@@ -8257,7 +8634,7 @@ async function doRunRefreshWatch(options, ctx) {
8257
8634
  async function computeRefresh(options, ctx) {
8258
8635
  const cwd = ctx.cwd ?? process.cwd();
8259
8636
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
8260
- const paths = basouPaths11(repositoryRoot);
8637
+ const paths = basouPaths12(repositoryRoot);
8261
8638
  await assertWorkspaceInitialized8(paths.root);
8262
8639
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
8263
8640
  const result = await refreshAll({
@@ -8366,10 +8743,10 @@ async function assertWorkspaceInitialized8(basouRoot) {
8366
8743
  import { isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
8367
8744
  import {
8368
8745
  assertBasouRootSafe as assertBasouRootSafe10,
8369
- basouPaths as basouPaths12,
8746
+ basouPaths as basouPaths13,
8370
8747
  findErrorCode as findErrorCode10,
8371
8748
  renderReport,
8372
- resolveRepositoryRoot as resolveRepositoryRoot9,
8749
+ resolveRepositoryRoot as resolveRepositoryRoot8,
8373
8750
  writeMarkdownFile as writeMarkdownFile6
8374
8751
  } from "@basou/core";
8375
8752
  function registerReportCommand(program2) {
@@ -8391,7 +8768,7 @@ async function runReportGenerate(options, ctx = {}) {
8391
8768
  async function doRunReportGenerate(options, ctx) {
8392
8769
  const cwd = ctx.cwd ?? process.cwd();
8393
8770
  const repositoryRoot = await resolveRepositoryRootForReport(cwd);
8394
- const paths = basouPaths12(repositoryRoot);
8771
+ const paths = basouPaths13(repositoryRoot);
8395
8772
  await assertWorkspaceInitialized9(paths.root);
8396
8773
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
8397
8774
  const result = await renderReport({
@@ -8418,7 +8795,7 @@ async function doRunReportGenerate(options, ctx) {
8418
8795
  }
8419
8796
  async function resolveRepositoryRootForReport(cwd) {
8420
8797
  try {
8421
- return await resolveRepositoryRoot9(cwd);
8798
+ return await resolveRepositoryRoot8(cwd);
8422
8799
  } catch (error) {
8423
8800
  if (error instanceof Error && error.message === "Not a git repository") {
8424
8801
  throw new Error(
@@ -8446,7 +8823,7 @@ import { homedir as homedir10 } from "os";
8446
8823
  import { resolve as resolve10 } from "path";
8447
8824
  import {
8448
8825
  assertBasouRootSafe as assertBasouRootSafe11,
8449
- basouPaths as basouPaths13,
8826
+ basouPaths as basouPaths14,
8450
8827
  buildReviewRecordedEvent,
8451
8828
  buildReviewRecordLabel,
8452
8829
  createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
@@ -8528,7 +8905,7 @@ async function runReviewRecord(options, ctx = {}) {
8528
8905
  async function doRunReviewRecord(options, ctx) {
8529
8906
  const cwd = ctx.cwd ?? process.cwd();
8530
8907
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "review record");
8531
- const paths = basouPaths13(repositoryRoot);
8908
+ const paths = basouPaths14(repositoryRoot);
8532
8909
  await assertWorkspaceInitialized10(paths.root);
8533
8910
  const raw = await readReviewInput(options, ctx);
8534
8911
  const review = parseReviewRecordInput(raw);
@@ -8675,17 +9052,17 @@ async function assertWorkspaceInitialized10(basouRoot) {
8675
9052
 
8676
9053
  // src/commands/review-gaps.ts
8677
9054
  import {
8678
- basouPaths as basouPaths14,
9055
+ basouPaths as basouPaths15,
8679
9056
  findReviewGaps
8680
9057
  } from "@basou/core";
8681
- import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
9058
+ import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
8682
9059
  function collectRepo(value, previous) {
8683
9060
  return [...previous, value];
8684
9061
  }
8685
9062
  function parseWindow(value) {
8686
9063
  const hours = Number(value);
8687
9064
  if (!Number.isInteger(hours) || hours <= 0) {
8688
- throw new InvalidArgumentError4("--window must be a positive integer (hours).");
9065
+ throw new InvalidArgumentError5("--window must be a positive integer (hours).");
8689
9066
  }
8690
9067
  return hours;
8691
9068
  }
@@ -8716,7 +9093,7 @@ async function runReviewGaps(options, ctx = {}) {
8716
9093
  async function doRunReviewGaps(options, ctx) {
8717
9094
  const cwd = ctx.cwd ?? process.cwd();
8718
9095
  const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
8719
- const paths = basouPaths14(repositoryRoot);
9096
+ const paths = basouPaths15(repositoryRoot);
8720
9097
  const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
8721
9098
  const summary = await findReviewGaps({
8722
9099
  paths,
@@ -8733,7 +9110,7 @@ async function doRunReviewGaps(options, ctx) {
8733
9110
  }
8734
9111
  return summary;
8735
9112
  }
8736
- function relAge(iso, now) {
9113
+ function relAge2(iso, now) {
8737
9114
  if (iso === null) return "(unknown)";
8738
9115
  const ms = now.getTime() - Date.parse(iso);
8739
9116
  if (!Number.isFinite(ms) || ms < 0) return "just now";
@@ -8746,11 +9123,11 @@ function relAge(iso, now) {
8746
9123
  function flatten(value) {
8747
9124
  return value.replace(/\s+/gu, " ").trim();
8748
9125
  }
8749
- var GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
9126
+ var GRAPHEMES2 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
8750
9127
  function graphemes(value) {
8751
- return [...GRAPHEMES.segment(value)].map((s) => s.segment);
9128
+ return [...GRAPHEMES2.segment(value)].map((s) => s.segment);
8752
9129
  }
8753
- function oneLine(value, max) {
9130
+ function oneLine2(value, max) {
8754
9131
  const flat = graphemes(flatten(value));
8755
9132
  return flat.length > max ? `${flat.slice(0, max - 1).join("")}\u2026` : flat.join("");
8756
9133
  }
@@ -8763,7 +9140,7 @@ var SELF_REPORTS_SHOWN = 3;
8763
9140
  function selfReportSuffix(u, stillCounted) {
8764
9141
  if (u.selfReports.length === 0) return "";
8765
9142
  const parts = u.selfReports.slice(0, SELF_REPORTS_SHOWN).map(
8766
- (r) => `${oneLine(r.reviewer, 40)}${claimedCommits(r.commits)}${r.recordedAfterCommit ? " (recorded after the commit)" : ""}`
9143
+ (r) => `${oneLine2(r.reviewer, 40)}${claimedCommits(r.commits)}${r.recordedAfterCommit ? " (recorded after the commit)" : ""}`
8767
9144
  );
8768
9145
  const rest = u.selfReports.length - parts.length;
8769
9146
  if (rest > 0) parts.push(`+${rest} more`);
@@ -8776,7 +9153,7 @@ function unobservedOutcomeSuffix(u) {
8776
9153
  return ` \xB7 ${scope}exited with no recorded status \u2014 landing assumed, not observed`;
8777
9154
  }
8778
9155
  function unitLine(u, now) {
8779
- const when = relAge(u.lastCommitAt, now);
9156
+ const when = relAge2(u.lastCommitAt, now);
8780
9157
  const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
8781
9158
  if (u.verdict === "near_unbound") {
8782
9159
  const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
@@ -8785,7 +9162,7 @@ function unitLine(u, now) {
8785
9162
  return `${head} \u2014 no bound cross-model review${selfReportSuffix(u, true)}${unobservedOutcomeSuffix(u)}`;
8786
9163
  }
8787
9164
  function candidateLine(u, now) {
8788
- const when = relAge(u.lastCommitAt, now);
9165
+ const when = relAge2(u.lastCommitAt, now);
8789
9166
  const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
8790
9167
  return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}${selfReportSuffix(u, false)}${unobservedOutcomeSuffix(u)}`;
8791
9168
  }
@@ -8821,7 +9198,7 @@ function renderReviewGaps(summary) {
8821
9198
  );
8822
9199
  for (const u of summary.unknowns) {
8823
9200
  lines.push(
8824
- `- ${relAge(u.lastCommitAt, now)} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) [${u.sessionId}]`
9201
+ `- ${relAge2(u.lastCommitAt, now)} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) [${u.sessionId}]`
8825
9202
  );
8826
9203
  }
8827
9204
  lines.push("");
@@ -8834,7 +9211,7 @@ function renderReviewGaps(summary) {
8834
9211
  }
8835
9212
  lines.push("");
8836
9213
  lines.push(
8837
- `Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" : relAge(summary.newestCommitAt, now)}). It never auto-judges that a review "happened", and temporal proximity alone is not a pass. It does not enforce.`
9214
+ `Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" : relAge2(summary.newestCommitAt, now)}). It never auto-judges that a review "happened", and temporal proximity alone is not a pass. It does not enforce.`
8838
9215
  );
8839
9216
  if (summary.gaps.some((u) => u.selfReports.length > 0)) {
8840
9217
  lines.push(
@@ -8884,7 +9261,7 @@ import { join as join14 } from "path";
8884
9261
  import {
8885
9262
  acquireLock as acquireLock5,
8886
9263
  assertBasouRootSafe as assertBasouRootSafe12,
8887
- basouPaths as basouPaths15,
9264
+ basouPaths as basouPaths16,
8888
9265
  ChildProcessRunner as ChildProcessRunner2,
8889
9266
  claudeCodeAdapterMetadata,
8890
9267
  codexAdapterMetadata,
@@ -8900,7 +9277,8 @@ import {
8900
9277
  readYamlFile as readYamlFile6,
8901
9278
  resolveClaudeCodeCommand,
8902
9279
  resolveCodexCommand,
8903
- resolveRepositoryRoot as resolveRepositoryRoot10,
9280
+ resolveRepositoryRoot as resolveRepositoryRoot9,
9281
+ SESSION_SCHEMA_VERSION as SESSION_SCHEMA_VERSION2,
8904
9282
  SessionSchema as SessionSchema2,
8905
9283
  sanitizeRelatedFiles,
8906
9284
  sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
@@ -8955,7 +9333,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
8955
9333
  const childArgs = adapter.transformArgs ? adapter.transformArgs(args) : args;
8956
9334
  const cwd = options.cwd ?? process.cwd();
8957
9335
  const repoRoot = await resolveRepositoryRootForRun(cwd);
8958
- const paths = basouPaths15(repoRoot);
9336
+ const paths = basouPaths16(repoRoot);
8959
9337
  await assertBasouRootSafe12(paths.root);
8960
9338
  const manifest = await readManifest9(paths);
8961
9339
  const sessionId = prefixedUlid4("ses");
@@ -9239,7 +9617,7 @@ function normalizeFileChangedSkipMessage(error) {
9239
9617
  function buildInitialSession2(input) {
9240
9618
  const cmdline = [input.command, ...input.args].join(" ");
9241
9619
  return {
9242
- schema_version: "0.1.0",
9620
+ schema_version: SESSION_SCHEMA_VERSION2,
9243
9621
  session: {
9244
9622
  id: input.id,
9245
9623
  label: `basou run ${cmdline} (${input.startedAt})`,
@@ -9310,7 +9688,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
9310
9688
  }
9311
9689
  async function resolveRepositoryRootForRun(cwd) {
9312
9690
  try {
9313
- return await resolveRepositoryRoot10(cwd);
9691
+ return await resolveRepositoryRoot9(cwd);
9314
9692
  } catch (error) {
9315
9693
  if (error instanceof Error && error.message === "Not a git repository") {
9316
9694
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
@@ -9344,11 +9722,12 @@ import {
9344
9722
  acquireLock as acquireLock6,
9345
9723
  appendEventToExistingSession as appendEventToExistingSession3,
9346
9724
  assertBasouRootSafe as assertBasouRootSafe13,
9347
- basouPaths as basouPaths16,
9725
+ basouPaths as basouPaths17,
9348
9726
  EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION6,
9349
9727
  enumerateSessionDirs as enumerateSessionDirs2,
9350
9728
  findErrorCode as findErrorCode12,
9351
9729
  importSessionFromJson as importSessionFromJson2,
9730
+ LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE4,
9352
9731
  loadSessionEntries as loadSessionEntries2,
9353
9732
  readAllEvents,
9354
9733
  readManifest as readManifest10,
@@ -9363,7 +9742,7 @@ import {
9363
9742
  SessionStatusSchema,
9364
9743
  sessionWorkStatsFromEvents
9365
9744
  } from "@basou/core";
9366
- import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
9745
+ import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
9367
9746
 
9368
9747
  // src/lib/format-duration.ts
9369
9748
  import { formatDurationMs } from "@basou/core";
@@ -9412,7 +9791,7 @@ async function runSessionList(options, ctx = {}) {
9412
9791
  async function doRunSessionList(options, ctx) {
9413
9792
  const cwd = ctx.cwd ?? process.cwd();
9414
9793
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
9415
- const paths = basouPaths16(repositoryRoot);
9794
+ const paths = basouPaths17(repositoryRoot);
9416
9795
  await assertWorkspaceInitialized11(paths.root);
9417
9796
  const now = /* @__PURE__ */ new Date();
9418
9797
  const records = (await loadSessionEntries2(paths, {
@@ -9464,7 +9843,7 @@ async function runSessionShow(idInput, options, ctx = {}) {
9464
9843
  async function doRunSessionShow(idInput, options, ctx) {
9465
9844
  const cwd = ctx.cwd ?? process.cwd();
9466
9845
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
9467
- const paths = basouPaths16(repositoryRoot);
9846
+ const paths = basouPaths17(repositoryRoot);
9468
9847
  await assertWorkspaceInitialized11(paths.root);
9469
9848
  const sessionId = await resolveSessionId3(paths, idInput);
9470
9849
  const sessionDir = join15(paths.sessions, sessionId);
@@ -9776,7 +10155,7 @@ async function runSessionImport(options, ctx = {}) {
9776
10155
  async function doRunSessionImport(options, ctx) {
9777
10156
  const cwd = ctx.cwd ?? process.cwd();
9778
10157
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
9779
- const paths = basouPaths16(repositoryRoot);
10158
+ const paths = basouPaths17(repositoryRoot);
9780
10159
  await assertWorkspaceInitialized11(paths.root);
9781
10160
  const manifest = await readManifest10(paths);
9782
10161
  const rawBody = await readInputFile(options.from);
@@ -9786,7 +10165,9 @@ async function doRunSessionImport(options, ctx) {
9786
10165
  throw new Error("Invalid import payload", { cause: parsed.error });
9787
10166
  }
9788
10167
  if (parsed.data.schema_version !== SESSION_IMPORT_SCHEMA_VERSION2) {
9789
- throw new Error(`Unsupported import schema_version: ${parsed.data.schema_version}`);
10168
+ throw new Error(
10169
+ `Unsupported import schema_version: ${parsed.data.schema_version} (expected ${SESSION_IMPORT_SCHEMA_VERSION2}). The envelope's shape is published as @basou/core/schemas/session-import.schema.json; this version may move at a minor release, so a producer should read it from the installed @basou/core rather than pinning it.`
10170
+ );
9790
10171
  }
9791
10172
  const importOptions2 = { dryRun: options.dryRun === true };
9792
10173
  if (options.label !== void 0) importOptions2.labelOverride = options.label;
@@ -9825,19 +10206,19 @@ function parseJsonStrict(body) {
9825
10206
  }
9826
10207
  function parseImportFormat(raw) {
9827
10208
  if (raw !== "json") {
9828
- throw new InvalidArgumentError5(`Unsupported format: ${raw}. Valid values: json`);
10209
+ throw new InvalidArgumentError6(`Unsupported format: ${raw}. Valid values: json`);
9829
10210
  }
9830
10211
  return "json";
9831
10212
  }
9832
10213
  function parseLabelOverride(raw) {
9833
10214
  if (raw.length === 0) {
9834
- throw new InvalidArgumentError5("Label must not be empty");
10215
+ throw new InvalidArgumentError6("Label must not be empty");
9835
10216
  }
9836
10217
  return raw;
9837
10218
  }
9838
10219
  function parseTaskIdOverride(raw) {
9839
10220
  if (raw.length === 0) {
9840
- throw new InvalidArgumentError5("Task id is empty");
10221
+ throw new InvalidArgumentError6("Task id is empty");
9841
10222
  }
9842
10223
  return raw;
9843
10224
  }
@@ -9890,7 +10271,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
9890
10271
  }
9891
10272
  const cwd = ctx.cwd ?? process.cwd();
9892
10273
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
9893
- const paths = basouPaths16(repositoryRoot);
10274
+ const paths = basouPaths17(repositoryRoot);
9894
10275
  await assertWorkspaceInitialized11(paths.root);
9895
10276
  const sessionId = await resolveSessionId3(paths, sessionIdInput);
9896
10277
  const body = hasBody ? options.body : await readNoteFile(options.fromFile);
@@ -9910,7 +10291,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
9910
10291
  id: eventId,
9911
10292
  session_id: sesId,
9912
10293
  occurred_at: occurredAt,
9913
- source: "local-cli",
10294
+ source: LOCAL_CLI_EVENT_SOURCE4,
9914
10295
  type: "note_added",
9915
10296
  body
9916
10297
  })
@@ -9935,7 +10316,7 @@ async function readNoteFile(path) {
9935
10316
  }
9936
10317
  function parseNoteBodyOption(raw) {
9937
10318
  if (raw.length === 0) {
9938
- throw new InvalidArgumentError5("--body must not be empty");
10319
+ throw new InvalidArgumentError6("--body must not be empty");
9939
10320
  }
9940
10321
  return raw;
9941
10322
  }
@@ -9972,7 +10353,7 @@ async function doRunSessionRechain(options, ctx) {
9972
10353
  }
9973
10354
  const cwd = ctx.cwd ?? process.cwd();
9974
10355
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
9975
- const paths = basouPaths16(repositoryRoot);
10356
+ const paths = basouPaths17(repositoryRoot);
9976
10357
  await assertWorkspaceInitialized11(paths.root);
9977
10358
  const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
9978
10359
  const dryRun = options.dryRun === true;
@@ -10027,10 +10408,10 @@ function renderRechainRow(row, dryRun) {
10027
10408
  // src/commands/stats.ts
10028
10409
  import {
10029
10410
  assertBasouRootSafe as assertBasouRootSafe14,
10030
- basouPaths as basouPaths17,
10411
+ basouPaths as basouPaths18,
10031
10412
  computeWorkStats,
10032
10413
  findErrorCode as findErrorCode13,
10033
- resolveRepositoryRoot as resolveRepositoryRoot11
10414
+ resolveRepositoryRoot as resolveRepositoryRoot10
10034
10415
  } from "@basou/core";
10035
10416
  function registerStatsCommand(program2) {
10036
10417
  program2.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
@@ -10048,7 +10429,7 @@ async function runStats(options, ctx = {}) {
10048
10429
  async function doRunStats(options, ctx) {
10049
10430
  const cwd = ctx.cwd ?? process.cwd();
10050
10431
  const repositoryRoot = await resolveRepositoryRootForStats(cwd);
10051
- const paths = basouPaths17(repositoryRoot);
10432
+ const paths = basouPaths18(repositoryRoot);
10052
10433
  await assertWorkspaceInitialized12(paths.root);
10053
10434
  const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
10054
10435
  const result = await computeWorkStats({
@@ -10137,7 +10518,7 @@ function formatInt(n) {
10137
10518
  }
10138
10519
  async function resolveRepositoryRootForStats(cwd) {
10139
10520
  try {
10140
- return await resolveRepositoryRoot11(cwd);
10521
+ return await resolveRepositoryRoot10(cwd);
10141
10522
  } catch (error) {
10142
10523
  if (error instanceof Error && error.message === "Not a git repository") {
10143
10524
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
@@ -10161,11 +10542,11 @@ async function assertWorkspaceInitialized12(basouRoot) {
10161
10542
  // src/commands/status.ts
10162
10543
  import {
10163
10544
  assertBasouRootSafe as assertBasouRootSafe15,
10164
- basouPaths as basouPaths18,
10545
+ basouPaths as basouPaths19,
10165
10546
  buildStatusSnapshot,
10166
10547
  findErrorCode as findErrorCode14,
10167
10548
  readManifest as readManifest11,
10168
- resolveRepositoryRoot as resolveRepositoryRoot12,
10549
+ resolveRepositoryRoot as resolveRepositoryRoot11,
10169
10550
  writeStatus
10170
10551
  } from "@basou/core";
10171
10552
  function registerStatusCommand(program2) {
@@ -10184,7 +10565,7 @@ async function runStatus(options, ctx = {}) {
10184
10565
  async function doRunStatus(options, ctx) {
10185
10566
  const cwd = ctx.cwd ?? process.cwd();
10186
10567
  const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
10187
- const paths = basouPaths18(repositoryRoot);
10568
+ const paths = basouPaths19(repositoryRoot);
10188
10569
  try {
10189
10570
  await assertBasouRootSafe15(paths.root);
10190
10571
  } catch (error) {
@@ -10223,7 +10604,7 @@ function renderTextStatus(s) {
10223
10604
  }
10224
10605
  async function resolveRepositoryRootForStatus(cwd) {
10225
10606
  try {
10226
- return await resolveRepositoryRoot12(cwd);
10607
+ return await resolveRepositoryRoot11(cwd);
10227
10608
  } catch (error) {
10228
10609
  if (error instanceof Error && error.message === "Not a git repository") {
10229
10610
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
@@ -10252,7 +10633,7 @@ import { join as join16 } from "path";
10252
10633
  import {
10253
10634
  archiveTask,
10254
10635
  assertBasouRootSafe as assertBasouRootSafe16,
10255
- basouPaths as basouPaths19,
10636
+ basouPaths as basouPaths20,
10256
10637
  createTaskWithEvent,
10257
10638
  deleteTask,
10258
10639
  editTask,
@@ -10274,7 +10655,7 @@ import {
10274
10655
  TaskWriteAfterEventError,
10275
10656
  updateTaskStatusWithEvent
10276
10657
  } from "@basou/core";
10277
- import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
10658
+ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
10278
10659
  var STATUS_VALUES3 = TaskStatusSchema.options;
10279
10660
  function registerTaskCommand(program2) {
10280
10661
  const task = program2.command("task").description("Manage Basou tasks (purpose units that span sessions)");
@@ -10353,7 +10734,7 @@ async function doRunTaskNew(options, ctx) {
10353
10734
  }
10354
10735
  const cwd = ctx.cwd ?? process.cwd();
10355
10736
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
10356
- const paths = basouPaths19(repositoryRoot);
10737
+ const paths = basouPaths20(repositoryRoot);
10357
10738
  await assertWorkspaceInitialized13(paths.root);
10358
10739
  const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
10359
10740
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
@@ -10462,7 +10843,7 @@ async function runTaskList(options, ctx = {}) {
10462
10843
  async function doRunTaskList(options, ctx) {
10463
10844
  const cwd = ctx.cwd ?? process.cwd();
10464
10845
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
10465
- const paths = basouPaths19(repositoryRoot);
10846
+ const paths = basouPaths20(repositoryRoot);
10466
10847
  await assertWorkspaceInitialized13(paths.root);
10467
10848
  const entries = await loadTaskEntries(paths, {
10468
10849
  onSkip: (id, reason) => printTaskSkip(id, reason)
@@ -10566,7 +10947,7 @@ async function runTaskShow(idInput, options, ctx = {}) {
10566
10947
  async function doRunTaskShow(idInput, options, ctx) {
10567
10948
  const cwd = ctx.cwd ?? process.cwd();
10568
10949
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
10569
- const paths = basouPaths19(repositoryRoot);
10950
+ const paths = basouPaths20(repositoryRoot);
10570
10951
  await assertWorkspaceInitialized13(paths.root);
10571
10952
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
10572
10953
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
@@ -10710,7 +11091,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
10710
11091
  const newStatus = parseTaskStatusPositional(newStatusInput);
10711
11092
  const cwd = ctx.cwd ?? process.cwd();
10712
11093
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
10713
- const paths = basouPaths19(repositoryRoot);
11094
+ const paths = basouPaths20(repositoryRoot);
10714
11095
  await assertWorkspaceInitialized13(paths.root);
10715
11096
  const taskId = await resolveTaskId2(paths, taskIdInput);
10716
11097
  const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
@@ -10787,7 +11168,7 @@ async function runTaskReconcile(options, ctx = {}) {
10787
11168
  async function doRunTaskReconcile(options, ctx) {
10788
11169
  const cwd = ctx.cwd ?? process.cwd();
10789
11170
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
10790
- const paths = basouPaths19(repositoryRoot);
11171
+ const paths = basouPaths20(repositoryRoot);
10791
11172
  await assertWorkspaceInitialized13(paths.root);
10792
11173
  const manifest = await readManifest12(paths);
10793
11174
  const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
@@ -10967,7 +11348,7 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
10967
11348
  }
10968
11349
  const cwd = ctx.cwd ?? process.cwd();
10969
11350
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
10970
- const paths = basouPaths19(repositoryRoot);
11351
+ const paths = basouPaths20(repositoryRoot);
10971
11352
  await assertWorkspaceInitialized13(paths.root);
10972
11353
  const manifest = await readManifest12(paths);
10973
11354
  const taskId = await resolveTaskId2(paths, taskIdInput);
@@ -11047,7 +11428,7 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
11047
11428
  }
11048
11429
  const cwd = ctx.cwd ?? process.cwd();
11049
11430
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
11050
- const paths = basouPaths19(repositoryRoot);
11431
+ const paths = basouPaths20(repositoryRoot);
11051
11432
  await assertWorkspaceInitialized13(paths.root);
11052
11433
  const manifest = await readManifest12(paths);
11053
11434
  const taskId = await resolveTaskId2(paths, taskIdInput);
@@ -11103,7 +11484,7 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
11103
11484
  }
11104
11485
  const cwd = ctx.cwd ?? process.cwd();
11105
11486
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
11106
- const paths = basouPaths19(repositoryRoot);
11487
+ const paths = basouPaths20(repositoryRoot);
11107
11488
  await assertWorkspaceInitialized13(paths.root);
11108
11489
  const manifest = await readManifest12(paths);
11109
11490
  const taskId = await resolveTaskId2(paths, taskIdInput);
@@ -11148,7 +11529,7 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
11148
11529
  }
11149
11530
  const cwd = ctx.cwd ?? process.cwd();
11150
11531
  const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
11151
- const paths = basouPaths19(repositoryRoot);
11532
+ const paths = basouPaths20(repositoryRoot);
11152
11533
  await assertWorkspaceInitialized13(paths.root);
11153
11534
  const manifest = await readManifest12(paths);
11154
11535
  const taskId = await resolveTaskId2(paths, taskIdInput);
@@ -11203,20 +11584,20 @@ async function readSingleLineFromStdin() {
11203
11584
  }
11204
11585
  function parseTitle2(raw) {
11205
11586
  if (raw.length === 0) {
11206
- throw new InvalidArgumentError6("Title must not be empty");
11587
+ throw new InvalidArgumentError7("Title must not be empty");
11207
11588
  }
11208
11589
  return raw;
11209
11590
  }
11210
11591
  function parseLabel(raw) {
11211
11592
  if (raw.length === 0) {
11212
- throw new InvalidArgumentError6("Label must not be empty");
11593
+ throw new InvalidArgumentError7("Label must not be empty");
11213
11594
  }
11214
11595
  return raw;
11215
11596
  }
11216
11597
  function parseInitialTaskStatus(raw) {
11217
11598
  const result = TaskStatusSchema.safeParse(raw);
11218
11599
  if (!result.success) {
11219
- throw new InvalidArgumentError6(
11600
+ throw new InvalidArgumentError7(
11220
11601
  `Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
11221
11602
  );
11222
11603
  }
@@ -11225,7 +11606,7 @@ function parseInitialTaskStatus(raw) {
11225
11606
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
11226
11607
  function parseIsoTimestampOption(raw) {
11227
11608
  if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
11228
- throw new InvalidArgumentError6(
11609
+ throw new InvalidArgumentError7(
11229
11610
  "Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
11230
11611
  );
11231
11612
  }
@@ -11234,7 +11615,7 @@ function parseIsoTimestampOption(raw) {
11234
11615
  function parseTaskStatusFilter(raw) {
11235
11616
  const result = TaskStatusSchema.safeParse(raw);
11236
11617
  if (!result.success) {
11237
- throw new InvalidArgumentError6(
11618
+ throw new InvalidArgumentError7(
11238
11619
  `Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
11239
11620
  );
11240
11621
  }
@@ -11249,14 +11630,14 @@ function parseTaskStatusPositional(raw) {
11249
11630
  }
11250
11631
  function parseDescriptionOption(raw) {
11251
11632
  if (raw.length === 0) {
11252
- throw new InvalidArgumentError6("Description must not be empty");
11633
+ throw new InvalidArgumentError7("Description must not be empty");
11253
11634
  }
11254
11635
  return raw;
11255
11636
  }
11256
11637
  function parsePositiveInt2(raw) {
11257
11638
  const n = Number.parseInt(raw, 10);
11258
11639
  if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
11259
- throw new InvalidArgumentError6(`Invalid number: ${raw}`);
11640
+ throw new InvalidArgumentError7(`Invalid number: ${raw}`);
11260
11641
  }
11261
11642
  return n;
11262
11643
  }
@@ -11369,10 +11750,10 @@ function maxLen3(values, floor) {
11369
11750
  // src/commands/verify.ts
11370
11751
  import {
11371
11752
  assertBasouRootSafe as assertBasouRootSafe17,
11372
- basouPaths as basouPaths20,
11753
+ basouPaths as basouPaths21,
11373
11754
  enumerateSessionDirs as enumerateSessionDirs3,
11374
11755
  findErrorCode as findErrorCode16,
11375
- resolveRepositoryRoot as resolveRepositoryRoot13,
11756
+ resolveRepositoryRoot as resolveRepositoryRoot12,
11376
11757
  resolveSessionId as resolveSessionId5,
11377
11758
  verifyEventsChain
11378
11759
  } from "@basou/core";
@@ -11395,7 +11776,7 @@ async function doRunVerify(options, ctx) {
11395
11776
  }
11396
11777
  const cwd = ctx.cwd ?? process.cwd();
11397
11778
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
11398
- const paths = basouPaths20(repositoryRoot);
11779
+ const paths = basouPaths21(repositoryRoot);
11399
11780
  await assertWorkspaceInitialized14(paths.root);
11400
11781
  const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
11401
11782
  const rows = [];
@@ -11443,7 +11824,7 @@ function renderVerdict(row) {
11443
11824
  }
11444
11825
  async function resolveRepositoryRootForVerify(cwd) {
11445
11826
  try {
11446
- return await resolveRepositoryRoot13(cwd);
11827
+ return await resolveRepositoryRoot12(cwd);
11447
11828
  } catch (error) {
11448
11829
  if (error instanceof Error && error.message === "Not a git repository") {
11449
11830
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
@@ -11470,20 +11851,20 @@ import { createHash as createHash2 } from "crypto";
11470
11851
  import { basename as basename10, resolve as resolve13 } from "path";
11471
11852
  import {
11472
11853
  assertBasouRootSafe as assertBasouRootSafe18,
11473
- basouPaths as basouPaths22,
11854
+ basouPaths as basouPaths23,
11474
11855
  findErrorCode as findErrorCode18,
11475
11856
  readManifest as readManifest16,
11476
- resolveRepositoryRoot as resolveRepositoryRoot15
11857
+ resolveRepositoryRoot as resolveRepositoryRoot14
11477
11858
  } from "@basou/core";
11478
- import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
11859
+ import { InvalidArgumentError as InvalidArgumentError8 } from "commander";
11479
11860
 
11480
11861
  // src/lib/portfolio-coverage.ts
11481
11862
  import { createReadStream as createReadStream2 } from "fs";
11482
- import { readdir as readdir3, stat as stat6 } from "fs/promises";
11863
+ import { readdir as readdir3, stat as stat7 } from "fs/promises";
11483
11864
  import { homedir as homedir12 } from "os";
11484
11865
  import { basename as basename8, dirname as dirname5, join as join17 } from "path";
11485
11866
  import { createInterface as createInterface2 } from "readline";
11486
- import { basouPaths as basouPaths21, readManifest as readManifest13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
11867
+ import { basouPaths as basouPaths22, readManifest as readManifest13, resolveRepositoryRoot as resolveRepositoryRoot13 } from "@basou/core";
11487
11868
  function uncapturedTotal(result) {
11488
11869
  return result.groups.reduce((sum, g) => sum + g.logs, 0) + result.unplaceable;
11489
11870
  }
@@ -11519,14 +11900,14 @@ async function collectDeclaredRoots(workspaces) {
11519
11900
  for (const ws of workspaces) {
11520
11901
  let importRoot;
11521
11902
  try {
11522
- importRoot = await resolveRepositoryRoot14(ws.repoRoot);
11903
+ importRoot = await resolveRepositoryRoot13(ws.repoRoot);
11523
11904
  } catch {
11524
11905
  inertWorkspaces.push({ path: ws.repoRoot, reason: "not_a_git_repo" });
11525
11906
  continue;
11526
11907
  }
11527
11908
  let resolved;
11528
11909
  try {
11529
- const manifest = await readManifest13(basouPaths21(importRoot));
11910
+ const manifest = await readManifest13(basouPaths22(importRoot));
11530
11911
  resolved = resolveSourceRoots({
11531
11912
  projectFlags: [],
11532
11913
  manifest,
@@ -11626,7 +12007,7 @@ async function isDirEntry(parent, entry) {
11626
12007
  if (entry.isDirectory()) return true;
11627
12008
  if (!entry.isSymbolicLink()) return false;
11628
12009
  try {
11629
- return (await stat6(join17(parent, entry.name))).isDirectory();
12010
+ return (await stat7(join17(parent, entry.name))).isDirectory();
11630
12011
  } catch {
11631
12012
  return false;
11632
12013
  }
@@ -11793,12 +12174,12 @@ function inertLines(result) {
11793
12174
  }
11794
12175
 
11795
12176
  // src/lib/portfolio-safety.ts
11796
- import { execFile } from "child_process";
12177
+ import { execFile as execFile2 } from "child_process";
11797
12178
  import { lstat as lstat2, realpath as realpath4 } from "fs/promises";
11798
12179
  import { isAbsolute as isAbsolute7, join as join18, relative as relative4, resolve as resolve11 } from "path";
11799
- import { promisify } from "util";
12180
+ import { promisify as promisify2 } from "util";
11800
12181
  import { readManifest as readManifest14 } from "@basou/core";
11801
- var execFileAsync = promisify(execFile);
12182
+ var execFileAsync2 = promisify2(execFile2);
11802
12183
  function errorCode(error) {
11803
12184
  return error instanceof Error ? error.code : void 0;
11804
12185
  }
@@ -11830,7 +12211,7 @@ async function inspectRepo(repoPath) {
11830
12211
  }
11831
12212
  }
11832
12213
  try {
11833
- const { stdout } = await execFileAsync("git", ["-C", repoPath, "ls-files", "-z"]);
12214
+ const { stdout } = await execFileAsync2("git", ["-C", repoPath, "ls-files", "-z"]);
11834
12215
  const tracked = stdout.split("\0").some((f) => f.length > 0 && isBasouPath(f));
11835
12216
  if (tracked) {
11836
12217
  return {
@@ -13163,7 +13544,7 @@ var DEFAULT_PORT = 4319;
13163
13544
  function parsePort(value) {
13164
13545
  const port = Number.parseInt(value, 10);
13165
13546
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
13166
- throw new InvalidArgumentError7("Port must be an integer between 1 and 65535.");
13547
+ throw new InvalidArgumentError8("Port must be an integer between 1 and 65535.");
13167
13548
  }
13168
13549
  return port;
13169
13550
  }
@@ -13249,7 +13630,7 @@ async function doRunView(options, ctx) {
13249
13630
  }
13250
13631
  async function buildSingleDeps(ctx, cwd) {
13251
13632
  const repositoryRoot = await resolveRepositoryRootForView(cwd);
13252
- const paths = basouPaths22(repositoryRoot);
13633
+ const paths = basouPaths23(repositoryRoot);
13253
13634
  await assertWorkspaceInitialized15(paths.root);
13254
13635
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
13255
13636
  return {
@@ -13283,7 +13664,7 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
13283
13664
  };
13284
13665
  }
13285
13666
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
13286
- const paths = basouPaths22(repoRoot);
13667
+ const paths = basouPaths23(repoRoot);
13287
13668
  const importCtx = {
13288
13669
  cwd: repoRoot,
13289
13670
  ...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
@@ -13370,7 +13751,7 @@ function waitForShutdown(signal) {
13370
13751
  }
13371
13752
  async function resolveRepositoryRootForView(cwd) {
13372
13753
  try {
13373
- return await resolveRepositoryRoot15(cwd);
13754
+ return await resolveRepositoryRoot14(cwd);
13374
13755
  } catch (error) {
13375
13756
  if (error instanceof Error && error.message === "Not a git repository") {
13376
13757
  throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
@@ -13392,12 +13773,30 @@ async function assertWorkspaceInitialized15(basouRoot) {
13392
13773
  }
13393
13774
 
13394
13775
  // src/program.ts
13776
+ function readBuildStamp() {
13777
+ if (false) return void 0;
13778
+ try {
13779
+ return JSON.parse('{"version":"0.46.0","commit":"dbc9c57","committedAt":"2026-09-19T15:43:35+09:00"}');
13780
+ } catch {
13781
+ return void 0;
13782
+ }
13783
+ }
13395
13784
  var require2 = createRequire(import.meta.url);
13396
13785
  var pkg = require2("../package.json");
13397
- var BASOU_CLI_VERSION = pkg.version;
13786
+ var BASOU_BUILD = readBuildStamp();
13787
+ var BASOU_CLI_VERSION = BASOU_BUILD?.version ?? pkg.version;
13788
+ var BASOU_VERSION_LINE = buildVersionLine();
13789
+ function buildVersionLine() {
13790
+ if (BASOU_BUILD === void 0) return `${pkg.version} (source)`;
13791
+ const coreBuild = basouCore.BASOU_CORE_BUILD;
13792
+ const self = `${BASOU_BUILD.version} (build ${BASOU_BUILD.commit}, ${BASOU_BUILD.committedAt})`;
13793
+ if (coreBuild === void 0) return `${self}; core build unknown (not stamped)`;
13794
+ if (coreBuild.commit === BASOU_BUILD.commit) return self;
13795
+ return `${self}; core build ${coreBuild.commit}, ${coreBuild.committedAt}`;
13796
+ }
13398
13797
  function buildProgram() {
13399
13798
  const program2 = new Command();
13400
- program2.name("basou").description("A harness for steering AI coding agents").version(BASOU_CLI_VERSION).enablePositionalOptions();
13799
+ program2.name("basou").description("A harness for steering AI coding agents").version(BASOU_VERSION_LINE).enablePositionalOptions();
13401
13800
  registerInitCommand(program2);
13402
13801
  registerStatusCommand(program2);
13403
13802
  registerStatsCommand(program2);