@basou/cli 0.13.0 → 0.14.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
@@ -633,6 +633,9 @@ function printNoApprovals(options) {
633
633
  }
634
634
 
635
635
  // src/commands/decision.ts
636
+ import { readFile } from "fs/promises";
637
+ import { homedir } from "os";
638
+ import { resolve } from "path";
636
639
  import {
637
640
  acquireLock as acquireLock2,
638
641
  appendEventToExistingSession,
@@ -640,12 +643,34 @@ import {
640
643
  basouPaths as basouPaths2,
641
644
  createAdHocSessionWithEvent,
642
645
  findErrorCode as findErrorCode2,
646
+ isValidPrefixedId,
643
647
  prefixedUlid as prefixedUlid2,
644
648
  readManifest,
645
649
  resolveRepositoryRoot as resolveRepositoryRoot2,
646
- resolveSessionId
650
+ resolveSessionId,
651
+ sanitizePath
647
652
  } from "@basou/core";
648
653
  import { InvalidArgumentError } from "commander";
654
+
655
+ // src/lib/repo-root.ts
656
+ import { resolveBasouRepositoryRoot } from "@basou/core";
657
+ async function resolveBasouRootForCommand(cwd, commandName) {
658
+ try {
659
+ return await resolveBasouRepositoryRoot(cwd, {
660
+ onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
661
+ });
662
+ } catch (error) {
663
+ if (error instanceof Error && error.message === "Not a git repository") {
664
+ throw new Error(
665
+ `Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
666
+ { cause: error }
667
+ );
668
+ }
669
+ throw error;
670
+ }
671
+ }
672
+
673
+ // src/commands/decision.ts
649
674
  var LABEL_TITLE_MAX = 80;
650
675
  var LABEL_TRUNCATE_HEAD = LABEL_TITLE_MAX - 3;
651
676
  function registerDecisionCommand(program) {
@@ -675,7 +700,34 @@ function registerDecisionCommand(program) {
675
700
  ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
676
701
  await runDecisionRecord(options);
677
702
  });
703
+ decision.command("capture").description(
704
+ "Capture a batch of decisions from a JSON array (stdin or --file). The in-loop agent extracts a session's conversational decisions -- with rationale, alternatives, and rejected reasons -- and pipes them in; basou writes them deterministically into one ad-hoc session."
705
+ ).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) => {
706
+ await runDecisionCapture(options);
707
+ });
678
708
  }
709
+ var CAPTURE_HELP = `
710
+ Input format (a JSON array; one object per decision):
711
+ [
712
+ {
713
+ "title": "Adopt pnpm for the monorepo",
714
+ "rationale": "Workspace protocol and a content-addressed store fit our layout.",
715
+ "alternatives": ["npm workspaces", "yarn"],
716
+ "rejected_reason": "npm hoisting caused phantom-dependency bugs",
717
+ "linked_files": ["pnpm-workspace.yaml"]
718
+ }
719
+ ]
720
+
721
+ Only "title" is required; every other field is optional. All decisions are
722
+ written into one ad-hoc session timestamped now, so orientation surfaces them
723
+ as the latest decisions. Run from a workspace-view directory and it resolves to
724
+ the planning repo, like 'basou orient' / 'basou refresh' / 'basou note'.
725
+
726
+ Example (heredoc on stdin):
727
+ basou decision capture <<'JSON'
728
+ [{ "title": "Ship the capture command", "rationale": "Close the why-capture gap" }]
729
+ JSON
730
+ `;
679
731
  async function runDecisionRecord(options, ctx = {}) {
680
732
  try {
681
733
  await doRunDecisionRecord(options, ctx);
@@ -761,6 +813,259 @@ async function doRunDecisionRecord(options, ctx) {
761
813
  rich
762
814
  });
763
815
  }
816
+ async function runDecisionCapture(options, ctx = {}) {
817
+ try {
818
+ await doRunDecisionCapture(options, ctx);
819
+ } catch (error) {
820
+ renderCliError(error, {
821
+ verbose: isVerbose(options),
822
+ classifiers: [failedToFinalizeClassifier]
823
+ });
824
+ process.exitCode = 1;
825
+ }
826
+ }
827
+ async function doRunDecisionCapture(options, ctx) {
828
+ const cwd = ctx.cwd ?? process.cwd();
829
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision capture");
830
+ const paths = basouPaths2(repositoryRoot);
831
+ await assertWorkspaceInitialized2(paths.root);
832
+ const raw = await readCaptureInput(options, ctx);
833
+ const decisions = parseCaptureInput(raw);
834
+ if (options.dryRun === true) {
835
+ printCapturePreview(options, decisions);
836
+ return;
837
+ }
838
+ const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
839
+ const occurredAt = now.toISOString();
840
+ const decisionIds = decisions.map(() => prefixedUlid2("decision"));
841
+ const manifest = await readManifest(paths);
842
+ const invocationArgs = options.file !== void 0 ? [
843
+ "--file",
844
+ sanitizePath(resolve(cwd, options.file), {
845
+ workingDirectory: repositoryRoot,
846
+ homedir: homedir()
847
+ })
848
+ ] : [];
849
+ const adHoc = await createAdHocSessionWithEvent({
850
+ paths,
851
+ manifest,
852
+ label: buildCaptureLabel(decisions.length),
853
+ occurredAt,
854
+ sessionSource: "human",
855
+ workingDirectory: repositoryRoot,
856
+ invocation: {
857
+ command: "basou decision capture",
858
+ args: invocationArgs
859
+ },
860
+ targetEventBuilders: decisions.map(
861
+ (decision, index) => (sessionId, eventId) => buildDecisionEvent({
862
+ eventId,
863
+ sessionId,
864
+ decisionId: decisionIds[index],
865
+ title: decision.title,
866
+ occurredAt,
867
+ rich: toRichFields(decision)
868
+ })
869
+ )
870
+ });
871
+ printCaptureResult(options, {
872
+ sessionId: adHoc.sessionId,
873
+ items: decisions.map((decision, index) => ({
874
+ decisionId: decisionIds[index],
875
+ eventId: adHoc.targetEventIds[index],
876
+ input: decision
877
+ }))
878
+ });
879
+ }
880
+ async function readCaptureInput(options, ctx) {
881
+ if (options.file !== void 0) {
882
+ try {
883
+ return await readFile(options.file, "utf8");
884
+ } catch (error) {
885
+ if (findErrorCode2(error, "ENOENT")) {
886
+ throw new Error(`Input file not found: ${options.file}`);
887
+ }
888
+ throw error;
889
+ }
890
+ }
891
+ if (ctx.readInput !== void 0) {
892
+ return await ctx.readInput();
893
+ }
894
+ if (process.stdin.isTTY === true) {
895
+ throw new Error(NO_INPUT_HINT);
896
+ }
897
+ return await readStdinToEnd();
898
+ }
899
+ async function readStdinToEnd() {
900
+ const chunks = [];
901
+ for await (const chunk of process.stdin) {
902
+ chunks.push(Buffer.from(chunk));
903
+ }
904
+ return Buffer.concat(chunks).toString("utf8");
905
+ }
906
+ var NO_INPUT_HINT = "No input: pipe a JSON array of decisions to stdin or pass --file <path>.";
907
+ var CAPTURE_ALLOWED_KEYS = /* @__PURE__ */ new Set([
908
+ "title",
909
+ "rationale",
910
+ "rejected_reason",
911
+ "alternatives",
912
+ "linked_events",
913
+ "linked_files"
914
+ ]);
915
+ function parseCaptureInput(raw) {
916
+ if (raw.trim().length === 0) {
917
+ throw new Error(NO_INPUT_HINT);
918
+ }
919
+ let parsed;
920
+ try {
921
+ parsed = JSON.parse(raw);
922
+ } catch (error) {
923
+ const detail = error instanceof Error ? error.message : String(error);
924
+ throw new Error(`Input is not valid JSON: ${detail}`);
925
+ }
926
+ if (!Array.isArray(parsed)) {
927
+ throw new Error("Input must be a JSON array of decision objects.");
928
+ }
929
+ if (parsed.length === 0) {
930
+ throw new Error("Input array must contain at least one decision.");
931
+ }
932
+ return parsed.map((item, index) => validateCaptureItem(item, index));
933
+ }
934
+ function validateCaptureItem(item, index) {
935
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
936
+ throw new Error(`decision[${index}] must be a JSON object.`);
937
+ }
938
+ const obj = item;
939
+ for (const key of Object.keys(obj)) {
940
+ if (!CAPTURE_ALLOWED_KEYS.has(key)) {
941
+ throw new Error(
942
+ `decision[${index}]: unknown field '${key}'. Allowed: title, rationale, rejected_reason, alternatives, linked_events, linked_files.`
943
+ );
944
+ }
945
+ }
946
+ if (typeof obj.title !== "string" || isBlank(obj.title)) {
947
+ throw new Error(`decision[${index}].title must be a non-empty string.`);
948
+ }
949
+ const out = { title: obj.title };
950
+ if (obj.rationale !== void 0) {
951
+ out.rationale = requireNonEmptyString(obj.rationale, index, "rationale");
952
+ }
953
+ if (obj.rejected_reason !== void 0) {
954
+ out.rejected_reason = requireNonEmptyString(obj.rejected_reason, index, "rejected_reason");
955
+ }
956
+ if (obj.alternatives !== void 0) {
957
+ out.alternatives = validateStringArray(obj.alternatives, index, "alternatives", (value, i) => {
958
+ if (isBlank(value)) {
959
+ throw new Error(`decision[${index}].alternatives[${i}] must not be empty.`);
960
+ }
961
+ });
962
+ }
963
+ if (obj.linked_events !== void 0) {
964
+ out.linked_events = validateStringArray(
965
+ obj.linked_events,
966
+ index,
967
+ "linked_events",
968
+ (value, i) => {
969
+ if (!isValidEventId(value)) {
970
+ throw new Error(
971
+ `decision[${index}].linked_events[${i}] must match evt_<ULID>, got '${value}'.`
972
+ );
973
+ }
974
+ }
975
+ );
976
+ }
977
+ if (obj.linked_files !== void 0) {
978
+ out.linked_files = validateStringArray(obj.linked_files, index, "linked_files", (value, i) => {
979
+ if (isBlank(value)) {
980
+ throw new Error(`decision[${index}].linked_files[${i}] must not be empty.`);
981
+ }
982
+ if (value.length > 4096) {
983
+ throw new Error(`decision[${index}].linked_files[${i}] exceeds 4096 chars.`);
984
+ }
985
+ });
986
+ }
987
+ return out;
988
+ }
989
+ function requireNonEmptyString(value, index, field) {
990
+ if (typeof value !== "string" || isBlank(value)) {
991
+ throw new Error(`decision[${index}].${field} must be a non-empty string.`);
992
+ }
993
+ return value;
994
+ }
995
+ function isBlank(value) {
996
+ return value.trim().length === 0;
997
+ }
998
+ function validateStringArray(value, index, field, checkEach) {
999
+ if (!Array.isArray(value)) {
1000
+ throw new Error(`decision[${index}].${field} must be an array of strings.`);
1001
+ }
1002
+ return value.map((entry, i) => {
1003
+ if (typeof entry !== "string") {
1004
+ throw new Error(`decision[${index}].${field}[${i}] must be a string.`);
1005
+ }
1006
+ checkEach(entry, i);
1007
+ return entry;
1008
+ });
1009
+ }
1010
+ function toRichFields(decision) {
1011
+ const out = {};
1012
+ if (decision.rationale !== void 0) out.rationale = decision.rationale;
1013
+ if (decision.rejected_reason !== void 0) out.rejected_reason = decision.rejected_reason;
1014
+ if (decision.alternatives !== void 0) out.alternatives = [...decision.alternatives];
1015
+ if (decision.linked_events !== void 0) out.linked_events = [...decision.linked_events];
1016
+ if (decision.linked_files !== void 0) out.linked_files = [...decision.linked_files];
1017
+ return out;
1018
+ }
1019
+ function buildCaptureLabel(count) {
1020
+ return `Ad-hoc capture: ${count} decision${count === 1 ? "" : "s"}`;
1021
+ }
1022
+ function captureItemToPayload(item) {
1023
+ const payload = {
1024
+ decision_id: item.decisionId,
1025
+ event_id: item.eventId,
1026
+ title: item.input.title
1027
+ };
1028
+ if (item.input.rationale !== void 0) payload.rationale = item.input.rationale;
1029
+ if (item.input.alternatives !== void 0) payload.alternatives = item.input.alternatives;
1030
+ if (item.input.rejected_reason !== void 0)
1031
+ payload.rejected_reason = item.input.rejected_reason;
1032
+ if (item.input.linked_events !== void 0) payload.linked_events = item.input.linked_events;
1033
+ if (item.input.linked_files !== void 0) payload.linked_files = item.input.linked_files;
1034
+ return payload;
1035
+ }
1036
+ function printCapturePreview(options, decisions) {
1037
+ if (options.json === true) {
1038
+ console.log(JSON.stringify({ dry_run: true, count: decisions.length, decisions }));
1039
+ return;
1040
+ }
1041
+ console.log(
1042
+ `Would capture ${decisions.length} decision${decisions.length === 1 ? "" : "s"} (dry run; nothing written):`
1043
+ );
1044
+ for (const decision of decisions) {
1045
+ console.log(`- ${decision.title}`);
1046
+ }
1047
+ }
1048
+ function printCaptureResult(options, result) {
1049
+ const sid = shortSessionId(result.sessionId);
1050
+ if (options.json === true) {
1051
+ console.log(
1052
+ JSON.stringify({
1053
+ mode: "ad-hoc",
1054
+ session_id: result.sessionId,
1055
+ session_status: "completed",
1056
+ count: result.items.length,
1057
+ decisions: result.items.map(captureItemToPayload)
1058
+ })
1059
+ );
1060
+ return;
1061
+ }
1062
+ console.log(
1063
+ `Captured ${result.items.length} decision${result.items.length === 1 ? "" : "s"} in ad-hoc session ${sid}:`
1064
+ );
1065
+ for (const item of result.items) {
1066
+ console.log(`- ${item.decisionId}: ${item.input.title}`);
1067
+ }
1068
+ }
764
1069
  function pickRichFields(options) {
765
1070
  const out = {};
766
1071
  if (options.rationale !== void 0) out.rationale = options.rationale;
@@ -798,38 +1103,40 @@ function buildAdHocLabel(title) {
798
1103
  return `Ad-hoc decision: ${truncated}`;
799
1104
  }
800
1105
  function parseTitle(raw) {
801
- if (raw.length === 0) {
1106
+ if (isBlank(raw)) {
802
1107
  throw new InvalidArgumentError("Title must not be empty");
803
1108
  }
804
1109
  return raw;
805
1110
  }
806
1111
  function parseRationale(raw) {
807
- if (raw.length === 0) {
1112
+ if (isBlank(raw)) {
808
1113
  throw new InvalidArgumentError("Rationale must not be empty");
809
1114
  }
810
1115
  return raw;
811
1116
  }
812
1117
  function parseRejectedReason(raw) {
813
- if (raw.length === 0) {
1118
+ if (isBlank(raw)) {
814
1119
  throw new InvalidArgumentError("Rejected reason must not be empty");
815
1120
  }
816
1121
  return raw;
817
1122
  }
818
1123
  function collectAlternative(value, prev) {
819
- if (value.length === 0) {
1124
+ if (isBlank(value)) {
820
1125
  throw new InvalidArgumentError("Alternative must not be empty");
821
1126
  }
822
1127
  return prev.concat(value);
823
1128
  }
824
- var EVENT_ID_RE = /^evt_[A-Z0-9]+$/;
1129
+ function isValidEventId(value) {
1130
+ return isValidPrefixedId(value) && value.startsWith("evt_");
1131
+ }
825
1132
  function collectLinkedEvent(value, prev) {
826
- if (!EVENT_ID_RE.test(value)) {
1133
+ if (!isValidEventId(value)) {
827
1134
  throw new InvalidArgumentError(`Linked event id must match evt_<ULID>, got '${value}'`);
828
1135
  }
829
1136
  return prev.concat(value);
830
1137
  }
831
1138
  function collectLinkedFile(value, prev) {
832
- if (value.length === 0) {
1139
+ if (isBlank(value)) {
833
1140
  throw new InvalidArgumentError("Linked file path must not be empty");
834
1141
  }
835
1142
  if (value.length > 4096) {
@@ -960,7 +1267,7 @@ async function assertWorkspaceInitialized3(basouRoot) {
960
1267
 
961
1268
  // src/commands/exec.ts
962
1269
  import { mkdir } from "fs/promises";
963
- import { homedir } from "os";
1270
+ import { homedir as homedir2 } from "os";
964
1271
  import { join as join2 } from "path";
965
1272
  import {
966
1273
  acquireLock as acquireLock3,
@@ -1212,7 +1519,7 @@ function buildInitialSession(input) {
1212
1519
  source: { kind: "terminal", version: "0.1.0" },
1213
1520
  started_at: input.startedAt,
1214
1521
  status: "initialized",
1215
- working_directory: sanitizeWorkingDirectory(input.cwd, { homedir: homedir() }),
1522
+ working_directory: sanitizeWorkingDirectory(input.cwd, { homedir: homedir2() }),
1216
1523
  invocation: {
1217
1524
  command: input.command,
1218
1525
  args: [...input.args],
@@ -1355,9 +1662,9 @@ async function assertWorkspaceInitialized4(basouRoot) {
1355
1662
 
1356
1663
  // src/commands/import.ts
1357
1664
  import { createReadStream } from "fs";
1358
- import { readdir, readFile, rm, stat } from "fs/promises";
1359
- import { homedir as homedir2 } from "os";
1360
- import { basename, join as join3, resolve } from "path";
1665
+ import { readdir, readFile as readFile2, rm, stat } from "fs/promises";
1666
+ import { homedir as homedir3 } from "os";
1667
+ import { basename, join as join3, resolve as resolve2 } from "path";
1361
1668
  import { createInterface } from "readline";
1362
1669
  import {
1363
1670
  assertBasouRootSafe as assertBasouRootSafe6,
@@ -1425,10 +1732,10 @@ function resolveSourceRoots(args) {
1425
1732
  const { projectFlags, manifest, repoRoot, cwd } = args;
1426
1733
  let resolved;
1427
1734
  if (projectFlags.length > 0) {
1428
- resolved = projectFlags.map((p) => resolve(cwd, p));
1735
+ resolved = projectFlags.map((p) => resolve2(cwd, p));
1429
1736
  } else {
1430
1737
  const roots = manifest.import?.source_roots;
1431
- resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve(repoRoot, r)) : [repoRoot];
1738
+ resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve2(repoRoot, r)) : [repoRoot];
1432
1739
  }
1433
1740
  return [...new Set(resolved)];
1434
1741
  }
@@ -1441,8 +1748,9 @@ async function doRunImportClaudeCode(options, ctx) {
1441
1748
  repoRoot: repositoryRoot,
1442
1749
  cwd: ctx.cwd ?? process.cwd()
1443
1750
  });
1444
- const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir2(), ".claude", "projects");
1751
+ const projectsRoot = ctx.claudeProjectsDir ?? join3(homedir3(), ".claude", "projects");
1445
1752
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
1753
+ const projectSet = new Set(projectPaths);
1446
1754
  const candidates = files.map((file) => {
1447
1755
  const externalId = basename(file, ".jsonl");
1448
1756
  return {
@@ -1450,6 +1758,8 @@ async function doRunImportClaudeCode(options, ctx) {
1450
1758
  sourcePath: file,
1451
1759
  toPayload: async () => {
1452
1760
  const { records, sizeBytes } = await readJsonlRecords(file);
1761
+ const cwd = firstTranscriptCwd(records);
1762
+ if (cwd === void 0 || !projectSet.has(cwd)) return null;
1453
1763
  return claudeTranscriptToImportPayload(records, {
1454
1764
  workspaceId: manifest.workspace.id,
1455
1765
  externalId,
@@ -1469,7 +1779,7 @@ async function doRunImportCodex(options, ctx) {
1469
1779
  repoRoot: repositoryRoot,
1470
1780
  cwd: ctx.cwd ?? process.cwd()
1471
1781
  });
1472
- const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir2(), ".codex", "sessions");
1782
+ const sessionsRoot = ctx.codexSessionsDir ?? join3(homedir3(), ".codex", "sessions");
1473
1783
  const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
1474
1784
  const candidates = rollouts.map(({ file, externalId }) => ({
1475
1785
  externalId,
@@ -1623,7 +1933,14 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
1623
1933
  return prior;
1624
1934
  }
1625
1935
  function encodeProjectDir(projectPath) {
1626
- return projectPath.replaceAll("/", "-");
1936
+ return projectPath.replace(/[^a-zA-Z0-9]/g, "-");
1937
+ }
1938
+ function firstTranscriptCwd(records) {
1939
+ for (const record of records) {
1940
+ const cwd = record.cwd;
1941
+ if (typeof cwd === "string" && cwd.length > 0) return cwd;
1942
+ }
1943
+ return void 0;
1627
1944
  }
1628
1945
  async function loadExistingByExternalId(paths, sourceKind) {
1629
1946
  const byExternalId = /* @__PURE__ */ new Map();
@@ -1789,7 +2106,7 @@ async function readFirstLine(file) {
1789
2106
  async function readJsonlRecords(file) {
1790
2107
  let buffer;
1791
2108
  try {
1792
- buffer = await readFile(file);
2109
+ buffer = await readFile2(file);
1793
2110
  } catch (error) {
1794
2111
  if (findErrorCode5(error, "ENOENT")) {
1795
2112
  throw new Error("Source log not found", { cause: error });
@@ -1919,7 +2236,7 @@ async function assertWorkspaceInitialized5(basouRoot) {
1919
2236
  }
1920
2237
 
1921
2238
  // src/commands/init.ts
1922
- import { basename as basename2, relative, resolve as resolve2 } from "path";
2239
+ import { basename as basename2, relative, resolve as resolve3 } from "path";
1923
2240
  import {
1924
2241
  appendBasouGitignore,
1925
2242
  createManifest,
@@ -1966,7 +2283,7 @@ async function doRunInit(options, ctx) {
1966
2283
  repositoryUrl = await tryRemoteUrl(repositoryRoot);
1967
2284
  }
1968
2285
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
1969
- const rel = relative(repositoryRoot, resolve2(cwd, p));
2286
+ const rel = relative(repositoryRoot, resolve3(cwd, p));
1970
2287
  return rel === "" ? "." : rel;
1971
2288
  });
1972
2289
  const paths = await ensureBasouDirectory(repositoryRoot);
@@ -2020,26 +2337,6 @@ import {
2020
2337
  resolveSessionId as resolveSessionId2
2021
2338
  } from "@basou/core";
2022
2339
  import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
2023
-
2024
- // src/lib/repo-root.ts
2025
- import { resolveBasouRepositoryRoot } from "@basou/core";
2026
- async function resolveBasouRootForCommand(cwd, commandName) {
2027
- try {
2028
- return await resolveBasouRepositoryRoot(cwd, {
2029
- onRedirect: ({ via, root }) => console.error(`Resolved workspace view to ${root} (via ${via}).`)
2030
- });
2031
- } catch (error) {
2032
- if (error instanceof Error && error.message === "Not a git repository") {
2033
- throw new Error(
2034
- `Not a git repository. Run 'git init' first, then re-run 'basou ${commandName}'.`,
2035
- { cause: error }
2036
- );
2037
- }
2038
- throw error;
2039
- }
2040
- }
2041
-
2042
- // src/commands/note.ts
2043
2340
  var LABEL_BODY_MAX = 80;
2044
2341
  var LABEL_TRUNCATE_HEAD2 = LABEL_BODY_MAX - 3;
2045
2342
  function registerNoteCommand(program) {
@@ -2433,7 +2730,7 @@ import {
2433
2730
  unlinkSync,
2434
2731
  writeFileSync
2435
2732
  } from "fs";
2436
- import { basename as basename3, dirname, isAbsolute, join as join4, relative as relative2, resolve as resolve3 } from "path";
2733
+ import { basename as basename3, dirname, isAbsolute, join as join4, relative as relative2, resolve as resolve4 } from "path";
2437
2734
  import {
2438
2735
  basouPaths as basouPaths9,
2439
2736
  GENERATED_END,
@@ -2685,7 +2982,7 @@ async function runProjectAdopt(options, ctx = {}) {
2685
2982
  }
2686
2983
  }
2687
2984
  function classifySourceRoot(repositoryRoot, declaredPath) {
2688
- const absolute = resolve3(repositoryRoot, declaredPath);
2985
+ const absolute = resolve4(repositoryRoot, declaredPath);
2689
2986
  let real;
2690
2987
  try {
2691
2988
  real = realpathSync(absolute);
@@ -2787,7 +3084,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
2787
3084
  };
2788
3085
  let real;
2789
3086
  try {
2790
- real = realpathSync(resolve3(repositoryRoot, entry.path));
3087
+ real = realpathSync(resolve4(repositoryRoot, entry.path));
2791
3088
  } catch {
2792
3089
  return { ...base, reachable: false, instructionFiles: [] };
2793
3090
  }
@@ -2892,7 +3189,7 @@ function gatherRepoGitignore(repositoryRoot, entry) {
2892
3189
  };
2893
3190
  let real;
2894
3191
  try {
2895
- real = realpathSync(resolve3(repositoryRoot, entry.path));
3192
+ real = realpathSync(resolve4(repositoryRoot, entry.path));
2896
3193
  } catch {
2897
3194
  return { ...base, reachable: false, currentLines: [] };
2898
3195
  }
@@ -2913,7 +3210,7 @@ function readGitignoreLines(file) {
2913
3210
  }
2914
3211
  }
2915
3212
  function applyGitignorePlan(repositoryRoot, plan) {
2916
- const file = join4(realpathSync(resolve3(repositoryRoot, plan.path)), ".gitignore");
3213
+ const file = join4(realpathSync(resolve4(repositoryRoot, plan.path)), ".gitignore");
2917
3214
  let existing = "";
2918
3215
  try {
2919
3216
  existing = readFileSync(file, "utf8");
@@ -3025,7 +3322,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
3025
3322
  const base = { path: entry.path };
3026
3323
  let real;
3027
3324
  try {
3028
- real = realpathSync(resolve3(repositoryRoot, entry.path));
3325
+ real = realpathSync(resolve4(repositoryRoot, entry.path));
3029
3326
  } catch {
3030
3327
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
3031
3328
  }
@@ -3062,7 +3359,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
3062
3359
  function applySymlinkPlan(repositoryRoot, plan) {
3063
3360
  let real;
3064
3361
  try {
3065
- real = realpathSync(resolve3(repositoryRoot, plan.path));
3362
+ real = realpathSync(resolve4(repositoryRoot, plan.path));
3066
3363
  } catch (error) {
3067
3364
  const message = failureReason(error);
3068
3365
  return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
@@ -3207,7 +3504,7 @@ async function runProjectWorkspace(options, ctx = {}) {
3207
3504
  }
3208
3505
  }
3209
3506
  function resolveViewDir(repositoryRoot, viewPath) {
3210
- const abs = resolve3(repositoryRoot, viewPath);
3507
+ const abs = resolve4(repositoryRoot, viewPath);
3211
3508
  try {
3212
3509
  return realpathSync(abs);
3213
3510
  } catch {
@@ -3221,7 +3518,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
3221
3518
  function gatherViewRepo(repositoryRoot, viewDir, entry) {
3222
3519
  let repoReal;
3223
3520
  try {
3224
- repoReal = realpathSync(resolve3(repositoryRoot, entry.path));
3521
+ repoReal = realpathSync(resolve4(repositoryRoot, entry.path));
3225
3522
  } catch {
3226
3523
  return { path: entry.path, reachable: false };
3227
3524
  }
@@ -3273,7 +3570,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
3273
3570
  } catch {
3274
3571
  return null;
3275
3572
  }
3276
- const resolved = isAbsolute(target) ? target : resolve3(viewDir, target);
3573
+ const resolved = isAbsolute(target) ? target : resolve4(viewDir, target);
3277
3574
  try {
3278
3575
  if (rosterRealpaths.has(realpathSync(resolved))) return null;
3279
3576
  } catch {
@@ -3359,11 +3656,11 @@ async function doRunProjectWorkspace(options, ctx) {
3359
3656
  } else {
3360
3657
  const viewDir = resolveViewDir(repositoryRoot, viewPath);
3361
3658
  const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
3362
- const rosterNames = roster.map((entry) => basename3(resolve3(repositoryRoot, entry.path)));
3659
+ const rosterNames = roster.map((entry) => basename3(resolve4(repositoryRoot, entry.path)));
3363
3660
  const rosterRealpaths = /* @__PURE__ */ new Set();
3364
3661
  for (const entry of roster) {
3365
3662
  try {
3366
- rosterRealpaths.add(realpathSync(resolve3(repositoryRoot, entry.path)));
3663
+ rosterRealpaths.add(realpathSync(resolve4(repositoryRoot, entry.path)));
3367
3664
  } catch {
3368
3665
  }
3369
3666
  }
@@ -3538,7 +3835,7 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
3538
3835
  };
3539
3836
  let real;
3540
3837
  try {
3541
- real = realpathSync(resolve3(repositoryRoot, entry.path));
3838
+ real = realpathSync(resolve4(repositoryRoot, entry.path));
3542
3839
  } catch {
3543
3840
  return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
3544
3841
  }
@@ -3767,7 +4064,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
3767
4064
  };
3768
4065
  let real;
3769
4066
  try {
3770
- real = realpathSync(resolve3(repositoryRoot, target));
4067
+ real = realpathSync(resolve4(repositoryRoot, target));
3771
4068
  } catch {
3772
4069
  return empty;
3773
4070
  }
@@ -3835,7 +4132,7 @@ async function doRunProjectArchive(target, options, ctx) {
3835
4132
  const roster = manifest.repos ?? [];
3836
4133
  let targetIsAnchor = false;
3837
4134
  try {
3838
- targetIsAnchor = realpathSync(resolve3(repositoryRoot, target)) === realpathSync(repositoryRoot);
4135
+ targetIsAnchor = realpathSync(resolve4(repositoryRoot, target)) === realpathSync(repositoryRoot);
3839
4136
  } catch {
3840
4137
  targetIsAnchor = false;
3841
4138
  }
@@ -3986,7 +4283,7 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
3986
4283
  const roster = manifest.repos ?? [];
3987
4284
  let oldIsAnchor = false;
3988
4285
  try {
3989
- oldIsAnchor = realpathSync(resolve3(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
4286
+ oldIsAnchor = realpathSync(resolve4(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
3990
4287
  } catch {
3991
4288
  oldIsAnchor = false;
3992
4289
  }
@@ -4101,13 +4398,13 @@ import { assertBasouRootSafe as assertBasouRootSafe9, basouPaths as basouPaths10
4101
4398
  import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
4102
4399
 
4103
4400
  // src/lib/portfolio-config.ts
4104
- import { homedir as homedir3 } from "os";
4105
- import { isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
4401
+ import { homedir as homedir4 } from "os";
4402
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve5 } from "path";
4106
4403
  import { readYamlFile as readYamlFile3 } from "@basou/core";
4107
- var DEFAULT_PORTFOLIO_CONFIG_PATH = join5(homedir3(), ".basou", "portfolio.yaml");
4404
+ var DEFAULT_PORTFOLIO_CONFIG_PATH = join5(homedir4(), ".basou", "portfolio.yaml");
4108
4405
  function expandTilde(p) {
4109
- if (p === "~") return homedir3();
4110
- if (p.startsWith("~/")) return join5(homedir3(), p.slice(2));
4406
+ if (p === "~") return homedir4();
4407
+ if (p.startsWith("~/")) return join5(homedir4(), p.slice(2));
4111
4408
  return p;
4112
4409
  }
4113
4410
  function isRecord(value) {
@@ -4146,7 +4443,7 @@ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
4146
4443
  "Portfolio workspace paths must be absolute (or start with '~'); use --workspace for relative ad-hoc paths."
4147
4444
  );
4148
4445
  }
4149
- const abs = resolve4(expanded);
4446
+ const abs = resolve5(expanded);
4150
4447
  if (seen.has(abs)) continue;
4151
4448
  seen.add(abs);
4152
4449
  result.push(entry.label !== void 0 ? { path: abs, label: entry.label } : { path: abs });
@@ -4159,7 +4456,7 @@ async function loadPortfolioConfig(configPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
4159
4456
 
4160
4457
  // src/commands/refresh-watch.ts
4161
4458
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
4162
- import { homedir as homedir4 } from "os";
4459
+ import { homedir as homedir5 } from "os";
4163
4460
  import { join as join6 } from "path";
4164
4461
  import { findErrorCode as findErrorCode8 } from "@basou/core";
4165
4462
  var DEFAULT_WATCH_INTERVAL_SEC = 30;
@@ -4167,8 +4464,8 @@ var MIN_WATCH_INTERVAL_SEC = 5;
4167
4464
  var MAX_WATCH_INTERVAL_SEC = 86400;
4168
4465
  function watchedRoots(ctx) {
4169
4466
  return [
4170
- ctx.codexSessionsDir ?? join6(homedir4(), ".codex", "sessions"),
4171
- ctx.claudeProjectsDir ?? join6(homedir4(), ".claude", "projects")
4467
+ ctx.codexSessionsDir ?? join6(homedir5(), ".codex", "sessions"),
4468
+ ctx.claudeProjectsDir ?? join6(homedir5(), ".claude", "projects")
4172
4469
  ];
4173
4470
  }
4174
4471
  async function scanSourceLogs(roots) {
@@ -4289,19 +4586,19 @@ function parseInterval(value) {
4289
4586
  return seconds;
4290
4587
  }
4291
4588
  function abortableSleep(ms, signal) {
4292
- return new Promise((resolve8) => {
4589
+ return new Promise((resolve9) => {
4293
4590
  if (signal.aborted) {
4294
- resolve8();
4591
+ resolve9();
4295
4592
  return;
4296
4593
  }
4297
4594
  let timer;
4298
4595
  const onAbort = () => {
4299
4596
  clearTimeout(timer);
4300
- resolve8();
4597
+ resolve9();
4301
4598
  };
4302
4599
  timer = setTimeout(() => {
4303
4600
  signal.removeEventListener("abort", onAbort);
4304
- resolve8();
4601
+ resolve9();
4305
4602
  }, ms);
4306
4603
  signal.addEventListener("abort", onAbort, { once: true });
4307
4604
  });
@@ -4472,7 +4769,7 @@ function printRefreshSummary(result) {
4472
4769
  if (result.decisions.decisionCount === 0) {
4473
4770
  const hasSessions = result.handoff.status === "generated" && result.handoff.sessionCount > 0;
4474
4771
  console.log(
4475
- hasSessions ? "decisions: 0 (none auto-recorded from these sessions; record any made with 'basou decision record')" : "decisions: 0"
4772
+ hasSessions ? "decisions: 0 (none auto-recorded from these sessions; capture any made with 'basou decision capture')" : "decisions: 0"
4476
4773
  );
4477
4774
  } else {
4478
4775
  console.log(`decisions: regenerated (${result.decisions.decisionCount})`);
@@ -4500,7 +4797,7 @@ async function assertWorkspaceInitialized8(basouRoot) {
4500
4797
  }
4501
4798
 
4502
4799
  // src/commands/report.ts
4503
- import { isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
4800
+ import { isAbsolute as isAbsolute3, resolve as resolve6 } from "path";
4504
4801
  import {
4505
4802
  assertBasouRootSafe as assertBasouRootSafe10,
4506
4803
  basouPaths as basouPaths11,
@@ -4540,7 +4837,7 @@ async function doRunReportGenerate(options, ctx) {
4540
4837
  onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
4541
4838
  });
4542
4839
  if (options.out !== void 0) {
4543
- const outPath = isAbsolute3(options.out) ? options.out : resolve5(cwd, options.out);
4840
+ const outPath = isAbsolute3(options.out) ? options.out : resolve6(cwd, options.out);
4544
4841
  await writeMarkdownFile6(outPath, result.body);
4545
4842
  const { sessions, decisions, tasks } = result.data;
4546
4843
  console.error(
@@ -4703,7 +5000,7 @@ function renderReviewGaps(summary) {
4703
5000
 
4704
5001
  // src/commands/run.ts
4705
5002
  import { mkdir as mkdir2 } from "fs/promises";
4706
- import { homedir as homedir5 } from "os";
5003
+ import { homedir as homedir6 } from "os";
4707
5004
  import { join as join7 } from "path";
4708
5005
  import {
4709
5006
  acquireLock as acquireLock5,
@@ -4889,7 +5186,7 @@ async function runClaudeCode(args, options, ctx = {}) {
4889
5186
  const rawRelated = computeRelatedFiles(preSnapshot, postSnapshot, diff);
4890
5187
  const relatedFiles = sanitizeRelatedFiles(rawRelated, {
4891
5188
  workingDirectory: repoRoot,
4892
- homedir: homedir5()
5189
+ homedir: homedir6()
4893
5190
  }).sanitized;
4894
5191
  const finalStatus = decideFinalStatus2(result, signalReceived);
4895
5192
  await appendEvent(sessionDir, {
@@ -5033,7 +5330,7 @@ function buildInitialSession2(input) {
5033
5330
  source: { ...claudeCodeAdapterMetadata },
5034
5331
  started_at: input.startedAt,
5035
5332
  status: "initialized",
5036
- working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir5() }),
5333
+ working_directory: sanitizeWorkingDirectory2(input.cwd, { homedir: homedir6() }),
5037
5334
  invocation: {
5038
5335
  command: input.command,
5039
5336
  args: [...input.args],
@@ -5105,7 +5402,7 @@ async function resolveRepositoryRootForRun(cwd) {
5105
5402
  }
5106
5403
 
5107
5404
  // src/commands/session.ts
5108
- import { readFile as readFile2 } from "fs/promises";
5405
+ import { readFile as readFile3 } from "fs/promises";
5109
5406
  import { basename as basename4, isAbsolute as isAbsolute4, join as join8, relative as relative3 } from "path";
5110
5407
  import {
5111
5408
  acquireLock as acquireLock6,
@@ -5545,7 +5842,7 @@ async function doRunSessionImport(options, ctx) {
5545
5842
  }
5546
5843
  async function readInputFile(path) {
5547
5844
  try {
5548
- return await readFile2(path, "utf8");
5845
+ return await readFile3(path, "utf8");
5549
5846
  } catch (error) {
5550
5847
  if (findErrorCode11(error, "ENOENT")) {
5551
5848
  throw new Error("Import source not found", { cause: error });
@@ -5662,7 +5959,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
5662
5959
  }
5663
5960
  async function readNoteFile(path) {
5664
5961
  try {
5665
- return await readFile2(path, "utf8");
5962
+ return await readFile3(path, "utf8");
5666
5963
  } catch (error) {
5667
5964
  if (findErrorCode11(error, "ENOENT")) {
5668
5965
  throw new Error("Note source not found", { cause: error });
@@ -5969,7 +6266,7 @@ async function resolveRepositoryRootForStatus(cwd) {
5969
6266
  }
5970
6267
 
5971
6268
  // src/commands/task.ts
5972
- import { readFile as readFile3 } from "fs/promises";
6269
+ import { readFile as readFile4 } from "fs/promises";
5973
6270
  import { join as join9 } from "path";
5974
6271
  import {
5975
6272
  archiveTask,
@@ -6985,7 +7282,7 @@ function parsePositiveInt2(raw) {
6985
7282
  }
6986
7283
  async function readDescriptionFile(path) {
6987
7284
  try {
6988
- return await readFile3(path, "utf8");
7285
+ return await readFile4(path, "utf8");
6989
7286
  } catch (error) {
6990
7287
  if (findErrorCode14(error, "ENOENT")) {
6991
7288
  throw new Error("Description source not found", { cause: error });
@@ -7200,7 +7497,7 @@ async function assertWorkspaceInitialized13(basouRoot) {
7200
7497
  // src/commands/view.ts
7201
7498
  import { spawn } from "child_process";
7202
7499
  import { createHash } from "crypto";
7203
- import { basename as basename5, resolve as resolve7 } from "path";
7500
+ import { basename as basename5, resolve as resolve8 } from "path";
7204
7501
  import {
7205
7502
  assertBasouRootSafe as assertBasouRootSafe17,
7206
7503
  basouPaths as basouPaths19,
@@ -7213,7 +7510,7 @@ import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
7213
7510
  // src/lib/portfolio-safety.ts
7214
7511
  import { execFile } from "child_process";
7215
7512
  import { lstat, realpath } from "fs/promises";
7216
- import { isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve6 } from "path";
7513
+ import { isAbsolute as isAbsolute5, join as join10, relative as relative4, resolve as resolve7 } from "path";
7217
7514
  import { promisify } from "util";
7218
7515
  import { readManifest as readManifest10 } from "@basou/core";
7219
7516
  var execFileAsync = promisify(execFile);
@@ -7224,7 +7521,7 @@ async function canonical(p) {
7224
7521
  try {
7225
7522
  return await realpath(p);
7226
7523
  } catch {
7227
- return resolve6(p);
7524
+ return resolve7(p);
7228
7525
  }
7229
7526
  }
7230
7527
  function isInside(child, parent) {
@@ -7286,7 +7583,7 @@ async function checkPortfolioSafety(workspaces) {
7286
7583
  }
7287
7584
  const monitored = /* @__PURE__ */ new Map();
7288
7585
  for (const root of sourceRoots) {
7289
- const display = resolve6(ws.repoRoot, root);
7586
+ const display = resolve7(ws.repoRoot, root);
7290
7587
  const real = await canonical(display);
7291
7588
  if (real !== wsReal) monitored.set(real, display);
7292
7589
  }
@@ -7995,7 +8292,7 @@ function startViewServer(opts) {
7995
8292
  };
7996
8293
  let boundPort = port;
7997
8294
  const getPort = () => boundPort;
7998
- return new Promise((resolve8, reject) => {
8295
+ return new Promise((resolve9, reject) => {
7999
8296
  const server = createServer((req, res) => {
8000
8297
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
8001
8298
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -8006,7 +8303,7 @@ function startViewServer(opts) {
8006
8303
  const address = server.address();
8007
8304
  boundPort = isAddressInfo(address) ? address.port : port;
8008
8305
  server.off("error", reject);
8009
- resolve8({
8306
+ resolve9({
8010
8307
  url: `http://${host}:${boundPort}`,
8011
8308
  port: boundPort,
8012
8309
  close: () => closeServer(server)
@@ -8018,8 +8315,8 @@ function isAddressInfo(value) {
8018
8315
  return value !== null && typeof value === "object";
8019
8316
  }
8020
8317
  function closeServer(server) {
8021
- return new Promise((resolve8) => {
8022
- server.close(() => resolve8());
8318
+ return new Promise((resolve9) => {
8319
+ server.close(() => resolve9());
8023
8320
  server.closeAllConnections();
8024
8321
  });
8025
8322
  }
@@ -8513,12 +8810,12 @@ async function buildSingleDeps(ctx, cwd) {
8513
8810
  return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
8514
8811
  }
8515
8812
  async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
8516
- const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve7(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
8813
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve8(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
8517
8814
  const entries = [];
8518
8815
  const seenPath = /* @__PURE__ */ new Set();
8519
8816
  const seenKey = /* @__PURE__ */ new Set();
8520
8817
  for (const spec of specs) {
8521
- const repoRoot = resolve7(spec.path);
8818
+ const repoRoot = resolve8(spec.path);
8522
8819
  if (seenPath.has(repoRoot)) continue;
8523
8820
  seenPath.add(repoRoot);
8524
8821
  const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
@@ -8590,7 +8887,7 @@ function openInBrowser(url, override) {
8590
8887
  }
8591
8888
  }
8592
8889
  function waitForShutdown(signal) {
8593
- return new Promise((resolve8) => {
8890
+ return new Promise((resolve9) => {
8594
8891
  const cleanup = () => {
8595
8892
  process.off("SIGINT", onSignal);
8596
8893
  process.off("SIGTERM", onSignal);
@@ -8598,18 +8895,18 @@ function waitForShutdown(signal) {
8598
8895
  };
8599
8896
  const onSignal = () => {
8600
8897
  cleanup();
8601
- resolve8();
8898
+ resolve9();
8602
8899
  };
8603
8900
  const onAbort = () => {
8604
8901
  cleanup();
8605
- resolve8();
8902
+ resolve9();
8606
8903
  };
8607
8904
  process.on("SIGINT", onSignal);
8608
8905
  process.on("SIGTERM", onSignal);
8609
8906
  if (signal !== void 0) {
8610
8907
  if (signal.aborted) {
8611
8908
  cleanup();
8612
- resolve8();
8909
+ resolve9();
8613
8910
  return;
8614
8911
  }
8615
8912
  signal.addEventListener("abort", onAbort);