@basou/cli 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/program.js CHANGED
@@ -635,17 +635,21 @@ function printNoApprovals(options) {
635
635
  // src/commands/decision.ts
636
636
  import { readFile } from "fs/promises";
637
637
  import { homedir as homedir2 } from "os";
638
- import { resolve as resolve3 } from "path";
638
+ import { join as join3, resolve as resolve3 } from "path";
639
639
  import {
640
+ AGENT_INFRA_DIRS,
640
641
  acquireLock as acquireLock2,
641
642
  appendEventToExistingSession,
642
643
  assertBasouRootSafe as assertBasouRootSafe2,
643
644
  basouPaths as basouPaths3,
645
+ classifyFilesBySourceRoot,
644
646
  createAdHocSessionWithEvent,
645
647
  findErrorCode as findErrorCode2,
646
648
  isValidPrefixedId,
649
+ loadSessionEntries,
647
650
  prefixedUlid as prefixedUlid2,
648
651
  readManifest as readManifest2,
652
+ replayEvents as replayEvents2,
649
653
  resolveRepositoryRoot as resolveRepositoryRoot2,
650
654
  resolveSessionId,
651
655
  sanitizePath
@@ -845,6 +849,17 @@ function registerDecisionCommand(program) {
845
849
  ).option("--file <path>", "Read the JSON array from a file instead of stdin").option("--dry-run", "Validate and preview the decisions without writing them").option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").addHelpText("after", CAPTURE_HELP).action(async (options) => {
846
850
  await runDecisionCapture(options);
847
851
  });
852
+ decision.command("void").description(
853
+ "Void (or supersede) a recorded decision. Append-only: the original is kept but struck in decisions.md and skipped as orientation's latest direction. Use when a decision was wrong or recorded in the wrong project."
854
+ ).argument("<decision_id>", "The decision to void (its decision_ ULID)").option("--reason <text>", "Why the decision is voided", parseReason).option(
855
+ "--superseded-by <decision_id>",
856
+ "The decision that replaces this one (records a supersede rather than a plain void)"
857
+ ).option(
858
+ "--session <session_id>",
859
+ "Attach to an existing session; otherwise an ad-hoc session is created"
860
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (decisionId, options) => {
861
+ await runDecisionVoid(decisionId, options);
862
+ });
848
863
  }
849
864
  var CAPTURE_HELP = `
850
865
  Input format (a JSON array; one object per decision):
@@ -879,6 +894,28 @@ async function runDecisionRecord(options, ctx = {}) {
879
894
  process.exitCode = 1;
880
895
  }
881
896
  }
897
+ async function warnLinkedFilesOutsideRoots(input) {
898
+ if (input.linkedFiles.length === 0) return;
899
+ try {
900
+ const manifest = await readManifest2(input.paths);
901
+ if ((manifest.import?.source_roots?.length ?? 0) === 0) return;
902
+ const scope = await classifyFilesBySourceRoot({
903
+ files: input.linkedFiles,
904
+ workingDirectory: input.cwd,
905
+ sourceRoots: manifest.import?.source_roots,
906
+ masterRoot: input.repositoryRoot,
907
+ extraInRoot: AGENT_INFRA_DIRS
908
+ });
909
+ if (scope.outOfRoot.length === 0) return;
910
+ const PATH_SAMPLE = 5;
911
+ const sample = scope.outOfRoot.slice(0, PATH_SAMPLE).join(", ");
912
+ const more = scope.outOfRoot.length > PATH_SAMPLE ? ` (... +${scope.outOfRoot.length - PATH_SAMPLE} more)` : "";
913
+ console.error(
914
+ `basou: ${scope.outOfRoot.length} linked file(s) resolve outside this project's source_roots: ${sample}${more} \u2014 this decision may belong to another project.`
915
+ );
916
+ } catch {
917
+ }
918
+ }
882
919
  async function doRunDecisionRecord(options, ctx) {
883
920
  const cwd = ctx.cwd ?? process.cwd();
884
921
  const repositoryRoot = await resolveRepositoryRootForDecision(cwd);
@@ -888,6 +925,12 @@ async function doRunDecisionRecord(options, ctx) {
888
925
  const occurredAt = now.toISOString();
889
926
  const decisionId = prefixedUlid2("decision");
890
927
  const rich = pickRichFields(options);
928
+ await warnLinkedFilesOutsideRoots({
929
+ linkedFiles: rich.linked_files ?? [],
930
+ cwd,
931
+ paths,
932
+ repositoryRoot
933
+ });
891
934
  if (options.session !== void 0) {
892
935
  const sessionId = await resolveSessionId(paths, options.session);
893
936
  const sesId = sessionId;
@@ -971,6 +1014,12 @@ async function doRunDecisionCapture(options, ctx) {
971
1014
  await assertWorkspaceInitialized2(paths.root);
972
1015
  const raw = await readCaptureInput(options, ctx);
973
1016
  const decisions = parseCaptureInput(raw);
1017
+ await warnLinkedFilesOutsideRoots({
1018
+ linkedFiles: decisions.flatMap((d) => d.linked_files ?? []),
1019
+ cwd,
1020
+ paths,
1021
+ repositoryRoot
1022
+ });
974
1023
  if (options.dryRun === true) {
975
1024
  printCapturePreview(options, decisions);
976
1025
  return;
@@ -1017,6 +1066,161 @@ async function doRunDecisionCapture(options, ctx) {
1017
1066
  }))
1018
1067
  });
1019
1068
  }
1069
+ async function runDecisionVoid(decisionId, options, ctx = {}) {
1070
+ try {
1071
+ await doRunDecisionVoid(decisionId, options, ctx);
1072
+ } catch (error) {
1073
+ renderCliError(error, {
1074
+ verbose: isVerbose(options),
1075
+ classifiers: [failedToFinalizeClassifier]
1076
+ });
1077
+ process.exitCode = 1;
1078
+ }
1079
+ }
1080
+ async function doRunDecisionVoid(decisionId, options, ctx) {
1081
+ if (!isDecisionId(decisionId)) {
1082
+ throw new Error(`Invalid decision id: ${decisionId} (expected a decision_<ULID>).`);
1083
+ }
1084
+ if (options.supersededBy !== void 0 && !isDecisionId(options.supersededBy)) {
1085
+ throw new Error(
1086
+ `Invalid --superseded-by id: ${options.supersededBy} (expected a decision_<ULID>).`
1087
+ );
1088
+ }
1089
+ if (options.supersededBy === decisionId) {
1090
+ throw new Error("A decision cannot supersede itself.");
1091
+ }
1092
+ const cwd = ctx.cwd ?? process.cwd();
1093
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision void");
1094
+ const paths = basouPaths3(repositoryRoot);
1095
+ await assertWorkspaceInitialized2(paths.root);
1096
+ if (!await decisionExists(paths, decisionId)) {
1097
+ throw new Error(
1098
+ `Decision ${decisionId} not found in this workspace. Run 'basou decisions generate' or check the id.`
1099
+ );
1100
+ }
1101
+ const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
1102
+ const occurredAt = now.toISOString();
1103
+ const reason = options.reason;
1104
+ const supersededBy = options.supersededBy;
1105
+ if (options.session !== void 0) {
1106
+ const sessionId = await resolveSessionId(paths, options.session);
1107
+ const sessionLock = await acquireLock2(paths, "session", sessionId);
1108
+ let result;
1109
+ try {
1110
+ result = await appendEventToExistingSession({
1111
+ paths,
1112
+ sessionId,
1113
+ eventBuilder: (eventId) => buildDecisionVoidedEvent({
1114
+ eventId,
1115
+ sessionId,
1116
+ decisionId,
1117
+ occurredAt,
1118
+ reason,
1119
+ supersededBy
1120
+ })
1121
+ });
1122
+ } finally {
1123
+ await sessionLock.release();
1124
+ }
1125
+ printVoidResult(options, {
1126
+ mode: "attached",
1127
+ sessionId,
1128
+ decisionId,
1129
+ eventId: result.eventId,
1130
+ sessionStatus: result.sessionStatus,
1131
+ reason,
1132
+ supersededBy
1133
+ });
1134
+ return;
1135
+ }
1136
+ const manifest = await readManifest2(paths);
1137
+ const adHoc = await createAdHocSessionWithEvent({
1138
+ paths,
1139
+ manifest,
1140
+ label: `Ad-hoc decision void: ${decisionId}`,
1141
+ occurredAt,
1142
+ sessionSource: "human",
1143
+ workingDirectory: repositoryRoot,
1144
+ invocation: { command: "basou decision void", args: [decisionId] },
1145
+ targetEventBuilders: [
1146
+ (sessionId, eventId) => buildDecisionVoidedEvent({
1147
+ eventId,
1148
+ sessionId,
1149
+ decisionId,
1150
+ occurredAt,
1151
+ reason,
1152
+ supersededBy
1153
+ })
1154
+ ]
1155
+ });
1156
+ printVoidResult(options, {
1157
+ mode: "ad-hoc",
1158
+ sessionId: adHoc.sessionId,
1159
+ decisionId,
1160
+ eventId: adHoc.targetEventIds[0],
1161
+ sessionStatus: "completed",
1162
+ reason,
1163
+ supersededBy
1164
+ });
1165
+ }
1166
+ function isDecisionId(value) {
1167
+ return value.startsWith("decision_") && isValidPrefixedId(value);
1168
+ }
1169
+ async function decisionExists(paths, decisionId) {
1170
+ const entries = await loadSessionEntries(paths, { now: /* @__PURE__ */ new Date() });
1171
+ for (const entry of entries) {
1172
+ const sessionDir = join3(paths.sessions, entry.sessionId);
1173
+ try {
1174
+ for await (const ev of replayEvents2(sessionDir, {})) {
1175
+ if (ev.type === "decision_recorded" && ev.decision_id === decisionId) return true;
1176
+ }
1177
+ } catch {
1178
+ }
1179
+ }
1180
+ return false;
1181
+ }
1182
+ function buildDecisionVoidedEvent(input) {
1183
+ return {
1184
+ schema_version: "0.1.0",
1185
+ id: input.eventId,
1186
+ session_id: input.sessionId,
1187
+ occurred_at: input.occurredAt,
1188
+ source: "local-cli",
1189
+ type: "decision_voided",
1190
+ decision_id: input.decisionId,
1191
+ ...input.reason !== void 0 ? { reason: input.reason } : {},
1192
+ ...input.supersededBy !== void 0 ? { superseded_by: input.supersededBy } : {}
1193
+ };
1194
+ }
1195
+ function printVoidResult(options, result) {
1196
+ if (options.json === true) {
1197
+ console.log(
1198
+ JSON.stringify({
1199
+ event_id: result.eventId,
1200
+ session_id: result.sessionId,
1201
+ decision_id: result.decisionId,
1202
+ session_status: result.sessionStatus,
1203
+ mode: result.mode,
1204
+ ...result.reason !== void 0 ? { reason: result.reason } : {},
1205
+ ...result.supersededBy !== void 0 ? { superseded_by: result.supersededBy } : {}
1206
+ })
1207
+ );
1208
+ return;
1209
+ }
1210
+ const sid = shortSessionId(result.sessionId);
1211
+ const tail = result.supersededBy !== void 0 ? ` (superseded by ${result.supersededBy})` : "";
1212
+ if (result.mode === "ad-hoc") {
1213
+ console.log(`Voided ${result.decisionId} in ad-hoc session ${sid}${tail}`);
1214
+ } else {
1215
+ console.log(`Voided ${result.decisionId} in session ${sid} (${result.sessionStatus})${tail}`);
1216
+ }
1217
+ }
1218
+ function parseReason(raw) {
1219
+ if (raw.trim().length === 0) {
1220
+ throw new InvalidArgumentError("--reason must not be empty");
1221
+ }
1222
+ return raw;
1223
+ }
1020
1224
  async function readCaptureInput(options, ctx) {
1021
1225
  if (options.file !== void 0) {
1022
1226
  try {
@@ -1408,7 +1612,7 @@ async function assertWorkspaceInitialized3(basouRoot) {
1408
1612
  // src/commands/exec.ts
1409
1613
  import { mkdir } from "fs/promises";
1410
1614
  import { homedir as homedir3 } from "os";
1411
- import { join as join3 } from "path";
1615
+ import { join as join4 } from "path";
1412
1616
  import {
1413
1617
  acquireLock as acquireLock3,
1414
1618
  assertBasouRootSafe as assertBasouRootSafe4,
@@ -1448,13 +1652,13 @@ async function runExec(command, args, options, ctx = {}) {
1448
1652
  await assertBasouRootSafe4(paths.root);
1449
1653
  const manifest = await readManifest3(paths);
1450
1654
  const sessionId = prefixedUlid3("ses");
1451
- const sessionDir = join3(paths.sessions, sessionId);
1655
+ const sessionDir = join4(paths.sessions, sessionId);
1452
1656
  await mkdir(sessionDir, { recursive: true });
1453
1657
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
1454
1658
  await coreAppendChainedEvent(paths, sessionId, event);
1455
1659
  });
1456
1660
  const startedAt = now().toISOString();
1457
- const sessionYamlPath = join3(sessionDir, "session.yaml");
1661
+ const sessionYamlPath = join4(sessionDir, "session.yaml");
1458
1662
  const session = buildInitialSession({
1459
1663
  id: sessionId,
1460
1664
  command,
@@ -1804,15 +2008,15 @@ async function assertWorkspaceInitialized4(basouRoot) {
1804
2008
  import { createReadStream } from "fs";
1805
2009
  import { readdir, readFile as readFile2, rm, stat as stat2 } from "fs/promises";
1806
2010
  import { homedir as homedir4 } from "os";
1807
- import { basename as basename2, dirname, join as join4, resolve as resolve4 } from "path";
2011
+ import { basename as basename2, dirname, join as join5, resolve as resolve4 } from "path";
1808
2012
  import { createInterface } from "readline";
1809
2013
  import {
1810
- AGENT_INFRA_DIRS,
2014
+ AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
1811
2015
  assertBasouRootSafe as assertBasouRootSafe6,
1812
2016
  basouPaths as basouPaths7,
1813
2017
  CLAUDE_IMPORT_SOURCE,
1814
2018
  CODEX_IMPORT_SOURCE,
1815
- classifyFilesBySourceRoot,
2019
+ classifyFilesBySourceRoot as classifyFilesBySourceRoot2,
1816
2020
  claudeTranscriptToImportPayload,
1817
2021
  codexRolloutToImportPayload,
1818
2022
  enumerateSessionDirs,
@@ -1890,7 +2094,7 @@ async function doRunImportClaudeCode(options, ctx) {
1890
2094
  repoRoot: repositoryRoot,
1891
2095
  cwd: ctx.cwd ?? process.cwd()
1892
2096
  });
1893
- const projectsRoot = ctx.claudeProjectsDir ?? join4(homedir4(), ".claude", "projects");
2097
+ const projectsRoot = ctx.claudeProjectsDir ?? join5(homedir4(), ".claude", "projects");
1894
2098
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
1895
2099
  const projectSet = new Set(projectPaths);
1896
2100
  const candidates = files.map((file) => {
@@ -1929,7 +2133,7 @@ async function doRunImportCodex(options, ctx) {
1929
2133
  repoRoot: repositoryRoot,
1930
2134
  cwd: ctx.cwd ?? process.cwd()
1931
2135
  });
1932
- const sessionsRoot = ctx.codexSessionsDir ?? join4(homedir4(), ".codex", "sessions");
2136
+ const sessionsRoot = ctx.codexSessionsDir ?? join5(homedir4(), ".codex", "sessions");
1933
2137
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
1934
2138
  const candidates = rollouts.map(({ file, externalId }) => ({
1935
2139
  externalId,
@@ -1980,12 +2184,12 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1980
2184
  const noteCrossProject = async (externalId, payload) => {
1981
2185
  if (!crossProjectCheck) return;
1982
2186
  try {
1983
- const scope = await classifyFilesBySourceRoot({
2187
+ const scope = await classifyFilesBySourceRoot2({
1984
2188
  files: payload.session.related_files ?? [],
1985
2189
  workingDirectory: payload.session.working_directory,
1986
2190
  sourceRoots: projectPaths,
1987
2191
  masterRoot: dirname(paths.root),
1988
- extraInRoot: AGENT_INFRA_DIRS
2192
+ extraInRoot: AGENT_INFRA_DIRS2
1989
2193
  });
1990
2194
  if (scope.outOfRoot.length > 0) crossProject.push({ externalId, outOfRoot: scope.outOfRoot });
1991
2195
  } catch {
@@ -2058,7 +2262,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
2058
2262
  if (priors.length > 0 && options.force === true) {
2059
2263
  if (options.dryRun !== true) {
2060
2264
  for (const { sessionId } of priors) {
2061
- await rm(join4(paths.sessions, sessionId), { recursive: true, force: true });
2265
+ await rm(join5(paths.sessions, sessionId), { recursive: true, force: true });
2062
2266
  }
2063
2267
  }
2064
2268
  counts.replaced++;
@@ -2169,7 +2373,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2169
2373
  if (options.session !== void 0) {
2170
2374
  const matches = [];
2171
2375
  for (const projectPath of projectPaths) {
2172
- const file = join4(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2376
+ const file = join5(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
2173
2377
  if (await pathExists(file)) matches.push(file);
2174
2378
  }
2175
2379
  if (matches.length === 0) {
@@ -2180,7 +2384,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2180
2384
  const files = [];
2181
2385
  let anyDirFound = false;
2182
2386
  for (const projectPath of projectPaths) {
2183
- const transcriptDir = join4(projectsRoot, encodeProjectDir(projectPath));
2387
+ const transcriptDir = join5(projectsRoot, encodeProjectDir(projectPath));
2184
2388
  let entries;
2185
2389
  try {
2186
2390
  entries = await readdir(transcriptDir);
@@ -2190,7 +2394,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
2190
2394
  }
2191
2395
  anyDirFound = true;
2192
2396
  for (const name of entries) {
2193
- if (name.endsWith(".jsonl")) files.push(join4(transcriptDir, name));
2397
+ if (name.endsWith(".jsonl")) files.push(join5(transcriptDir, name));
2194
2398
  }
2195
2399
  }
2196
2400
  if (!anyDirFound) {
@@ -2247,7 +2451,7 @@ async function findRolloutFiles(sessionsRoot) {
2247
2451
  throw new Error("Failed to read Codex sessions directory", { cause: error });
2248
2452
  }
2249
2453
  for (const entry of entries) {
2250
- const full = join4(dir, entry.name);
2454
+ const full = join5(dir, entry.name);
2251
2455
  if (entry.isDirectory()) {
2252
2456
  await walk(full, false);
2253
2457
  } else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
@@ -2671,12 +2875,12 @@ import {
2671
2875
 
2672
2876
  // src/lib/hosts-config.ts
2673
2877
  import { homedir as homedir5 } from "os";
2674
- import { isAbsolute as isAbsolute2, join as join5, resolve as resolve6 } from "path";
2878
+ import { isAbsolute as isAbsolute2, join as join6, resolve as resolve6 } from "path";
2675
2879
  import { readYamlFile as readYamlFile4 } from "@basou/core";
2676
- var DEFAULT_HOSTS_CONFIG_PATH = join5(homedir5(), ".basou", "hosts.yaml");
2880
+ var DEFAULT_HOSTS_CONFIG_PATH = join6(homedir5(), ".basou", "hosts.yaml");
2677
2881
  function expandTilde2(p) {
2678
2882
  if (p === "~") return homedir5();
2679
- if (p.startsWith("~/")) return join5(homedir5(), p.slice(2));
2883
+ if (p.startsWith("~/")) return join6(homedir5(), p.slice(2));
2680
2884
  return p;
2681
2885
  }
2682
2886
  function isRecord2(value) {
@@ -2990,7 +3194,7 @@ import {
2990
3194
  unlinkSync,
2991
3195
  writeFileSync
2992
3196
  } from "fs";
2993
- import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve7 } from "path";
3197
+ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve7 } from "path";
2994
3198
  import {
2995
3199
  basouPaths as basouPaths10,
2996
3200
  GENERATED_END,
@@ -3249,7 +3453,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
3249
3453
  } catch {
3250
3454
  return { path: declaredPath, kind: "unresolved" };
3251
3455
  }
3252
- return { path: declaredPath, kind: existsSync(join6(real, ".git")) ? "repo" : "non-repo" };
3456
+ return { path: declaredPath, kind: existsSync(join7(real, ".git")) ? "repo" : "non-repo" };
3253
3457
  }
3254
3458
  async function doRunProjectAdopt(options, ctx) {
3255
3459
  const cwd = ctx.cwd ?? process.cwd();
@@ -3348,7 +3552,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3348
3552
  } catch {
3349
3553
  return { ...base, reachable: false, instructionFiles: [] };
3350
3554
  }
3351
- if (!existsSync(join6(real, ".git"))) {
3555
+ if (!existsSync(join7(real, ".git"))) {
3352
3556
  return { ...base, reachable: false, instructionFiles: [] };
3353
3557
  }
3354
3558
  try {
@@ -3356,7 +3560,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
3356
3560
  for (const name of INSTRUCTION_FILES) {
3357
3561
  let present = true;
3358
3562
  try {
3359
- lstatSync(join6(real, name));
3563
+ lstatSync(join7(real, name));
3360
3564
  } catch {
3361
3565
  present = false;
3362
3566
  }
@@ -3453,10 +3657,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
3453
3657
  } catch {
3454
3658
  return { ...base, reachable: false, currentLines: [] };
3455
3659
  }
3456
- if (!existsSync(join6(real, ".git"))) {
3660
+ if (!existsSync(join7(real, ".git"))) {
3457
3661
  return { ...base, reachable: false, currentLines: [] };
3458
3662
  }
3459
- return { ...base, reachable: true, currentLines: readGitignoreLines(join6(real, ".gitignore")) };
3663
+ return { ...base, reachable: true, currentLines: readGitignoreLines(join7(real, ".gitignore")) };
3460
3664
  }
3461
3665
  function hasErrorCode(error) {
3462
3666
  return error instanceof Error && typeof error.code === "string";
@@ -3470,7 +3674,7 @@ function readGitignoreLines(file) {
3470
3674
  }
3471
3675
  }
3472
3676
  function applyGitignorePlan(repositoryRoot, plan) {
3473
- const file = join6(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
3677
+ const file = join7(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
3474
3678
  let existing = "";
3475
3679
  try {
3476
3680
  existing = readFileSync(file, "utf8");
@@ -3589,16 +3793,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
3589
3793
  if (real === anchorReal) {
3590
3794
  return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
3591
3795
  }
3592
- if (!existsSync(join6(real, ".git"))) {
3796
+ if (!existsSync(join7(real, ".git"))) {
3593
3797
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
3594
3798
  }
3595
- const canonicalFile = join6(anchorReal, "agents", basename4(real), CANONICAL_FILE);
3799
+ const canonicalFile = join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
3596
3800
  if (!existsSync(canonicalFile)) {
3597
3801
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
3598
3802
  }
3599
3803
  const files = expectedSymlinkTargets(real, canonicalFile).map(
3600
3804
  (spec) => {
3601
- const { state, actualTarget } = inspectSymlink(join6(real, spec.name), spec.target);
3805
+ const { state, actualTarget } = inspectSymlink(join7(real, spec.name), spec.target);
3602
3806
  return {
3603
3807
  name: spec.name,
3604
3808
  expectedTarget: spec.target,
@@ -3627,7 +3831,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
3627
3831
  const created = [];
3628
3832
  const failed = [];
3629
3833
  for (const { name, target } of plan.toCreate) {
3630
- const filePath = join6(real, name);
3834
+ const filePath = join7(real, name);
3631
3835
  try {
3632
3836
  mkdirSync(dirname2(filePath), { recursive: true });
3633
3837
  symlinkSync(target, filePath);
@@ -3769,7 +3973,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
3769
3973
  return realpathSync(abs);
3770
3974
  } catch {
3771
3975
  try {
3772
- return join6(realpathSync(dirname2(abs)), basename4(abs));
3976
+ return join7(realpathSync(dirname2(abs)), basename4(abs));
3773
3977
  } catch {
3774
3978
  return abs;
3775
3979
  }
@@ -3787,7 +3991,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
3787
3991
  return { path: entry.path, reachable: false };
3788
3992
  }
3789
3993
  const linkName = basename4(repoReal);
3790
- const { state, actualTarget } = inspectSymlink(join6(viewDir, linkName), expectedTarget);
3994
+ const { state, actualTarget } = inspectSymlink(join7(viewDir, linkName), expectedTarget);
3791
3995
  return {
3792
3996
  path: entry.path,
3793
3997
  reachable: true,
@@ -3801,7 +4005,7 @@ function applyViewPlan(viewDir, toCreate) {
3801
4005
  const created = [];
3802
4006
  const failed = [];
3803
4007
  for (const { name, target } of toCreate) {
3804
- const filePath = join6(viewDir, name);
4008
+ const filePath = join7(viewDir, name);
3805
4009
  try {
3806
4010
  mkdirSync(dirname2(filePath), { recursive: true });
3807
4011
  symlinkSync(target, filePath);
@@ -3816,7 +4020,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
3816
4020
  INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
3817
4021
  );
3818
4022
  function classifyViewLink(viewDir, name, rosterRealpaths) {
3819
- const filePath = join6(viewDir, name);
4023
+ const filePath = join7(viewDir, name);
3820
4024
  let isLink;
3821
4025
  try {
3822
4026
  isLink = lstatSync(filePath).isSymbolicLink();
@@ -3845,7 +4049,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
3845
4049
  if (!isDir) {
3846
4050
  return { target, kind: existsSync(resolved) ? "non-repo" : "broken" };
3847
4051
  }
3848
- return { target, kind: existsSync(join6(resolved, ".git")) ? "repo" : "non-repo" };
4052
+ return { target, kind: existsSync(join7(resolved, ".git")) ? "repo" : "non-repo" };
3849
4053
  }
3850
4054
  function gatherExistingViewLinks(viewDir, rosterRealpaths) {
3851
4055
  let names;
@@ -3870,7 +4074,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
3870
4074
  const pruned = [];
3871
4075
  const failed = [];
3872
4076
  for (const { name } of toPrune) {
3873
- const filePath = join6(viewDir, name);
4077
+ const filePath = join7(viewDir, name);
3874
4078
  const c = classifyViewLink(viewDir, name, rosterRealpaths);
3875
4079
  if (c === null || c.kind !== "repo") {
3876
4080
  failed.push({
@@ -4081,10 +4285,10 @@ async function runProjectPreset(options, ctx = {}) {
4081
4285
  }
4082
4286
  }
4083
4287
  function canonicalFileFor(anchorReal, canonicalName) {
4084
- return join6(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4288
+ return join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
4085
4289
  }
4086
4290
  function canonicalLabelFor(canonicalName) {
4087
- return join6("agents", canonicalName, CANONICAL_FILE);
4291
+ return join7("agents", canonicalName, CANONICAL_FILE);
4088
4292
  }
4089
4293
  async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4090
4294
  const declared = {
@@ -4102,7 +4306,7 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4102
4306
  if (real === anchorReal) {
4103
4307
  return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
4104
4308
  }
4105
- if (!existsSync(join6(real, ".git"))) {
4309
+ if (!existsSync(join7(real, ".git"))) {
4106
4310
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
4107
4311
  }
4108
4312
  const canonicalName = basename4(real);
@@ -4333,24 +4537,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
4333
4537
  const instructionFiles = [];
4334
4538
  for (const name of INSTRUCTION_FILES) {
4335
4539
  try {
4336
- lstatSync(join6(real, name));
4540
+ lstatSync(join7(real, name));
4337
4541
  instructionFiles.push(name);
4338
4542
  } catch {
4339
4543
  }
4340
4544
  }
4341
4545
  let ignored;
4342
4546
  try {
4343
- ignored = new Set(readGitignoreLines(join6(real, ".gitignore")).map((l) => l.trim()));
4547
+ ignored = new Set(readGitignoreLines(join7(real, ".gitignore")).map((l) => l.trim()));
4344
4548
  } catch {
4345
4549
  ignored = /* @__PURE__ */ new Set();
4346
4550
  }
4347
4551
  const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
4348
- const canonical2 = existsSync(join6(anchorReal, "agents", canonicalName, CANONICAL_FILE));
4552
+ const canonical2 = existsSync(join7(anchorReal, "agents", canonicalName, CANONICAL_FILE));
4349
4553
  let viewLink = false;
4350
4554
  const viewPath = manifest.workspace.view;
4351
4555
  if (viewPath !== void 0) {
4352
4556
  try {
4353
- lstatSync(join6(resolveViewDir(repositoryRoot, viewPath), canonicalName));
4557
+ lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), canonicalName));
4354
4558
  viewLink = true;
4355
4559
  } catch {
4356
4560
  }
@@ -4512,12 +4716,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
4512
4716
  } catch {
4513
4717
  return { canonicalDirOld: false, viewLinkOld: false };
4514
4718
  }
4515
- const canonicalDirOld = existsSync(join6(anchorReal, "agents", oldBasename));
4719
+ const canonicalDirOld = existsSync(join7(anchorReal, "agents", oldBasename));
4516
4720
  let viewLinkOld = false;
4517
4721
  const viewPath = manifest.workspace.view;
4518
4722
  if (viewPath !== void 0) {
4519
4723
  try {
4520
- lstatSync(join6(resolveViewDir(repositoryRoot, viewPath), oldBasename));
4724
+ lstatSync(join7(resolveViewDir(repositoryRoot, viewPath), oldBasename));
4521
4725
  viewLinkOld = true;
4522
4726
  } catch {
4523
4727
  }
@@ -4666,7 +4870,7 @@ import {
4666
4870
  // src/lib/durable-write.ts
4667
4871
  import { randomUUID } from "crypto";
4668
4872
  import { lstat, open, rename, stat as stat3, unlink as unlink2 } from "fs/promises";
4669
- import { basename as basename5, dirname as dirname3, join as join7 } from "path";
4873
+ import { basename as basename5, dirname as dirname3, join as join8 } from "path";
4670
4874
  async function assertNotSymlink(targetPath) {
4671
4875
  try {
4672
4876
  const st = await lstat(targetPath);
@@ -4682,7 +4886,7 @@ async function assertNotSymlink(targetPath) {
4682
4886
  }
4683
4887
  async function writeFileDurable(targetPath, content) {
4684
4888
  const dir = dirname3(targetPath);
4685
- const tmpPath = join7(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
4889
+ const tmpPath = join8(dir, `.${basename5(targetPath)}.tmp.${randomUUID()}`);
4686
4890
  let mode = 420;
4687
4891
  try {
4688
4892
  mode = (await stat3(targetPath)).mode & 511;
@@ -4718,15 +4922,15 @@ async function writeFileDurable(targetPath, content) {
4718
4922
 
4719
4923
  // src/lib/protocols-config.ts
4720
4924
  import { homedir as homedir6 } from "os";
4721
- import { isAbsolute as isAbsolute4, join as join8, resolve as resolve8 } from "path";
4925
+ import { isAbsolute as isAbsolute4, join as join9, resolve as resolve8 } from "path";
4722
4926
  import { readYamlFile as readYamlFile5 } from "@basou/core";
4723
- var DEFAULT_PROTOCOLS_CONFIG_PATH = join8(homedir6(), ".basou", "protocols.yaml");
4724
- var DEFAULT_TARGET_PATH = join8(homedir6(), ".claude", "CLAUDE.md");
4927
+ var DEFAULT_PROTOCOLS_CONFIG_PATH = join9(homedir6(), ".basou", "protocols.yaml");
4928
+ var DEFAULT_TARGET_PATH = join9(homedir6(), ".claude", "CLAUDE.md");
4725
4929
  var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
4726
4930
  var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
4727
4931
  function expandTilde3(p) {
4728
4932
  if (p === "~") return homedir6();
4729
- if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
4933
+ if (p.startsWith("~/")) return join9(homedir6(), p.slice(2));
4730
4934
  return p;
4731
4935
  }
4732
4936
  function isRecord3(value) {
@@ -4979,15 +5183,15 @@ import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
4979
5183
  // src/commands/refresh-watch.ts
4980
5184
  import { readdir as readdir2, stat as stat4 } from "fs/promises";
4981
5185
  import { homedir as homedir7 } from "os";
4982
- import { join as join9 } from "path";
5186
+ import { join as join10 } from "path";
4983
5187
  import { findErrorCode as findErrorCode8 } from "@basou/core";
4984
5188
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
4985
5189
  var MIN_WATCH_INTERVAL_SEC = 5;
4986
5190
  var MAX_WATCH_INTERVAL_SEC = 86400;
4987
5191
  function watchedRoots(ctx) {
4988
5192
  return [
4989
- ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions"),
4990
- ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects")
5193
+ ctx.codexSessionsDir ?? join10(homedir7(), ".codex", "sessions"),
5194
+ ctx.claudeProjectsDir ?? join10(homedir7(), ".claude", "projects")
4991
5195
  ];
4992
5196
  }
4993
5197
  async function scanSourceLogs(roots) {
@@ -5001,7 +5205,7 @@ async function scanSourceLogs(roots) {
5001
5205
  throw new Error("Failed to read a source log directory", { cause: error });
5002
5206
  }
5003
5207
  for (const entry of entries) {
5004
- const full = join9(dir, entry.name);
5208
+ const full = join10(dir, entry.name);
5005
5209
  if (entry.isDirectory()) {
5006
5210
  await walk(full);
5007
5211
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -5523,7 +5727,7 @@ function renderReviewGaps(summary) {
5523
5727
  // src/commands/run.ts
5524
5728
  import { mkdir as mkdir2 } from "fs/promises";
5525
5729
  import { homedir as homedir8 } from "os";
5526
- import { join as join10 } from "path";
5730
+ import { join as join11 } from "path";
5527
5731
  import {
5528
5732
  acquireLock as acquireLock5,
5529
5733
  assertBasouRootSafe as assertBasouRootSafe11,
@@ -5576,13 +5780,13 @@ async function runClaudeCode(args, options, ctx = {}) {
5576
5780
  await assertBasouRootSafe11(paths.root);
5577
5781
  const manifest = await readManifest7(paths);
5578
5782
  const sessionId = prefixedUlid4("ses");
5579
- const sessionDir = join10(paths.sessions, sessionId);
5783
+ const sessionDir = join11(paths.sessions, sessionId);
5580
5784
  await mkdir2(sessionDir, { recursive: true });
5581
5785
  const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
5582
5786
  await coreAppendChainedEvent2(paths, sessionId, event);
5583
5787
  });
5584
5788
  const startedAt = now().toISOString();
5585
- const sessionYamlPath = join10(sessionDir, "session.yaml");
5789
+ const sessionYamlPath = join11(sessionDir, "session.yaml");
5586
5790
  const session = buildInitialSession2({
5587
5791
  id: sessionId,
5588
5792
  command,
@@ -5925,7 +6129,7 @@ async function resolveRepositoryRootForRun(cwd) {
5925
6129
 
5926
6130
  // src/commands/session.ts
5927
6131
  import { readFile as readFile4 } from "fs/promises";
5928
- import { basename as basename6, isAbsolute as isAbsolute6, join as join11, relative as relative3 } from "path";
6132
+ import { basename as basename6, isAbsolute as isAbsolute6, join as join12, relative as relative3 } from "path";
5929
6133
  import {
5930
6134
  acquireLock as acquireLock6,
5931
6135
  appendEventToExistingSession as appendEventToExistingSession3,
@@ -5934,7 +6138,7 @@ import {
5934
6138
  enumerateSessionDirs as enumerateSessionDirs2,
5935
6139
  findErrorCode as findErrorCode11,
5936
6140
  importSessionFromJson as importSessionFromJson2,
5937
- loadSessionEntries,
6141
+ loadSessionEntries as loadSessionEntries2,
5938
6142
  readAllEvents,
5939
6143
  readManifest as readManifest8,
5940
6144
  readYamlFile as readYamlFile7,
@@ -5998,7 +6202,7 @@ async function doRunSessionList(options, ctx) {
5998
6202
  const paths = basouPaths15(repositoryRoot);
5999
6203
  await assertWorkspaceInitialized10(paths.root);
6000
6204
  const now = /* @__PURE__ */ new Date();
6001
- const records = (await loadSessionEntries(paths, {
6205
+ const records = (await loadSessionEntries2(paths, {
6002
6206
  now,
6003
6207
  onWarning: (w, sid) => printReplayWarning(w, sid),
6004
6208
  onSkip: (sid, reason) => printSessionListSkip(sid, reason)
@@ -6050,8 +6254,8 @@ async function doRunSessionShow(idInput, options, ctx) {
6050
6254
  const paths = basouPaths15(repositoryRoot);
6051
6255
  await assertWorkspaceInitialized10(paths.root);
6052
6256
  const sessionId = await resolveSessionId3(paths, idInput);
6053
- const sessionDir = join11(paths.sessions, sessionId);
6054
- const sessionYamlPath = join11(sessionDir, "session.yaml");
6257
+ const sessionDir = join12(paths.sessions, sessionId);
6258
+ const sessionYamlPath = join12(sessionDir, "session.yaml");
6055
6259
  let session;
6056
6260
  try {
6057
6261
  const raw = await readYamlFile7(sessionYamlPath);
@@ -6226,6 +6430,11 @@ function eventVariantSummary(ev) {
6226
6430
  return `approval=${ev.approval_id}`;
6227
6431
  case "decision_recorded":
6228
6432
  return ev.title;
6433
+ case "decision_voided": {
6434
+ const sup = ev.superseded_by !== void 0 ? ` superseded by ${ev.superseded_by}` : "";
6435
+ const reason = typeof ev.reason === "string" && ev.reason.length > 0 ? `: ${ev.reason}` : "";
6436
+ return `voided ${ev.decision_id}${reason}${sup}`;
6437
+ }
6229
6438
  case "task_created":
6230
6439
  return ev.title;
6231
6440
  case "task_status_changed":
@@ -6789,7 +6998,7 @@ async function resolveRepositoryRootForStatus(cwd) {
6789
6998
 
6790
6999
  // src/commands/task.ts
6791
7000
  import { readFile as readFile5 } from "fs/promises";
6792
- import { join as join12 } from "path";
7001
+ import { join as join13 } from "path";
6793
7002
  import {
6794
7003
  archiveTask,
6795
7004
  assertBasouRootSafe as assertBasouRootSafe15,
@@ -6799,7 +7008,7 @@ import {
6799
7008
  editTask,
6800
7009
  enumerateArchivedTaskIds,
6801
7010
  findErrorCode as findErrorCode14,
6802
- loadSessionEntries as loadSessionEntries2,
7011
+ loadSessionEntries as loadSessionEntries3,
6803
7012
  loadTaskEntries,
6804
7013
  prefixedUlid as prefixedUlid5,
6805
7014
  readManifest as readManifest10,
@@ -6808,7 +7017,7 @@ import {
6808
7017
  reconcileAllTasks,
6809
7018
  reconcileTask,
6810
7019
  refreshTaskLinkedSessions,
6811
- replayEvents as replayEvents2,
7020
+ replayEvents as replayEvents3,
6812
7021
  resolveRepositoryRoot as resolveRepositoryRoot12,
6813
7022
  resolveSessionId as resolveSessionId4,
6814
7023
  resolveTaskId as resolveTaskId2,
@@ -7112,13 +7321,13 @@ async function doRunTaskShow(idInput, options, ctx) {
7112
7321
  await assertWorkspaceInitialized12(paths.root);
7113
7322
  const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
7114
7323
  const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
7115
- const sessions = await loadSessionEntries2(paths, { now: /* @__PURE__ */ new Date() });
7324
+ const sessions = await loadSessionEntries3(paths, { now: /* @__PURE__ */ new Date() });
7116
7325
  const events = [];
7117
7326
  const linkedSessionIds = new Set(doc.task.task.linked_sessions);
7118
7327
  for (const s of sessions) {
7119
- const sessionDir = join12(paths.sessions, s.sessionId);
7328
+ const sessionDir = join13(paths.sessions, s.sessionId);
7120
7329
  try {
7121
- for await (const ev of replayEvents2(sessionDir, {
7330
+ for await (const ev of replayEvents3(sessionDir, {
7122
7331
  onWarning: (w) => printReplayWarning(w, s.sessionId)
7123
7332
  })) {
7124
7333
  if ((ev.type === "task_created" || ev.type === "task_status_changed" || ev.type === "task_reconciled" || ev.type === "task_linkage_refreshed" || ev.type === "task_deleted" || ev.type === "task_archived") && ev.task_id === taskId) {
@@ -8032,7 +8241,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
8032
8241
  // src/lib/portfolio-safety.ts
8033
8242
  import { execFile } from "child_process";
8034
8243
  import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
8035
- import { isAbsolute as isAbsolute7, join as join13, relative as relative4, resolve as resolve10 } from "path";
8244
+ import { isAbsolute as isAbsolute7, join as join14, relative as relative4, resolve as resolve10 } from "path";
8036
8245
  import { promisify } from "util";
8037
8246
  import { readManifest as readManifest11 } from "@basou/core";
8038
8247
  var execFileAsync = promisify(execFile);
@@ -8056,7 +8265,7 @@ function isBasouPath(p) {
8056
8265
  async function inspectRepo(repoPath) {
8057
8266
  let hasEntry = false;
8058
8267
  try {
8059
- await lstat2(join13(repoPath, ".basou"));
8268
+ await lstat2(join14(repoPath, ".basou"));
8060
8269
  hasEntry = true;
8061
8270
  } catch (error) {
8062
8271
  if (errorCode(error) !== "ENOENT") {
@@ -8157,14 +8366,14 @@ function formatSafetyReport(result) {
8157
8366
 
8158
8367
  // src/lib/view-server.ts
8159
8368
  import { createServer } from "http";
8160
- import { join as join14 } from "path";
8369
+ import { join as join15 } from "path";
8161
8370
  import {
8162
8371
  computeWorkStats as computeWorkStats2,
8163
8372
  enumerateApprovals as enumerateApprovals2,
8164
8373
  findErrorCode as findErrorCode16,
8165
8374
  isLazyExpired as isLazyExpired2,
8166
8375
  loadApproval as loadApproval2,
8167
- loadSessionEntries as loadSessionEntries3,
8376
+ loadSessionEntries as loadSessionEntries4,
8168
8377
  loadTaskEntries as loadTaskEntries2,
8169
8378
  readAllEvents as readAllEvents2,
8170
8379
  readManifest as readManifest12,
@@ -8709,6 +8918,10 @@ var VIEW_HTML = `<!doctype html>
8709
8918
  }
8710
8919
  if (ev.type === 'file_changed') return ev.path + ' [' + ev.change_type + ']';
8711
8920
  if (ev.type === 'decision_recorded') return ev.title || '';
8921
+ if (ev.type === 'decision_voided') {
8922
+ var vs = ev.superseded_by ? ' superseded by ' + ev.superseded_by : '';
8923
+ return 'voided ' + ev.decision_id + (ev.reason ? ': ' + ev.reason : '') + vs;
8924
+ }
8712
8925
  return '';
8713
8926
  }
8714
8927
 
@@ -9086,7 +9299,7 @@ async function overview(ws, nowProvider) {
9086
9299
  };
9087
9300
  }
9088
9301
  async function sessionsList(ws, nowProvider) {
9089
- const entries = await loadSessionEntries3(ws.paths, { now: nowProvider() });
9302
+ const entries = await loadSessionEntries4(ws.paths, { now: nowProvider() });
9090
9303
  const sessions = entries.map((entry) => ({
9091
9304
  sessionId: entry.sessionId,
9092
9305
  label: entry.session.session.label ?? null,
@@ -9112,7 +9325,7 @@ async function sessionDetail(ws, sessionId) {
9112
9325
  throw error;
9113
9326
  }
9114
9327
  try {
9115
- const events = await readAllEvents2(join14(ws.paths.sessions, sessionId));
9328
+ const events = await readAllEvents2(join15(ws.paths.sessions, sessionId));
9116
9329
  return { session, events };
9117
9330
  } catch {
9118
9331
  return { session, events: [], degraded: true };