@orquentra/codebuddy 2.8.0 → 2.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.
Files changed (73) hide show
  1. package/CLAUDE_CODEX.md +33 -0
  2. package/README.md +27 -3
  3. package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
  4. package/dist/{chunk-GKNYMNFM.js → chunk-4DKYKDHI.js} +6 -1
  5. package/dist/chunk-4DKYKDHI.js.map +1 -0
  6. package/dist/{chunk-6KGYAFG7.js → chunk-7QMHIJBA.js} +30 -3
  7. package/dist/chunk-7QMHIJBA.js.map +1 -0
  8. package/dist/{chunk-6NZEZJYD.js → chunk-ESC4Y3L7.js} +3 -5
  9. package/dist/{chunk-6NZEZJYD.js.map → chunk-ESC4Y3L7.js.map} +1 -1
  10. package/dist/chunk-GQKWUHMA.js +20 -0
  11. package/dist/chunk-GQKWUHMA.js.map +1 -0
  12. package/dist/{chunk-J4T6HHI4.js → chunk-L6BRYZ6G.js} +537 -316
  13. package/dist/chunk-L6BRYZ6G.js.map +1 -0
  14. package/dist/{chunk-UK6XTS2Y.js → chunk-LRQNPYCK.js} +13 -19
  15. package/dist/chunk-LRQNPYCK.js.map +1 -0
  16. package/dist/{chunk-GXOML4UZ.js → chunk-LW75SKZR.js} +2 -2
  17. package/dist/{chunk-EJQVGFH2.js → chunk-UOTKHSQ5.js} +26 -9
  18. package/dist/chunk-UOTKHSQ5.js.map +1 -0
  19. package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
  20. package/dist/chunk-V7PT6ER5.js.map +1 -0
  21. package/dist/{chunk-ZNLSCV3J.js → chunk-XSNW6Y3X.js} +1253 -315
  22. package/dist/chunk-XSNW6Y3X.js.map +1 -0
  23. package/dist/{chunk-6DONJW3W.js → chunk-YJPCS2GP.js} +274 -69
  24. package/dist/chunk-YJPCS2GP.js.map +1 -0
  25. package/dist/{chunk-XL2HLACE.js → chunk-YN7TEIUH.js} +1 -1
  26. package/dist/chunk-YN7TEIUH.js.map +1 -0
  27. package/dist/{chunk-TLWGZVPC.js → chunk-YNSTIMCF.js} +746 -66
  28. package/dist/chunk-YNSTIMCF.js.map +1 -0
  29. package/dist/{chunk-NY2YXP3R.js → chunk-YWDH6DQ6.js} +70 -4
  30. package/dist/chunk-YWDH6DQ6.js.map +1 -0
  31. package/dist/cli/index.js +23 -9
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
  34. package/dist/index.d.ts +6 -4
  35. package/dist/index.js +9 -9
  36. package/dist/install-EU35ERK5.js +30 -0
  37. package/dist/langgraph/index.d.ts +1 -1
  38. package/dist/lean-43ADYTBW.js +28 -0
  39. package/dist/{main-QUASSFP7.js → main-SFC5LV4E.js} +1333 -437
  40. package/dist/main-SFC5LV4E.js.map +1 -0
  41. package/dist/{operations-WJR7AZZF.js → operations-6Z6ZDVBP.js} +3 -3
  42. package/dist/{pretool-command-JTKX6G3M.js → pretool-command-B5GY5T6E.js} +3 -3
  43. package/dist/prompt-gate-TDTFFQW2.js +8 -0
  44. package/dist/{runtime-OQQ22VJF.js → runtime-W5AW32SD.js} +15 -7
  45. package/dist/{summaries-ML53R7EL.js → summaries-32PHOB6L.js} +3 -4
  46. package/dist/{zvec-XDZNZ2OL.js → zvec-24HLAFGN.js} +3 -4
  47. package/dist/zvec-24HLAFGN.js.map +1 -0
  48. package/package.json +11 -2
  49. package/dist/chunk-6DONJW3W.js.map +0 -1
  50. package/dist/chunk-6KGYAFG7.js.map +0 -1
  51. package/dist/chunk-EJQVGFH2.js.map +0 -1
  52. package/dist/chunk-GKNYMNFM.js.map +0 -1
  53. package/dist/chunk-ITQO632W.js +0 -673
  54. package/dist/chunk-ITQO632W.js.map +0 -1
  55. package/dist/chunk-J4T6HHI4.js.map +0 -1
  56. package/dist/chunk-NY2YXP3R.js.map +0 -1
  57. package/dist/chunk-TLWGZVPC.js.map +0 -1
  58. package/dist/chunk-UK6XTS2Y.js.map +0 -1
  59. package/dist/chunk-W7GAXL6C.js.map +0 -1
  60. package/dist/chunk-XL2HLACE.js.map +0 -1
  61. package/dist/chunk-ZNLSCV3J.js.map +0 -1
  62. package/dist/install-BXM56QCD.js +0 -19
  63. package/dist/lean-command-7DFW32MA.js +0 -19
  64. package/dist/main-QUASSFP7.js.map +0 -1
  65. /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
  66. /package/dist/{chunk-GXOML4UZ.js.map → chunk-LW75SKZR.js.map} +0 -0
  67. /package/dist/{install-BXM56QCD.js.map → install-EU35ERK5.js.map} +0 -0
  68. /package/dist/{lean-command-7DFW32MA.js.map → lean-43ADYTBW.js.map} +0 -0
  69. /package/dist/{operations-WJR7AZZF.js.map → operations-6Z6ZDVBP.js.map} +0 -0
  70. /package/dist/{pretool-command-JTKX6G3M.js.map → pretool-command-B5GY5T6E.js.map} +0 -0
  71. /package/dist/{runtime-OQQ22VJF.js.map → prompt-gate-TDTFFQW2.js.map} +0 -0
  72. /package/dist/{summaries-ML53R7EL.js.map → runtime-W5AW32SD.js.map} +0 -0
  73. /package/dist/{zvec-XDZNZ2OL.js.map → summaries-32PHOB6L.js.map} +0 -0
@@ -1,15 +1,18 @@
1
+ import {
2
+ recordTrace
3
+ } from "./chunk-YN7TEIUH.js";
1
4
  import {
2
5
  SessionCapsuleLedger,
3
6
  detectTestCommand,
4
7
  estimateTokens,
5
8
  findCoveringTest
6
- } from "./chunk-GKNYMNFM.js";
7
- import {
8
- recordTrace
9
- } from "./chunk-XL2HLACE.js";
9
+ } from "./chunk-4DKYKDHI.js";
10
10
  import {
11
11
  hookRoot
12
12
  } from "./chunk-TGFTKWOR.js";
13
+ import {
14
+ skipPromptRetrieval
15
+ } from "./chunk-GQKWUHMA.js";
13
16
  import {
14
17
  MEMORY_CLASSES,
15
18
  memoryCandidateSchema
@@ -21,24 +24,25 @@ import {
21
24
  isEligibleMemory,
22
25
  lexicalTerms,
23
26
  termOverlap
24
- } from "./chunk-6KGYAFG7.js";
27
+ } from "./chunk-7QMHIJBA.js";
25
28
  import {
26
29
  generateEntityId
27
30
  } from "./chunk-CAEPHLHC.js";
28
31
  import {
32
+ NON_SOURCE_PATH,
29
33
  TASK_PATH_LIMIT,
34
+ TEST_PATH,
30
35
  countTerms,
31
36
  discoverTaskPaths,
37
+ hashContent,
32
38
  identifierTokens,
39
+ loadFreshIndex,
33
40
  loadProjectArchitectureMap,
34
41
  namedSegments,
35
42
  readBoundedSource,
36
43
  syncFileVectors,
37
44
  taskTerms
38
- } from "./chunk-TLWGZVPC.js";
39
- import {
40
- loadFreshIndex
41
- } from "./chunk-ITQO632W.js";
45
+ } from "./chunk-YNSTIMCF.js";
42
46
  import {
43
47
  extractSymbolTable,
44
48
  supportsSymbols
@@ -55,7 +59,8 @@ import {
55
59
  scanSensitive
56
60
  } from "./chunk-AL6VBTNA.js";
57
61
  import {
58
- DEFAULT_RERANK_MODELS
62
+ DEFAULT_RERANK_MODELS,
63
+ LOCAL_EMBEDDING_MODEL
59
64
  } from "./chunk-HPOR6YXA.js";
60
65
  import {
61
66
  loadPlanPolicy,
@@ -70,14 +75,14 @@ import {
70
75
  } from "./chunk-CG5B3LYG.js";
71
76
 
72
77
  // src/hooks/runtime.ts
73
- import { mkdir as mkdir5, readFile as readFile7, rm as rm4, stat as stat4, writeFile } from "fs/promises";
74
- import { basename, dirname as dirname5, isAbsolute, join as join7, relative as relative5, resolve as resolve9, sep as sep5 } from "path";
75
- import pTimeout2 from "p-timeout";
78
+ import { mkdir as mkdir7, readFile as readFile10, rm as rm5, stat as stat4, writeFile } from "fs/promises";
79
+ import { basename, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative6, resolve as resolve10, sep as sep6 } from "path";
80
+ import pTimeout3 from "p-timeout";
76
81
 
77
82
  // src/context/engine.ts
78
- import { execFile } from "child_process";
79
- import { join as join4, resolve as resolve5 } from "path";
80
- import { promisify } from "util";
83
+ import { execFile as execFile2 } from "child_process";
84
+ import { join as join4, resolve as resolve6 } from "path";
85
+ import { promisify as promisify2 } from "util";
81
86
 
82
87
  // src/core/plan-file-store.ts
83
88
  import { mkdir, open, readdir, readFile, rm, stat } from "fs/promises";
@@ -308,7 +313,7 @@ function formatIssues(error) {
308
313
  });
309
314
  }
310
315
  function delay(ms) {
311
- return new Promise((resolve10) => setTimeout(resolve10, ms));
316
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
312
317
  }
313
318
  async function releaseOwnedLock(lockPath) {
314
319
  try {
@@ -543,7 +548,7 @@ async function gitLogForPath(repositoryRoot, path, range) {
543
548
  }).filter((record) => record.sha.length > 0);
544
549
  }
545
550
  function runGit(cwd, args) {
546
- return new Promise((resolve10, reject) => {
551
+ return new Promise((resolve11, reject) => {
547
552
  const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
548
553
  let stdout = "";
549
554
  let stderr = "";
@@ -554,7 +559,7 @@ function runGit(cwd, args) {
554
559
  stderr += String(chunk);
555
560
  });
556
561
  child.on("error", reject);
557
- child.on("close", (code) => resolve10({ code: code ?? 0, stdout, stderr }));
562
+ child.on("close", (code) => resolve11({ code: code ?? 0, stdout, stderr }));
558
563
  });
559
564
  }
560
565
 
@@ -692,13 +697,13 @@ function escapeRegExp(value) {
692
697
  // src/risk/service.ts
693
698
  async function assessRisk(input) {
694
699
  const repositoryRoot = resolve3(input.repositoryRoot ?? process.cwd());
695
- const paths = await resolveRiskPaths({
700
+ const paths2 = await resolveRiskPaths({
696
701
  repositoryRoot,
697
702
  ...input.planId !== void 0 ? { planId: input.planId } : {},
698
703
  ...input.paths !== void 0 ? { paths: input.paths } : {},
699
704
  ...input.useGit !== void 0 ? { useGit: input.useGit } : {}
700
705
  });
701
- if (paths.length === 0) {
706
+ if (paths2.length === 0) {
702
707
  return {
703
708
  items: [],
704
709
  stats: { pathsConsidered: 0, itemsProduced: 0, signalsRun: 0, perSignal: [] }
@@ -718,7 +723,7 @@ async function assessRisk(input) {
718
723
  ]
719
724
  ];
720
725
  const assessment = await new RiskAssessor(signals).assess({
721
- paths,
726
+ paths: paths2,
722
727
  repositoryRoot,
723
728
  namespace: input.namespace
724
729
  });
@@ -728,30 +733,30 @@ async function assessRisk(input) {
728
733
  };
729
734
  }
730
735
  async function resolveRiskPaths(input) {
731
- const paths = /* @__PURE__ */ new Set();
736
+ const paths2 = /* @__PURE__ */ new Set();
732
737
  for (const path of input.paths ?? []) {
733
738
  if (!path.trim()) continue;
734
739
  const contained = containedRelativePath(input.repositoryRoot, path);
735
- if (contained) paths.add(contained);
740
+ if (contained) paths2.add(contained);
736
741
  }
737
742
  if (input.planId) {
738
743
  const plan = await new PlanFileStore(input.repositoryRoot).readPlan(input.planId);
739
- for (const file of plan.filesToTouch) {
740
- const contained = containedRelativePath(input.repositoryRoot, file.path);
741
- if (contained) paths.add(contained);
744
+ for (const file2 of plan.filesToTouch) {
745
+ const contained = containedRelativePath(input.repositoryRoot, file2.path);
746
+ if (contained) paths2.add(contained);
742
747
  }
743
748
  }
744
- if (input.useGit !== false && (input.useGit || paths.size === 0)) {
749
+ if (input.useGit !== false && (input.useGit || paths2.size === 0)) {
745
750
  for (const path of await changedGitPaths(input.repositoryRoot)) {
746
751
  const contained = containedRelativePath(input.repositoryRoot, path);
747
- if (contained) paths.add(contained);
752
+ if (contained) paths2.add(contained);
748
753
  }
749
754
  }
750
755
  const realRoot = await realpath2(input.repositoryRoot).catch(() => resolve3(input.repositoryRoot));
751
756
  const inside = await Promise.all(
752
- [...paths].map((path) => realPathInside(realRoot, resolve3(input.repositoryRoot, path)))
757
+ [...paths2].map((path) => realPathInside(realRoot, resolve3(input.repositoryRoot, path)))
753
758
  );
754
- const resolved = [...paths].filter((_, index) => inside[index]).sort();
759
+ const resolved = [...paths2].filter((_, index) => inside[index]).sort();
755
760
  return resolved.length > MAX_RESOLVED_PATHS ? resolved.slice(0, MAX_RESOLVED_PATHS) : resolved;
756
761
  }
757
762
  var MAX_RESOLVED_PATHS = 200;
@@ -999,10 +1004,64 @@ var PlanLifecycle = class {
999
1004
 
1000
1005
  // src/worklog/store.ts
1001
1006
  import { createHash } from "crypto";
1002
- import { mkdir as mkdir2, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
1003
- import { dirname as dirname3, join as join3, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
1007
+ import { mkdir as mkdir2, readdir as readdir2, readFile as readFile5 } from "fs/promises";
1008
+ import { dirname as dirname3, join as join3, resolve as resolve5 } from "path";
1004
1009
  import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
1005
1010
  import { z as z3 } from "zod";
1011
+
1012
+ // src/core/freshness.ts
1013
+ import { execFile } from "child_process";
1014
+ import { readFile as readFile4 } from "fs/promises";
1015
+ import { relative as relative3, resolve as resolve4, sep as sep3 } from "path";
1016
+ import { promisify } from "util";
1017
+ async function hashFile(root, path) {
1018
+ const absolute = resolve4(root, path);
1019
+ const rel = relative3(resolve4(root), absolute);
1020
+ if (rel === ".." || rel.startsWith(`..${sep3}`)) return null;
1021
+ try {
1022
+ return hashContent(await readFile4(absolute, "utf8")).slice(0, 16);
1023
+ } catch {
1024
+ return null;
1025
+ }
1026
+ }
1027
+ async function fileHashes(root, paths2) {
1028
+ const entries = await Promise.all(
1029
+ paths2.map(async (path) => [path, await hashFile(root, path)])
1030
+ );
1031
+ return Object.fromEntries(entries);
1032
+ }
1033
+ async function compareFreshness(root, paths2, recorded) {
1034
+ const result = { current: [], changed: [], missing: [], unknown: [] };
1035
+ for (const path of paths2) {
1036
+ const now = await hashFile(root, path);
1037
+ const then = recorded?.[path];
1038
+ if (now === null) result.missing.push(path);
1039
+ else if (then === void 0) result.unknown.push(path);
1040
+ else if (then === now) result.current.push(path);
1041
+ else result.changed.push(path);
1042
+ }
1043
+ return result;
1044
+ }
1045
+ var run = promisify(execFile);
1046
+ async function repoIdentity(root) {
1047
+ const git = async (...args) => {
1048
+ try {
1049
+ const { stdout } = await run("git", args, { cwd: root, timeout: 2e3 });
1050
+ return stdout.trim() || null;
1051
+ } catch {
1052
+ return null;
1053
+ }
1054
+ };
1055
+ const [worktree, gitDir, branch, head] = await Promise.all([
1056
+ git("rev-parse", "--show-toplevel"),
1057
+ git("rev-parse", "--absolute-git-dir"),
1058
+ git("rev-parse", "--abbrev-ref", "HEAD"),
1059
+ git("rev-parse", "HEAD")
1060
+ ]);
1061
+ return { worktree, gitDir, branch, head };
1062
+ }
1063
+
1064
+ // src/worklog/store.ts
1006
1065
  var LIMITS = {
1007
1066
  summaryChars: 600,
1008
1067
  files: 50,
@@ -1023,13 +1082,16 @@ var entrySchema = z3.object({
1023
1082
  verification: z3.array(z3.string()).default([]),
1024
1083
  savedMemory: z3.array(z3.string()).default([]),
1025
1084
  queuedForReview: z3.array(z3.string()).default([]),
1026
- fingerprint: z3.string().regex(/^[a-f0-9]{64}$/)
1085
+ fingerprint: z3.string().regex(/^[a-f0-9]{64}$/),
1086
+ /** Content hash per file when recorded (absent in entries written before 2.10). */
1087
+ fileHashes: z3.record(z3.string(), z3.string().nullable()).optional(),
1088
+ branch: z3.string().nullable().optional()
1027
1089
  });
1028
1090
  var WorklogStore = class {
1029
1091
  repositoryRoot;
1030
1092
  directory;
1031
1093
  constructor(repositoryRoot = process.cwd()) {
1032
- this.repositoryRoot = resolve4(repositoryRoot);
1094
+ this.repositoryRoot = resolve5(repositoryRoot);
1033
1095
  this.directory = join3(this.repositoryRoot, ".codebuddy", "worklog");
1034
1096
  }
1035
1097
  /**
@@ -1051,6 +1113,10 @@ var WorklogStore = class {
1051
1113
  ).digest("hex");
1052
1114
  const existing = (await this.list()).find((entry) => entry.fingerprint === fingerprint);
1053
1115
  if (existing) return existing.id;
1116
+ const [hashes, identity] = await Promise.all([
1117
+ fileHashes(this.repositoryRoot, files),
1118
+ repoIdentity(this.repositoryRoot)
1119
+ ]);
1054
1120
  const id = generateEntityId("wlg", now.getTime());
1055
1121
  const path = this.entryPath(id);
1056
1122
  await assertNoSymlinkBetween(this.repositoryRoot, dirname3(path));
@@ -1068,7 +1134,9 @@ var WorklogStore = class {
1068
1134
  verification,
1069
1135
  savedMemory: input.savedMemory ?? [],
1070
1136
  queuedForReview: input.queuedForReview ?? [],
1071
- fingerprint
1137
+ fingerprint,
1138
+ fileHashes: hashes,
1139
+ branch: identity.branch
1072
1140
  });
1073
1141
  await writeAtomic(path, composeMarkdown(frontMatter, summary));
1074
1142
  return id;
@@ -1076,7 +1144,7 @@ var WorklogStore = class {
1076
1144
  async read(id) {
1077
1145
  const path = this.entryPath(id);
1078
1146
  await assertNoSymlinkBetween(this.repositoryRoot, path);
1079
- const parsed = parseFrontMatter(await readFile4(path, "utf8"));
1147
+ const parsed = parseFrontMatter(await readFile5(path, "utf8"));
1080
1148
  return {
1081
1149
  ...entrySchema.parse(parseYaml3(parsed.frontMatter)),
1082
1150
  summary: parsed.content.trim(),
@@ -1116,11 +1184,11 @@ var WorklogStore = class {
1116
1184
  const now = input.now ?? /* @__PURE__ */ new Date();
1117
1185
  const limit = input.limit ?? 3;
1118
1186
  const entries = await this.list(input.namespace);
1119
- const paths = new Set((input.paths ?? []).map(normalizePath3));
1187
+ const paths2 = new Set((input.paths ?? []).map(normalizePath3));
1120
1188
  const taskWords = words(input.task ?? "");
1121
- const targeted = paths.size > 0 || taskWords.size > 0;
1189
+ const targeted = paths2.size > 0 || taskWords.size > 0;
1122
1190
  const ranked = entries.map((entry, recency) => {
1123
- const fileHits = entry.files.filter((file) => paths.has(file));
1191
+ const fileHits = entry.files.filter((file2) => paths2.has(file2));
1124
1192
  const summaryWords = words(entry.summary);
1125
1193
  const wordHits = [...taskWords].filter((word) => summaryWords.has(word));
1126
1194
  const score = fileHits.length * 3 + Math.min(wordHits.length, 5);
@@ -1151,20 +1219,12 @@ var WorklogStore = class {
1151
1219
  );
1152
1220
  }
1153
1221
  async freshness(entry) {
1154
- const createdAt = Date.parse(entry.createdAt);
1155
- const changedSince = [];
1156
- const missing = [];
1157
- for (const file of entry.files) {
1158
- const absolute = resolve4(this.repositoryRoot, file);
1159
- const rel = relative3(this.repositoryRoot, absolute);
1160
- if (rel.startsWith(`..${sep3}`) || rel === "..") continue;
1161
- try {
1162
- if ((await stat2(absolute)).mtimeMs > createdAt) changedSince.push(file);
1163
- } catch {
1164
- missing.push(file);
1165
- }
1166
- }
1167
- return { changedSince, missing };
1222
+ const { changed, missing, unknown } = await compareFreshness(
1223
+ this.repositoryRoot,
1224
+ entry.files,
1225
+ entry.fileHashes
1226
+ );
1227
+ return { changedSince: changed, missing, unknown };
1168
1228
  }
1169
1229
  entryPath(id) {
1170
1230
  if (!/^wlg_[a-z0-9]+$/.test(id)) throw new Error(`Invalid worklog id: ${id}`);
@@ -1206,6 +1266,7 @@ function enforceContextBudget(input) {
1206
1266
  ["facts", 40],
1207
1267
  ["rules", 45],
1208
1268
  ["symbols", 50],
1269
+ ["reuse", 75],
1209
1270
  ["policyRules", 90],
1210
1271
  ["incidents", 80]
1211
1272
  ]) {
@@ -1269,6 +1330,7 @@ function enforceContextBudget(input) {
1269
1330
  if (account(output) <= output.tokens.budget) continue;
1270
1331
  remove();
1271
1332
  if (candidate.essential) output.tokens.essentialOmitted++;
1333
+ if (candidate.section === "reuse") continue;
1272
1334
  const smaller = candidate.essential ? compactSafety(candidate.value) : compact(candidate.value, 100);
1273
1335
  insert(smaller);
1274
1336
  if (account(output) <= output.tokens.budget) continue;
@@ -1339,9 +1401,9 @@ var LIMITS2 = {
1339
1401
  var DEFAULT_TOKEN_BUDGET = 4e3;
1340
1402
  var MAX_DIFF_FILES = 8;
1341
1403
  var MAX_DIFF_CHARS_PER_FILE = 2400;
1342
- var execFileAsync = promisify(execFile);
1404
+ var execFileAsync = promisify2(execFile2);
1343
1405
  async function buildBootstrapContext(input) {
1344
- const repositoryRoot = resolve5(input.repositoryRoot ?? process.cwd());
1406
+ const repositoryRoot = resolve6(input.repositoryRoot ?? process.cwd());
1345
1407
  const namespace = input.namespace;
1346
1408
  const planStore = new PlanFileStore(repositoryRoot);
1347
1409
  const memoryStore = new MemoryFileStore(repositoryRoot);
@@ -1400,7 +1462,7 @@ async function buildBootstrapContext(input) {
1400
1462
  return enforceContextBudget({ ...assembled, tokens });
1401
1463
  }
1402
1464
  async function buildBeforeEditContext(input) {
1403
- const repositoryRoot = resolve5(input.repositoryRoot ?? process.cwd());
1465
+ const repositoryRoot = resolve6(input.repositoryRoot ?? process.cwd());
1404
1466
  const namespace = input.namespace;
1405
1467
  const hasExplicitTargets = (input.paths?.length ?? 0) > 0 || input.planId !== void 0;
1406
1468
  const fresh = await loadFreshIndex(repositoryRoot);
@@ -1482,7 +1544,7 @@ async function buildBeforeEditContext(input) {
1482
1544
  knownSecrets
1483
1545
  );
1484
1546
  if (symbols.length > 0) {
1485
- const spans = symbols.reduce((total, file) => total + (file.spans?.length ?? 0), 0);
1547
+ const spans = symbols.reduce((total, file2) => total + (file2.spans?.length ?? 0), 0);
1486
1548
  explain.push(
1487
1549
  `symbols: signatures for ${symbols.length} target file(s) (in place of full source)` + (spans > 0 ? `; ${spans} task-ranked span(s) read from current source` : "")
1488
1550
  );
@@ -1556,7 +1618,7 @@ function summarizePlan(plan) {
1556
1618
  title: plan.title,
1557
1619
  status: plan.status,
1558
1620
  brief: plan.brief,
1559
- filesToTouch: plan.filesToTouch.map((file) => file.path),
1621
+ filesToTouch: plan.filesToTouch.map((file2) => file2.path),
1560
1622
  risks: plan.risks
1561
1623
  };
1562
1624
  }
@@ -1596,9 +1658,9 @@ function summarizePolicyRules(rules) {
1596
1658
  weight: rule.weight
1597
1659
  }));
1598
1660
  }
1599
- function summarizeMatchingPolicyRules(rules, paths) {
1661
+ function summarizeMatchingPolicyRules(rules, paths2) {
1600
1662
  const matching = rules.filter(
1601
- (rule) => paths.some((path) => matchesPolicyGlob(path, rule.pattern))
1663
+ (rule) => paths2.some((path) => matchesPolicyGlob(path, rule.pattern))
1602
1664
  );
1603
1665
  return summarizePolicyRules(matching);
1604
1666
  }
@@ -1620,9 +1682,9 @@ function summarizeArchitecture(map) {
1620
1682
  }).slice(0, LIMITS2.hotspots);
1621
1683
  return { moduleCount: map.modules.length, edgeCount: map.edges.length, hotspots };
1622
1684
  }
1623
- function computeNeighbours(map, paths) {
1685
+ function computeNeighbours(map, paths2) {
1624
1686
  const result = [];
1625
- for (const path of paths.slice(0, LIMITS2.neighbourPaths)) {
1687
+ for (const path of paths2.slice(0, LIMITS2.neighbourPaths)) {
1626
1688
  const dependsOn = [
1627
1689
  ...new Set(map.edges.filter((edge) => edge.from === path).map((edge) => edge.to))
1628
1690
  ].slice(0, LIMITS2.neighbourEdges);
@@ -1744,8 +1806,8 @@ function headInSignature(lines, symbol) {
1744
1806
  const line = (lines[symbol.startLine - 1] ?? "").trim();
1745
1807
  return !head.endsWith("\u2026") && line.startsWith(head) && /^[\s{=>(]*$/.test(line.slice(head.length));
1746
1808
  }
1747
- async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1748
- if (paths.length === 0) return [];
1809
+ async function readGitDiffs(repositoryRoot, paths2, knownSecrets = []) {
1810
+ if (paths2.length === 0) return [];
1749
1811
  const sections = [];
1750
1812
  try {
1751
1813
  const { stdout } = await execFileAsync(
@@ -1758,7 +1820,7 @@ async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1758
1820
  "--no-ext-diff",
1759
1821
  "--unified=3",
1760
1822
  "--",
1761
- ...paths
1823
+ ...paths2
1762
1824
  ],
1763
1825
  { cwd: repositoryRoot, maxBuffer: 2e6 }
1764
1826
  );
@@ -1773,9 +1835,9 @@ async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1773
1835
  "--no-ext-diff",
1774
1836
  "--unified=3",
1775
1837
  "--",
1776
- ...paths
1838
+ ...paths2
1777
1839
  ],
1778
- ["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...paths]
1840
+ ["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...paths2]
1779
1841
  ]) {
1780
1842
  try {
1781
1843
  const { stdout } = await execFileAsync("git", args, {
@@ -1789,7 +1851,7 @@ async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1789
1851
  }
1790
1852
  const parsed = parseGitDiff(sections.join("\n"), knownSecrets);
1791
1853
  const known = new Set(parsed.map((diff) => diff.path));
1792
- const untracked = await listUntrackedPaths(repositoryRoot, paths);
1854
+ const untracked = await listUntrackedPaths(repositoryRoot, paths2);
1793
1855
  for (const path of untracked) {
1794
1856
  if (known.has(path)) continue;
1795
1857
  const patch = isCredentialPath(path) ? withheldPatch(path) : await buildUntrackedPatch(repositoryRoot, path, knownSecrets);
@@ -1850,11 +1912,11 @@ function parseGitDiff(diff, knownSecrets = []) {
1850
1912
  ] : [];
1851
1913
  });
1852
1914
  }
1853
- async function listUntrackedPaths(repositoryRoot, paths) {
1915
+ async function listUntrackedPaths(repositoryRoot, paths2) {
1854
1916
  try {
1855
1917
  const { stdout } = await execFileAsync(
1856
1918
  "git",
1857
- ["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths],
1919
+ ["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths2],
1858
1920
  { cwd: repositoryRoot, maxBuffer: 1e6 }
1859
1921
  );
1860
1922
  return stdout.split("\0").filter(Boolean);
@@ -2045,9 +2107,831 @@ function truncate(value, max) {
2045
2107
  return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max - 1)}\u2026`;
2046
2108
  }
2047
2109
 
2048
- // src/memory-extract/engine.ts
2110
+ // src/context/reuse-vectors.ts
2111
+ import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
2112
+ import { dirname as dirname4, join as join5 } from "path";
2113
+ import pTimeout from "p-timeout";
2114
+ var MODEL = `${LOCAL_EMBEDDING_MODEL.id}@${LOCAL_EMBEDDING_MODEL.revision}:${LOCAL_EMBEDDING_MODEL.schema}`;
2115
+ var file = (root) => join5(root, ".codebuddy", "cache", "reuse-vectors.json");
2116
+ var kinds = /* @__PURE__ */ new Set(["function", "class", "method", "const"]);
2117
+ var valid = (vector) => Array.isArray(vector) && vector.length === LOCAL_EMBEDDING_MODEL.dimensions && vector.every(Number.isFinite) && vector.some((value) => value !== 0);
2118
+ async function embedLocal(texts) {
2119
+ const { LocalModelProvider } = await import("./local-ST4WMRCG.js");
2120
+ return (await new LocalModelProvider().embed({ input: texts })).vectors;
2121
+ }
2122
+ async function load(root) {
2123
+ try {
2124
+ const cache = JSON.parse(await readFile6(file(root), "utf8"));
2125
+ if (cache.version === 1 && cache.model === MODEL && Array.isArray(cache.entries))
2126
+ return {
2127
+ ...cache,
2128
+ entries: cache.entries.filter(
2129
+ (entry) => typeof entry.path === "string" && typeof entry.name === "string" && valid(entry.vector)
2130
+ )
2131
+ };
2132
+ } catch {
2133
+ }
2134
+ return { version: 1, model: MODEL, entries: [] };
2135
+ }
2136
+ async function refreshReuseVectors(root, manifest, deadlineAt, embed = embedLocal) {
2137
+ const cache = await load(root);
2138
+ const cached = new Map(cache.entries.map((entry) => [`${entry.path}\0${entry.name}`, entry]));
2139
+ const entries = [];
2140
+ const pending = [];
2141
+ for (const entry of Object.values(manifest.entries)) {
2142
+ if (NON_SOURCE_PATH.test(entry.path) || TEST_PATH.test(entry.path)) continue;
2143
+ const symbols = (entry.symbolTable ?? []).filter(
2144
+ (symbol) => symbol.exported && kinds.has(symbol.kind)
2145
+ );
2146
+ if (!symbols.length) continue;
2147
+ const source = await readBoundedSource(root, entry.path);
2148
+ if (source === null) continue;
2149
+ const lines = source.split("\n");
2150
+ for (const symbol of symbols) {
2151
+ const old = cached.get(`${entry.path}\0${symbol.name}`);
2152
+ const existing = old?.hash === entry.hash ? old : void 0;
2153
+ if (existing) {
2154
+ entries.push(existing);
2155
+ continue;
2156
+ }
2157
+ pending.push({
2158
+ path: entry.path,
2159
+ name: symbol.name,
2160
+ hash: entry.hash,
2161
+ text: `${entry.path}
2162
+ ${symbol.signature}
2163
+ ${lines.slice(Math.max(0, symbol.startLine - 4), symbol.endLine).join("\n")}`.slice(
2164
+ 0,
2165
+ 2e3
2166
+ )
2167
+ });
2168
+ }
2169
+ }
2170
+ let embedded = 0;
2171
+ for (let start = 0; start < pending.length && Date.now() < deadlineAt; start += 16) {
2172
+ const batch = pending.slice(start, start + 16);
2173
+ const vectors = await embed(batch.map((item) => item.text));
2174
+ if (vectors.length !== batch.length || vectors.some((vector) => !valid(vector)))
2175
+ throw new Error("Invalid reuse embedding batch");
2176
+ batch.forEach(({ text: _text, ...entry }, index) => {
2177
+ entries.push({ ...entry, vector: vectors[index] });
2178
+ });
2179
+ embedded += batch.length;
2180
+ }
2181
+ await mkdir3(dirname4(file(root)), { recursive: true });
2182
+ await writeAtomic(file(root), JSON.stringify({ version: 1, model: MODEL, entries }), {
2183
+ mode: 384
2184
+ });
2185
+ return { embedded, pending: pending.length - embedded };
2186
+ }
2187
+ async function syncReuseVectors(root, budgetMs) {
2188
+ if (budgetMs <= 0 || process.env.CODEBUDDY_SEMANTIC === "0") return null;
2189
+ try {
2190
+ const { manifest } = await loadFreshIndex(root);
2191
+ return await refreshReuseVectors(root, manifest, Date.now() + budgetMs);
2192
+ } catch {
2193
+ return null;
2194
+ }
2195
+ }
2196
+ async function rankSemanticReuse(root, task, manifest, embed = embedLocal) {
2197
+ if (process.env.CODEBUDDY_SEMANTIC === "0") return [];
2198
+ const cache = await load(root);
2199
+ const eligible = cache.entries.filter(
2200
+ (entry) => manifest.entries[entry.path]?.hash === entry.hash
2201
+ );
2202
+ if (!eligible.length) return [];
2203
+ try {
2204
+ const vectors = await pTimeout(embed([task]), { milliseconds: 2e3 });
2205
+ const query = vectors[0];
2206
+ if (!query || !valid(query)) return [];
2207
+ const norm = Math.sqrt(query.reduce((sum, value) => sum + value * value, 0));
2208
+ return eligible.map((entry) => {
2209
+ const denominator = norm * Math.sqrt(entry.vector.reduce((sum, value) => sum + value * value, 0));
2210
+ return {
2211
+ path: entry.path,
2212
+ name: entry.name,
2213
+ score: entry.vector.reduce((sum, value, index) => sum + value * (query[index] ?? 0), 0) / denominator
2214
+ };
2215
+ }).sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
2216
+ } catch {
2217
+ return [];
2218
+ }
2219
+ }
2220
+
2221
+ // src/journal/journal.ts
2049
2222
  import { createHash as createHash2 } from "crypto";
2050
- import { resolve as resolve7 } from "path";
2223
+ import {
2224
+ appendFile,
2225
+ chmod,
2226
+ mkdir as mkdir4,
2227
+ open as open2,
2228
+ readdir as readdir3,
2229
+ readFile as readFile7,
2230
+ realpath as realpath3,
2231
+ rm as rm2,
2232
+ stat as stat2
2233
+ } from "fs/promises";
2234
+ import { homedir } from "os";
2235
+ import { isAbsolute, join as join6, relative as relative4, sep as sep4 } from "path";
2236
+ var JOURNAL_VERSION = 1;
2237
+ var JOURNAL_LIMITS = {
2238
+ eventChars: 1e3,
2239
+ journalBytes: 1e6,
2240
+ /** Largest transcript delta read per ingest; the rest is read by the next one. */
2241
+ chunkBytes: 4e6,
2242
+ maxAgeDays: 30,
2243
+ findings: 5,
2244
+ findingChars: 300,
2245
+ verification: 5,
2246
+ constraints: 5,
2247
+ editedPaths: 100
2248
+ };
2249
+ function journalDir(root) {
2250
+ return join6(root, ".codebuddy", "cache", "journal");
2251
+ }
2252
+ function safeSession(session) {
2253
+ const clean2 = session.replace(/[^A-Za-z0-9_.-]/g, "_");
2254
+ if (clean2 === session && clean2.length <= 120 && clean2 !== "." && clean2 !== ".." && clean2 !== "")
2255
+ return clean2;
2256
+ const name = clean2.slice(0, 100);
2257
+ return `${name.replace(/^\.+$/, "session") || "session"}-${createHash2("sha256").update(session).digest("hex").slice(0, 16)}`;
2258
+ }
2259
+ var paths = (root, session) => {
2260
+ const base = join6(journalDir(root), safeSession(session));
2261
+ return {
2262
+ journal: `${base}.jsonl`,
2263
+ cursor: `${base}.cursor.json`,
2264
+ checkpoint: `${base}.checkpoint.json`,
2265
+ lock: `${base}.lock`
2266
+ };
2267
+ };
2268
+ var VERIFICATION_SEGMENT = /^\s*(?:\w+=\S*\s+)*(?:npm|pnpm|yarn|bun|npx|cargo|go|make|mvn|gradle|\.\/gradlew|tsc|vitest|jest|pytest|python3? -m pytest|ruff|mypy|eslint|biome)\b.*\b(?:test|tests|lint|typecheck|tsc|build|check|vet|clippy|vitest|jest|pytest|ruff|mypy|eslint|biome)\b/;
2269
+ function isVerificationCommand(command) {
2270
+ return command.split(/&&|\|\||;/).some((segment) => VERIFICATION_SEGMENT.test(segment));
2271
+ }
2272
+ var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
2273
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
2274
+ var clip2 = (text, max) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2275
+ function clean(text) {
2276
+ const trimmed = text.replace(TAGGED_BLOCK, " ").replace(/\s+/g, " ").trim();
2277
+ return trimmed ? clip2(redactSensitive(trimmed), JOURNAL_LIMITS.eventChars) : null;
2278
+ }
2279
+ function eventsFromLine(line, offset) {
2280
+ let entry;
2281
+ try {
2282
+ entry = JSON.parse(line);
2283
+ } catch {
2284
+ return [];
2285
+ }
2286
+ const message = entry.message ?? {};
2287
+ const at = typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date(0)).toISOString();
2288
+ const lineId = typeof entry.uuid === "string" ? entry.uuid : createHash2("sha1").update(line).digest("hex").slice(0, 16);
2289
+ const blocks = typeof message.content === "string" ? [{ type: "text", text: message.content }] : Array.isArray(message.content) ? message.content : [];
2290
+ const events = [];
2291
+ blocks.forEach((block, index) => {
2292
+ const id = `${lineId}:${index}`;
2293
+ if (message.role === "user") {
2294
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
2295
+ events.push({
2296
+ id,
2297
+ at,
2298
+ kind: "result",
2299
+ ref: block.tool_use_id,
2300
+ ok: block.is_error !== true,
2301
+ offset
2302
+ });
2303
+ } else if (block.type === "text" && entry.isMeta !== true) {
2304
+ const text = clean(String(block.text ?? ""));
2305
+ if (text) events.push({ id, at, kind: "user", text, offset });
2306
+ }
2307
+ return;
2308
+ }
2309
+ if (message.role !== "assistant") return;
2310
+ if (block.type === "text") {
2311
+ const text = clean(String(block.text ?? ""));
2312
+ if (text) events.push({ id, at, kind: "assistant", text, offset });
2313
+ return;
2314
+ }
2315
+ if (block.type !== "tool_use") return;
2316
+ const input = block.input ?? {};
2317
+ const name = String(block.name ?? "");
2318
+ const path = input.file_path ?? input.notebook_path;
2319
+ if (EDIT_TOOLS.has(name) && typeof path === "string") {
2320
+ events.push({ id, at, kind: "edit", path: clip2(path, 500), offset });
2321
+ } else if (name === "Bash" && typeof input.command === "string") {
2322
+ const text = clean(input.command);
2323
+ if (text) {
2324
+ events.push({
2325
+ id,
2326
+ at,
2327
+ kind: "command",
2328
+ text,
2329
+ ...typeof block.id === "string" ? { ref: block.id } : {},
2330
+ offset
2331
+ });
2332
+ }
2333
+ }
2334
+ });
2335
+ return events;
2336
+ }
2337
+ async function readJournal(root, session) {
2338
+ let raw;
2339
+ try {
2340
+ raw = await readFile7(paths(root, session).journal, "utf8");
2341
+ } catch {
2342
+ return [];
2343
+ }
2344
+ const events = [];
2345
+ for (const line of raw.split("\n")) {
2346
+ if (!line) continue;
2347
+ try {
2348
+ const event = JSON.parse(line);
2349
+ if (event.v === JOURNAL_VERSION && typeof event.id === "string") events.push(event);
2350
+ } catch {
2351
+ }
2352
+ }
2353
+ return events;
2354
+ }
2355
+ async function readCursor(path) {
2356
+ try {
2357
+ const cursor = JSON.parse(await readFile7(path, "utf8"));
2358
+ return validCursor(cursor) ? cursor : null;
2359
+ } catch {
2360
+ return null;
2361
+ }
2362
+ }
2363
+ function validCursor(cursor) {
2364
+ return cursor !== null && typeof cursor === "object" && cursor.v === JOURNAL_VERSION && typeof cursor.transcript === "string" && typeof cursor.head === "string" && Number.isSafeInteger(cursor.offset) && cursor.offset >= 0 && Number.isSafeInteger(cursor.seq) && cursor.seq >= 0 && (cursor.headBytes === void 0 || Number.isInteger(cursor.headBytes) && cursor.headBytes >= 0 && cursor.headBytes <= 256);
2365
+ }
2366
+ async function headHash(path, length = 256) {
2367
+ const handle = await open2(path, "r");
2368
+ try {
2369
+ const buffer = Buffer.alloc(length);
2370
+ const { bytesRead } = await handle.read(buffer, 0, length, 0);
2371
+ return createHash2("sha1").update(buffer.subarray(0, bytesRead)).digest("hex");
2372
+ } finally {
2373
+ await handle.close();
2374
+ }
2375
+ }
2376
+ async function withLock(lock, run2, waitMs = 0) {
2377
+ const deadline = Date.now() + waitMs;
2378
+ for (; ; ) {
2379
+ try {
2380
+ await mkdir4(lock);
2381
+ break;
2382
+ } catch (error) {
2383
+ if (error.code !== "EEXIST") throw error;
2384
+ const age = Date.now() - await stat2(lock).then(
2385
+ (value) => value.mtimeMs,
2386
+ () => Date.now()
2387
+ );
2388
+ if (age >= 6e4) {
2389
+ await rm2(lock, { recursive: true, force: true });
2390
+ continue;
2391
+ }
2392
+ if (Date.now() >= deadline) return null;
2393
+ await new Promise((resolve11) => setTimeout(resolve11, 10));
2394
+ }
2395
+ }
2396
+ try {
2397
+ return await run2();
2398
+ } finally {
2399
+ await rm2(lock, { recursive: true, force: true });
2400
+ }
2401
+ }
2402
+ async function ingestTranscript(root, session, transcript, now = /* @__PURE__ */ new Date(), parse = eventsFromLine) {
2403
+ const files = paths(root, session);
2404
+ await mkdir4(journalDir(root), { recursive: true, mode: 448 });
2405
+ return withLock(files.lock, async () => {
2406
+ const size = (await stat2(transcript)).size;
2407
+ const previous = await readCheckpoint(root, session);
2408
+ const storedCursor = await readCursor(files.cursor);
2409
+ const cursor = storedCursor ?? (previous?.sourceCursor && validCursor(previous.sourceCursor) ? previous.sourceCursor : null);
2410
+ const headBytes = Math.min(size, 256);
2411
+ const head = size > 0 ? await headHash(transcript, headBytes) : "";
2412
+ const priorHead = cursor?.headBytes !== void 0 ? cursor.headBytes === 0 ? "" : await headHash(transcript, Math.min(size, cursor.headBytes)) : head;
2413
+ const rotated = cursor !== null && (cursor.transcript !== transcript || cursor.offset > size || cursor.head !== priorHead);
2414
+ let offset = cursor && !rotated ? cursor.offset : 0;
2415
+ if (cursor && previous && !rotated && cursor.offset === size && (previous.evidence?.toSeq ?? 0) === cursor.seq) {
2416
+ if (!storedCursor)
2417
+ await writeAtomic(files.cursor, `${JSON.stringify(cursor)}
2418
+ `, { mode: 384 });
2419
+ return { added: 0, offset: cursor.offset, rotated: false, checkpoint: previous };
2420
+ }
2421
+ const existing = await readJournal(root, session);
2422
+ const appended = [];
2423
+ const seen = new Set(existing.map((event) => event.id));
2424
+ let seq = Math.max(existing.at(-1)?.seq ?? 0, cursor?.seq ?? 0, previous?.evidence?.toSeq ?? 0);
2425
+ let added = 0;
2426
+ const handle = await open2(transcript, "r");
2427
+ try {
2428
+ while (offset < size) {
2429
+ const length = Math.min(size - offset, JOURNAL_LIMITS.chunkBytes);
2430
+ const buffer = Buffer.alloc(length);
2431
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
2432
+ const end = buffer.subarray(0, bytesRead).lastIndexOf(10);
2433
+ if (end === -1) {
2434
+ let scan = offset + bytesRead;
2435
+ let boundary = -1;
2436
+ while (scan < size) {
2437
+ const chunk = Buffer.alloc(Math.min(size - scan, JOURNAL_LIMITS.chunkBytes));
2438
+ const read = await handle.read(chunk, 0, chunk.length, scan);
2439
+ if (read.bytesRead === 0) break;
2440
+ const newline = chunk.subarray(0, read.bytesRead).indexOf(10);
2441
+ if (newline !== -1) {
2442
+ boundary = scan + newline;
2443
+ break;
2444
+ }
2445
+ scan += read.bytesRead;
2446
+ }
2447
+ if (boundary === -1) break;
2448
+ const id = `gap:${createHash2("sha256").update(`${transcript}:${offset}:${boundary}`).digest("hex").slice(0, 24)}`;
2449
+ if (!seen.has(id)) {
2450
+ const gap = {
2451
+ v: JOURNAL_VERSION,
2452
+ seq: ++seq,
2453
+ id,
2454
+ at: now.toISOString(),
2455
+ kind: "gap",
2456
+ offset
2457
+ };
2458
+ const torn = !await endsWithNewline(files.journal);
2459
+ await appendFile(files.journal, `${torn ? "\n" : ""}${JSON.stringify(gap)}
2460
+ `, {
2461
+ mode: 384
2462
+ });
2463
+ await chmod(files.journal, 384);
2464
+ seen.add(id);
2465
+ appended.push(gap);
2466
+ added++;
2467
+ }
2468
+ offset = boundary + 1;
2469
+ continue;
2470
+ }
2471
+ const lines = [];
2472
+ let lineStart = 0;
2473
+ for (const line of buffer.subarray(0, end).toString("utf8").split("\n")) {
2474
+ const lineOffset = offset + lineStart;
2475
+ lineStart += Buffer.byteLength(line) + 1;
2476
+ for (const event of parse(line, lineOffset)) {
2477
+ if (seen.has(event.id)) continue;
2478
+ seen.add(event.id);
2479
+ const saved = { ...event, v: JOURNAL_VERSION, seq: ++seq };
2480
+ appended.push(saved);
2481
+ lines.push(JSON.stringify(saved));
2482
+ }
2483
+ }
2484
+ if (lines.length > 0) {
2485
+ const torn = !await endsWithNewline(files.journal);
2486
+ await appendFile(files.journal, `${torn ? "\n" : ""}${lines.join("\n")}
2487
+ `, {
2488
+ mode: 384
2489
+ });
2490
+ await chmod(files.journal, 384);
2491
+ }
2492
+ added += lines.length;
2493
+ offset += end + 1;
2494
+ }
2495
+ } finally {
2496
+ await handle.close();
2497
+ }
2498
+ const next = { v: JOURNAL_VERSION, transcript, offset, head, headBytes, seq };
2499
+ await writeAtomic(files.cursor, `${JSON.stringify(next)}
2500
+ `, { mode: 384 });
2501
+ const events = [...existing, ...appended];
2502
+ const fresh = previous ? events.filter((event) => event.seq > (previous.evidence?.toSeq ?? 0)) : events;
2503
+ if (previous && fresh.length === 0) {
2504
+ await enforceRetention(files.journal);
2505
+ return { added, offset, rotated, checkpoint: previous };
2506
+ }
2507
+ const checkpoint = await persistCheckpoint(root, session, fresh, previous, now, {
2508
+ sourceCursor: next
2509
+ });
2510
+ await enforceRetention(files.journal);
2511
+ void pruneOldJournals(root, now).catch(() => void 0);
2512
+ return { added, offset, rotated, checkpoint };
2513
+ });
2514
+ }
2515
+ async function persistCheckpoint(root, session, fresh, previous, now, metadata = {}) {
2516
+ const derived = deriveCheckpoint(session, fresh, now, previous ?? void 0);
2517
+ const editedPaths = [...new Set(derived.editedPaths.map((path) => repoRelative(root, path)))];
2518
+ const sameTask = previous && derived.evidence?.fromSeq === previous.evidence?.fromSeq;
2519
+ const newlyEdited = fresh.filter((event) => event.kind === "edit" && event.path).map((event) => repoRelative(root, event.path));
2520
+ const hashes = {
2521
+ ...sameTask ? previous.fileHashes : {},
2522
+ ...await fileHashes(root, sameTask ? newlyEdited : editedPaths)
2523
+ };
2524
+ const checkpoint = {
2525
+ ...derived,
2526
+ editedPaths,
2527
+ identity: sameTask && previous.identity || await repoIdentity(root),
2528
+ fileHashes: Object.fromEntries(editedPaths.map((path) => [path, hashes[path] ?? null])),
2529
+ ...metadata
2530
+ };
2531
+ await writeAtomic(paths(root, session).checkpoint, `${JSON.stringify(checkpoint, null, 2)}
2532
+ `, {
2533
+ mode: 384
2534
+ });
2535
+ return checkpoint;
2536
+ }
2537
+ async function appendHostEvents(root, session, input, now = /* @__PURE__ */ new Date()) {
2538
+ if (!input.length) return null;
2539
+ const files = paths(root, session);
2540
+ await mkdir4(journalDir(root), { recursive: true, mode: 448 });
2541
+ return withLock(
2542
+ files.lock,
2543
+ async () => {
2544
+ const previous = await readCheckpoint(root, session);
2545
+ const existing = await readJournal(root, session);
2546
+ const pending = previous ? existing.filter((event) => event.seq > (previous.evidence?.toSeq ?? 0)) : existing;
2547
+ const seen = /* @__PURE__ */ new Set([
2548
+ ...existing.map((event) => event.id),
2549
+ ...previous?.recentHostEventIds ?? []
2550
+ ]);
2551
+ let seq = Math.max(existing.at(-1)?.seq ?? 0, previous?.evidence?.toSeq ?? 0);
2552
+ const fresh = [];
2553
+ for (const candidate of input.slice(0, 100)) {
2554
+ if (!candidate || typeof candidate.id !== "string" || !candidate.id || !["user", "assistant", "edit", "command", "result"].includes(candidate.kind))
2555
+ continue;
2556
+ const id = `host:${createHash2("sha256").update(candidate.id).digest("hex").slice(0, 32)}`;
2557
+ if (seen.has(id)) continue;
2558
+ const text = typeof candidate.text === "string" ? clean(candidate.text) : null;
2559
+ const path = typeof candidate.path === "string" ? clean(candidate.path) : null;
2560
+ if ((candidate.kind === "user" || candidate.kind === "assistant" || candidate.kind === "command") && !text)
2561
+ continue;
2562
+ if (candidate.kind === "edit" && !path) continue;
2563
+ if (candidate.kind === "result" && typeof candidate.ref !== "string") continue;
2564
+ fresh.push({
2565
+ v: JOURNAL_VERSION,
2566
+ seq: ++seq,
2567
+ id,
2568
+ at: now.toISOString(),
2569
+ kind: candidate.kind,
2570
+ offset: -1,
2571
+ ...text ? { text } : {},
2572
+ ...path ? { path: clip2(path, 500) } : {},
2573
+ ...typeof candidate.ref === "string" ? { ref: clip2(candidate.ref, 500) } : {},
2574
+ ...typeof candidate.ok === "boolean" ? { ok: candidate.ok } : {}
2575
+ });
2576
+ seen.add(id);
2577
+ }
2578
+ if (!fresh.length && !pending.length && previous) return { added: 0, checkpoint: previous };
2579
+ if (fresh.length) {
2580
+ const torn = !await endsWithNewline(files.journal);
2581
+ await appendFile(
2582
+ files.journal,
2583
+ `${torn ? "\n" : ""}${fresh.map((event) => JSON.stringify(event)).join("\n")}
2584
+ `,
2585
+ { mode: 384 }
2586
+ );
2587
+ await chmod(files.journal, 384);
2588
+ }
2589
+ const checkpoint = await persistCheckpoint(
2590
+ root,
2591
+ session,
2592
+ [...pending, ...fresh],
2593
+ previous,
2594
+ now,
2595
+ {
2596
+ recentHostEventIds: [
2597
+ .../* @__PURE__ */ new Set([
2598
+ ...previous?.recentHostEventIds ?? [],
2599
+ ...fresh.map((event) => event.id)
2600
+ ])
2601
+ ].slice(-256)
2602
+ }
2603
+ );
2604
+ await enforceRetention(files.journal);
2605
+ void pruneOldJournals(root, now).catch(() => void 0);
2606
+ return { added: fresh.length, checkpoint };
2607
+ },
2608
+ 2e3
2609
+ );
2610
+ }
2611
+ async function endsWithNewline(path) {
2612
+ try {
2613
+ const handle = await open2(path, "r");
2614
+ try {
2615
+ const { size } = await handle.stat();
2616
+ if (size === 0) return true;
2617
+ const byte = Buffer.alloc(1);
2618
+ await handle.read(byte, 0, 1, size - 1);
2619
+ return byte[0] === 10;
2620
+ } finally {
2621
+ await handle.close();
2622
+ }
2623
+ } catch {
2624
+ return true;
2625
+ }
2626
+ }
2627
+ function repoRelative(root, path) {
2628
+ if (!isAbsolute(path)) return path;
2629
+ const rel = relative4(root, path);
2630
+ return rel && rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute(rel) ? rel : path;
2631
+ }
2632
+ async function enforceRetention(journal) {
2633
+ const size = await stat2(journal).then(
2634
+ (s) => s.size,
2635
+ () => 0
2636
+ );
2637
+ if (size <= JOURNAL_LIMITS.journalBytes) return;
2638
+ const lines = (await readFile7(journal, "utf8")).split("\n").filter(Boolean);
2639
+ let bytes = 0;
2640
+ let cut = lines.length;
2641
+ const budget = Math.floor(JOURNAL_LIMITS.journalBytes / 2);
2642
+ while (cut > 0) {
2643
+ const length = Buffer.byteLength(lines[cut - 1]) + 1;
2644
+ if (bytes + length > budget) break;
2645
+ bytes += length;
2646
+ cut--;
2647
+ }
2648
+ const kept = lines.slice(cut);
2649
+ await writeAtomic(journal, `${kept.join("\n")}
2650
+ `, { mode: 384 });
2651
+ }
2652
+ async function pruneOldJournals(root, now) {
2653
+ const cutoff = now.getTime() - JOURNAL_LIMITS.maxAgeDays * 864e5;
2654
+ let removed = 0;
2655
+ for (const name of await readdir3(journalDir(root)).catch(() => [])) {
2656
+ const path = join6(journalDir(root), name);
2657
+ if (await stat2(path).then(
2658
+ (s) => s.mtimeMs,
2659
+ () => now.getTime()
2660
+ ) < cutoff) {
2661
+ await rm2(path, { recursive: true, force: true });
2662
+ removed++;
2663
+ }
2664
+ }
2665
+ return removed;
2666
+ }
2667
+ async function journalLag(root) {
2668
+ const names = await readdir3(journalDir(root)).catch(() => []);
2669
+ return Promise.all(
2670
+ names.filter((name) => name.endsWith(".cursor.json")).map(async (name) => {
2671
+ const cursor = await readCursor(join6(journalDir(root), name));
2672
+ const size = cursor ? await stat2(cursor.transcript).then(
2673
+ (s) => s.size,
2674
+ () => null
2675
+ ) : null;
2676
+ return {
2677
+ session: name.replace(/\.cursor\.json$/, ""),
2678
+ // null: the transcript is gone, so nothing more can be journaled.
2679
+ behindBytes: cursor && size !== null ? Math.max(0, size - cursor.offset) : null
2680
+ };
2681
+ })
2682
+ );
2683
+ }
2684
+ async function forgetJournal(root, session) {
2685
+ if (session === void 0) {
2686
+ await rm2(journalDir(root), { recursive: true, force: true });
2687
+ return;
2688
+ }
2689
+ const files = paths(root, session);
2690
+ await Promise.all(
2691
+ [files.journal, files.cursor, files.checkpoint].map((path) => rm2(path, { force: true }))
2692
+ );
2693
+ }
2694
+ async function readCheckpoint(root, session) {
2695
+ try {
2696
+ const checkpoint = JSON.parse(
2697
+ await readFile7(paths(root, session).checkpoint, "utf8")
2698
+ );
2699
+ return validCheckpoint(checkpoint) && checkpoint.session === session ? checkpoint : null;
2700
+ } catch {
2701
+ return null;
2702
+ }
2703
+ }
2704
+ function validCheckpoint(value) {
2705
+ return value !== null && typeof value === "object" && value.v === JOURNAL_VERSION && typeof value.session === "string" && typeof value.updatedAt === "string" && Number.isFinite(Date.parse(value.updatedAt)) && (value.goal === null || typeof value.goal === "string") && [value.constraints, value.editedPaths, value.findings, value.failedApproaches].every(
2706
+ (items) => Array.isArray(items) && items.every((item) => typeof item === "string")
2707
+ ) && Array.isArray(value.verification) && value.verification.every(
2708
+ (run2) => run2 !== null && typeof run2 === "object" && typeof run2.command === "string" && (run2.ok === null || typeof run2.ok === "boolean") && typeof run2.stale === "boolean"
2709
+ ) && (value.nextAction === null || typeof value.nextAction === "string");
2710
+ }
2711
+ async function listJournals(root) {
2712
+ const names = await readdir3(journalDir(root)).catch(() => []);
2713
+ const sessions = await Promise.all(
2714
+ names.filter((name) => name.endsWith(".checkpoint.json")).map(async (name) => {
2715
+ try {
2716
+ const checkpoint = JSON.parse(
2717
+ await readFile7(join6(journalDir(root), name), "utf8")
2718
+ );
2719
+ return validCheckpoint(checkpoint) ? { session: checkpoint.session, updatedAt: checkpoint.updatedAt } : null;
2720
+ } catch {
2721
+ return null;
2722
+ }
2723
+ })
2724
+ );
2725
+ return sessions.filter((item) => item !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2726
+ }
2727
+ function eventsFromCodexLine(line, offset) {
2728
+ let entry;
2729
+ try {
2730
+ entry = JSON.parse(line);
2731
+ } catch {
2732
+ return [];
2733
+ }
2734
+ const payload = entry.payload ?? {};
2735
+ if (entry.type !== "response_item") return [];
2736
+ const at = typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date(0)).toISOString();
2737
+ const base = typeof payload.id === "string" ? payload.id : createHash2("sha1").update(line).digest("hex").slice(0, 16);
2738
+ if (payload.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
2739
+ const kind = payload.role === "user" ? "user" : "assistant";
2740
+ return (payload.content ?? []).flatMap((item, index) => {
2741
+ if (item.type !== "input_text" && item.type !== "output_text") return [];
2742
+ const text = clean(String(item.text ?? ""));
2743
+ return text ? [{ id: `${base}:${index}`, at, kind, text, offset }] : [];
2744
+ });
2745
+ }
2746
+ if (payload.type === "custom_tool_call" && payload.name === "apply_patch") {
2747
+ const patch = String(payload.input ?? "");
2748
+ const files = [...patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)];
2749
+ return files.map((match, index) => ({
2750
+ id: `${base}:${index}`,
2751
+ at,
2752
+ kind: "edit",
2753
+ path: clip2((match[1] ?? "").trim(), 500),
2754
+ offset
2755
+ }));
2756
+ }
2757
+ return [];
2758
+ }
2759
+ async function firstLine(path) {
2760
+ const handle = await open2(path, "r");
2761
+ try {
2762
+ const buffer = Buffer.alloc(2e6);
2763
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
2764
+ const end = buffer.subarray(0, bytesRead).indexOf(10);
2765
+ return buffer.subarray(0, end === -1 ? bytesRead : end).toString("utf8");
2766
+ } finally {
2767
+ await handle.close();
2768
+ }
2769
+ }
2770
+ async function findCodexRollouts(root, codexHome = join6(homedir(), ".codex"), limit = 300) {
2771
+ const want = await realpath3(root).catch(() => root);
2772
+ const files = [];
2773
+ const walk = async (dir, depth) => {
2774
+ for (const name of await readdir3(dir).catch(() => [])) {
2775
+ const path = join6(dir, name);
2776
+ if (depth < 3) await walk(path, depth + 1);
2777
+ else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
2778
+ files.push({
2779
+ path,
2780
+ mtime: await stat2(path).then(
2781
+ (s) => s.mtimeMs,
2782
+ () => 0
2783
+ )
2784
+ });
2785
+ }
2786
+ }
2787
+ };
2788
+ await walk(join6(codexHome, "sessions"), 0);
2789
+ const found = [];
2790
+ for (const { path } of files.sort((a, b) => b.mtime - a.mtime).slice(0, limit)) {
2791
+ try {
2792
+ const meta = JSON.parse(await firstLine(path));
2793
+ if (meta.type !== "session_meta" || !meta.payload?.cwd || !meta.payload.id) continue;
2794
+ const cwd = await realpath3(meta.payload.cwd).catch(() => meta.payload?.cwd);
2795
+ if (cwd === want) found.push({ path, id: meta.payload.id });
2796
+ } catch {
2797
+ }
2798
+ }
2799
+ return found;
2800
+ }
2801
+ function importCodexRollout(root, rollout, now = /* @__PURE__ */ new Date()) {
2802
+ return ingestTranscript(root, `codex-${rollout.id}`, rollout.path, now, eventsFromCodexLine);
2803
+ }
2804
+ function renderCheckpoint(checkpoint, label, annotate = {}) {
2805
+ const mark = (path) => annotate.missing?.includes(path) ? `${path} (missing now)` : annotate.changed?.includes(path) ? `${path} (changed since)` : path;
2806
+ const lines = [
2807
+ `## CodeBuddy task checkpoint (${label}, ${checkpoint.updatedAt.slice(0, 16)}Z)`,
2808
+ checkpoint.omittedTranscriptLines ? `coverage: ${checkpoint.omittedTranscriptLines} oversized transcript line(s) omitted; consult the original transcript` : "",
2809
+ annotate.identity ?? "",
2810
+ checkpoint.goal ? `goal: ${clip2(checkpoint.goal, 300)}` : "",
2811
+ checkpoint.constraints.length > 0 ? `constraints: ${checkpoint.constraints.join(" ")}` : "",
2812
+ checkpoint.editedPaths.length > 0 ? `edited: ${checkpoint.editedPaths.slice(-8).map(mark).join(", ")}` : "",
2813
+ checkpoint.verification.length > 0 ? `checks: ${checkpoint.verification.map(
2814
+ (run2) => `${run2.command} (${run2.ok === null ? "no result" : run2.ok ? "passed" : "failed"}${run2.stale ? annotate.sourceChanged ? ", stale: source changed" : ", stale: edited after" : ""})`
2815
+ ).join("; ")}` : "",
2816
+ checkpoint.findings.length > 0 ? `findings: ${checkpoint.findings.slice(-3).join(" | ")}` : "",
2817
+ checkpoint.nextAction ? `next: ${checkpoint.nextAction}` : ""
2818
+ ];
2819
+ return clip2(lines.filter(Boolean).join("\n"), 1500);
2820
+ }
2821
+ async function renderCheckpointNow(root, checkpoint, label) {
2822
+ const freshness = await compareFreshness(root, checkpoint.editedPaths, checkpoint.fileHashes);
2823
+ const now = await repoIdentity(root);
2824
+ const then = checkpoint.identity;
2825
+ const moved = then && (then.branch !== now.branch || then.gitDir !== now.gitDir) ? `recorded on ${then.branch ?? "no branch"}${then.head ? ` at ${then.head.slice(0, 7)}` : ""}${then.gitDir !== now.gitDir ? ` in ${then.worktree ?? "another checkout"}` : ""}; now on ${now.branch ?? "no branch"}` : void 0;
2826
+ const sourceChanged = freshness.changed.length > 0 || freshness.missing.length > 0;
2827
+ const rendered = sourceChanged ? {
2828
+ ...checkpoint,
2829
+ verification: checkpoint.verification.map((run2) => ({ ...run2, stale: true }))
2830
+ } : checkpoint;
2831
+ return renderCheckpoint(rendered, label, {
2832
+ changed: freshness.changed,
2833
+ missing: freshness.missing,
2834
+ sourceChanged,
2835
+ ...moved ? { identity: moved } : {}
2836
+ });
2837
+ }
2838
+ function isUnfinished(checkpoint) {
2839
+ if (!checkpoint.goal) return false;
2840
+ return checkpoint.nextAction !== null || [...new Map(checkpoint.verification.map((run2) => [run2.command, run2])).values()].some(
2841
+ (run2) => run2.ok !== true || run2.stale
2842
+ ) || checkpoint.editedPaths.length > 0 && checkpoint.verification.length === 0 || !checkpoint.lastAssistant && checkpoint.verification.length === 0;
2843
+ }
2844
+ async function latestUnfinished(root, excludeSession, now = /* @__PURE__ */ new Date(), maxAgeDays = 3) {
2845
+ const cutoff = now.getTime() - maxAgeDays * 864e5;
2846
+ for (const { session, updatedAt } of await listJournals(root)) {
2847
+ if (session === excludeSession || Date.parse(updatedAt) < cutoff) continue;
2848
+ const checkpoint = await readCheckpoint(root, session);
2849
+ if (checkpoint && isUnfinished(checkpoint)) return checkpoint;
2850
+ }
2851
+ return null;
2852
+ }
2853
+ var CONSTRAINT = /\b(must|never|don't|do not|always|only|without|keep|avoid)\b/i;
2854
+ var NEXT_STEP = /\b(next|remaining|todo|still need|then I(?:'ll| will)|will now)\b/i;
2855
+ function isFollowUp(text) {
2856
+ return text.split(/\s+/).filter(Boolean).length <= 6 && taskTerms(text).length <= 3;
2857
+ }
2858
+ function deriveCheckpoint(session, events, now = /* @__PURE__ */ new Date(), previous) {
2859
+ let goal = previous?.goal ?? null;
2860
+ let constraints = [...previous?.constraints ?? []];
2861
+ let editedPaths = [...previous?.editedPaths ?? []];
2862
+ let runs = (previous?.verification ?? []).map((run2, index) => ({
2863
+ ...run2,
2864
+ ref: previous?.verificationRefs?.[index] ?? null
2865
+ }));
2866
+ let findings = [...previous?.findings ?? []];
2867
+ let lastAssistant = previous?.lastAssistant ?? "";
2868
+ let nextAction = previous?.nextAction ?? null;
2869
+ let fromSeq = previous?.evidence?.fromSeq ?? events[0]?.seq ?? 0;
2870
+ let omittedTranscriptLines = previous?.omittedTranscriptLines ?? 0;
2871
+ const continuation = /^(?:also|and|please also|following up|continue|keep|do not|don't|instead of|for (?:this|that)|what about|why|how|can you|could you)\b/i;
2872
+ const switchTask = /^(?:new task\s*:|switch (?:tasks?|to)\b|now (?:refactor|implement|build|create|fix|migrate|port)\b)/i;
2873
+ const bareFollowUp = /^(?:continue|go on|yes|ok(?:ay)?|thanks|run (?:the )?(?:tests?|checks?)(?: again)?)[.!]?$/i;
2874
+ for (const event of events) {
2875
+ if (event.kind === "user" && event.text) {
2876
+ const completed = runs.length > 0 && [...new Map(runs.map((run2) => [run2.command, run2])).values()].every(
2877
+ (run2) => run2.ok === true && !run2.stale
2878
+ ) && !nextAction;
2879
+ const start = !goal ? !bareFollowUp.test(event.text) : switchTask.test(event.text) || completed && !continuation.test(event.text) && !isFollowUp(event.text);
2880
+ if (start) {
2881
+ goal = event.text;
2882
+ constraints = [];
2883
+ editedPaths = [];
2884
+ runs = [];
2885
+ findings = [];
2886
+ lastAssistant = "";
2887
+ nextAction = null;
2888
+ fromSeq = event.seq;
2889
+ }
2890
+ constraints.push(
2891
+ ...event.text.split(/(?<=[.!?])\s+/).filter((sentence) => CONSTRAINT.test(sentence)).map((sentence) => clip2(sentence, 200))
2892
+ );
2893
+ constraints = [...new Set(constraints)].slice(-JOURNAL_LIMITS.constraints);
2894
+ } else if (event.kind === "edit" && event.path) {
2895
+ editedPaths = [.../* @__PURE__ */ new Set([...editedPaths, event.path])].slice(-JOURNAL_LIMITS.editedPaths);
2896
+ for (const run2 of runs) run2.stale = true;
2897
+ } else if (event.kind === "command" && event.text && isVerificationCommand(event.text)) {
2898
+ runs.push({ command: clip2(event.text, 160), ok: null, stale: false, ref: event.ref ?? null });
2899
+ runs = runs.slice(-JOURNAL_LIMITS.verification);
2900
+ } else if (event.kind === "result" && event.ref) {
2901
+ for (const run2 of runs) if (run2.ref === event.ref) run2.ok = event.ok ?? null;
2902
+ } else if (event.kind === "assistant" && event.text) {
2903
+ if (lastAssistant) findings.push(clip2(lastAssistant, JOURNAL_LIMITS.findingChars));
2904
+ findings = findings.slice(-JOURNAL_LIMITS.findings);
2905
+ lastAssistant = event.text;
2906
+ const next = lastAssistant.split(/(?<=[.!?])\s+/).find((sentence) => NEXT_STEP.test(sentence));
2907
+ nextAction = next ? clip2(next, 200) : null;
2908
+ } else if (event.kind === "gap") {
2909
+ omittedTranscriptLines++;
2910
+ }
2911
+ }
2912
+ return {
2913
+ v: JOURNAL_VERSION,
2914
+ session,
2915
+ updatedAt: now.toISOString(),
2916
+ goal,
2917
+ constraints,
2918
+ editedPaths,
2919
+ verification: runs.map(({ ref: _ref, ...run2 }) => run2),
2920
+ verificationRefs: runs.map((run2) => run2.ref),
2921
+ failedApproaches: [
2922
+ ...new Set(runs.filter((run2) => run2.ok === false).map((run2) => `${run2.command}: failed`))
2923
+ ].slice(-JOURNAL_LIMITS.verification),
2924
+ findings,
2925
+ lastAssistant,
2926
+ ...omittedTranscriptLines ? { omittedTranscriptLines } : {},
2927
+ nextAction,
2928
+ evidence: events.length > 0 ? { fromSeq, toSeq: events.at(-1)?.seq ?? 0 } : previous?.evidence ?? null
2929
+ };
2930
+ }
2931
+
2932
+ // src/memory-extract/engine.ts
2933
+ import { createHash as createHash3 } from "crypto";
2934
+ import { resolve as resolve8 } from "path";
2051
2935
 
2052
2936
  // src/memory-extract/deterministic.ts
2053
2937
  var EXPLICIT_RESOLUTION = /\b(?:fixed|resolved|closed)\b.*\bfact_[a-z0-9]+\b/i;
@@ -2290,8 +3174,8 @@ function matchResolution(fixText, pathCandidates, unresolved) {
2290
3174
  }
2291
3175
 
2292
3176
  // src/memory-extract/review-store.ts
2293
- import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, rename, rm as rm2 } from "fs/promises";
2294
- import { dirname as dirname4, join as join5, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
3177
+ import { mkdir as mkdir5, readdir as readdir4, readFile as readFile8, rename, rm as rm3 } from "fs/promises";
3178
+ import { dirname as dirname5, join as join7, relative as relative5, resolve as resolve7, sep as sep5 } from "path";
2295
3179
  import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
2296
3180
  import { z as z5 } from "zod";
2297
3181
  var reviewSchema = z5.object({
@@ -2326,15 +3210,15 @@ var ReviewStore = class {
2326
3210
  reviewDirectory;
2327
3211
  archiveDirectory;
2328
3212
  constructor(repositoryRoot = process.cwd()) {
2329
- this.repositoryRoot = resolve6(repositoryRoot);
2330
- this.reviewDirectory = join5(this.repositoryRoot, ".codebuddy", "memory", "review");
2331
- this.archiveDirectory = join5(this.repositoryRoot, ".codebuddy", "memory", "review-archive");
3213
+ this.repositoryRoot = resolve7(repositoryRoot);
3214
+ this.reviewDirectory = join7(this.repositoryRoot, ".codebuddy", "memory", "review");
3215
+ this.archiveDirectory = join7(this.repositoryRoot, ".codebuddy", "memory", "review-archive");
2332
3216
  }
2333
3217
  async write(raw) {
2334
3218
  const input = redactPersisted(raw);
2335
3219
  const path = this.itemPath(input.id);
2336
3220
  await this.prepareDirectory();
2337
- await assertNoSymlinkBetween(this.repositoryRoot, dirname4(path));
3221
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname5(path));
2338
3222
  const frontMatter = stringifyYaml3({
2339
3223
  schemaVersion: 1,
2340
3224
  id: input.id,
@@ -2363,7 +3247,7 @@ var ReviewStore = class {
2363
3247
  async read(id) {
2364
3248
  const path = this.itemPath(id);
2365
3249
  await assertNoSymlinkBetween(this.repositoryRoot, path);
2366
- const parsed = parseFrontMatter(await readFile5(path, "utf8"));
3250
+ const parsed = parseFrontMatter(await readFile8(path, "utf8"));
2367
3251
  return {
2368
3252
  ...reviewSchema.parse(parseYaml4(parsed.frontMatter)),
2369
3253
  content: parsed.content.trim(),
@@ -2373,7 +3257,7 @@ var ReviewStore = class {
2373
3257
  async list() {
2374
3258
  try {
2375
3259
  await this.prepareDirectory();
2376
- const entries = await readdir3(this.reviewDirectory, { withFileTypes: true });
3260
+ const entries = await readdir4(this.reviewDirectory, { withFileTypes: true });
2377
3261
  const items = [];
2378
3262
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
2379
3263
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
@@ -2390,7 +3274,7 @@ var ReviewStore = class {
2390
3274
  }
2391
3275
  }
2392
3276
  async delete(id) {
2393
- await rm2(this.itemPath(id), { force: true });
3277
+ await rm3(this.itemPath(id), { force: true });
2394
3278
  }
2395
3279
  async archiveExpired(options = {}) {
2396
3280
  const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - (options.olderThanDays ?? 30) * 24 * 60 * 60 * 1e3;
@@ -2412,25 +3296,25 @@ var ReviewStore = class {
2412
3296
  return overflow;
2413
3297
  }
2414
3298
  async archive(item) {
2415
- await mkdir3(this.archiveDirectory, { recursive: true, mode: 448 });
3299
+ await mkdir5(this.archiveDirectory, { recursive: true, mode: 448 });
2416
3300
  await assertNoSymlinkBetween(this.repositoryRoot, this.archiveDirectory);
2417
- await rename(item.path, join5(this.archiveDirectory, `${item.id}.md`));
3301
+ await rename(item.path, join7(this.archiveDirectory, `${item.id}.md`));
2418
3302
  }
2419
3303
  itemPath(id) {
2420
3304
  if (!/^rev_[a-z0-9]+$/.test(id)) throw new Error(`Invalid review id: ${id}`);
2421
- return this.safePath(join5(this.reviewDirectory, `${id}.md`));
3305
+ return this.safePath(join7(this.reviewDirectory, `${id}.md`));
2422
3306
  }
2423
3307
  safePath(path) {
2424
- const absolute = resolve6(path);
2425
- const relativePath = relative4(this.repositoryRoot, absolute);
2426
- if (relativePath === ".." || relativePath.startsWith(`..${sep4}`) || relativePath === "") {
3308
+ const absolute = resolve7(path);
3309
+ const relativePath = relative5(this.repositoryRoot, absolute);
3310
+ if (relativePath === ".." || relativePath.startsWith(`..${sep5}`) || relativePath === "") {
2427
3311
  throw new Error(`Review path escapes repository root: ${path}`);
2428
3312
  }
2429
3313
  return absolute;
2430
3314
  }
2431
3315
  async prepareDirectory() {
2432
- await assertNoSymlinkBetween(this.repositoryRoot, dirname4(this.reviewDirectory));
2433
- await mkdir3(this.reviewDirectory, { recursive: true, mode: 448 });
3316
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname5(this.reviewDirectory));
3317
+ await mkdir5(this.reviewDirectory, { recursive: true, mode: 448 });
2434
3318
  }
2435
3319
  };
2436
3320
 
@@ -2517,7 +3401,7 @@ var AUTO_SAVE_THRESHOLD = 0.7;
2517
3401
  var STATED_CONFIDENCE = 0.9;
2518
3402
  var POLICY_CONFIDENCE = 0.6;
2519
3403
  async function captureAfterTurn(input, options = {}) {
2520
- const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
3404
+ const repositoryRoot = resolve8(options.repositoryRoot ?? process.cwd());
2521
3405
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
2522
3406
  const memoryStore = new MemoryFileStore(repositoryRoot);
2523
3407
  const reviewStore = new ReviewStore(repositoryRoot);
@@ -2881,10 +3765,10 @@ function candidateFingerprint(namespace, raw) {
2881
3765
  content: candidate.content.trim().replace(/\s+/g, " ").toLowerCase(),
2882
3766
  paths: [...new Set(candidate.paths.map((path) => path.replace(/\\/g, "/")))].sort()
2883
3767
  });
2884
- return createHash2("sha256").update(canonical).digest("hex");
3768
+ return createHash3("sha256").update(canonical).digest("hex");
2885
3769
  }
2886
3770
  async function approveReviewItem(id, options = {}) {
2887
- const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
3771
+ const repositoryRoot = resolve8(options.repositoryRoot ?? process.cwd());
2888
3772
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
2889
3773
  const reviewStore = new ReviewStore(repositoryRoot);
2890
3774
  const memoryStore = new MemoryFileStore(repositoryRoot);
@@ -2931,17 +3815,17 @@ async function approveReviewItem(id, options = {}) {
2931
3815
  }
2932
3816
 
2933
3817
  // src/hooks/semantic.ts
2934
- import pTimeout from "p-timeout";
3818
+ import pTimeout2 from "p-timeout";
2935
3819
  var CONNECT_BUDGET_MS = 2500;
2936
3820
  var SEARCH_BUDGET_MS = 3e3;
2937
3821
  var RERANK_BUDGET_MS = 2e3;
2938
3822
  async function configuredReranker(repositoryRoot) {
2939
3823
  if (process.env.CODEBUDDY_RERANK === "0") return void 0;
2940
- const file = await readConfigFile(repositoryRoot);
2941
- const provider = file.rerank?.provider ?? "off";
3824
+ const file2 = await readConfigFile(repositoryRoot);
3825
+ const provider = file2.rerank?.provider ?? "off";
2942
3826
  if (provider === "off") return void 0;
2943
- const model = file.rerank?.model ?? DEFAULT_RERANK_MODELS[provider].id;
2944
- const bounded = (scores) => pTimeout(scores, { milliseconds: RERANK_BUDGET_MS });
3827
+ const model = file2.rerank?.model ?? DEFAULT_RERANK_MODELS[provider].id;
3828
+ const bounded = (scores) => pTimeout2(scores, { milliseconds: RERANK_BUDGET_MS });
2945
3829
  if (provider === "local") {
2946
3830
  const revision = model === DEFAULT_RERANK_MODELS.local.id ? DEFAULT_RERANK_MODELS.local.revision : void 0;
2947
3831
  return async (task, documents) => {
@@ -2951,7 +3835,7 @@ async function configuredReranker(repositoryRoot) {
2951
3835
  );
2952
3836
  };
2953
3837
  }
2954
- const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file.provider?.apiKey;
3838
+ const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file2.provider?.apiKey;
2955
3839
  if (!apiKey) return void 0;
2956
3840
  const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-LXZUR3U2.js");
2957
3841
  const call = provider === "openrouter" ? rerankOpenRouter : rerankHuggingFace;
@@ -2962,14 +3846,14 @@ async function openHookSemantics(repositoryRoot) {
2962
3846
  const connect = async () => {
2963
3847
  const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
2964
3848
  if (!config.postgresUrl) return null;
2965
- const { createRuntime } = await import("./operations-WJR7AZZF.js");
3849
+ const { createRuntime } = await import("./operations-6Z6ZDVBP.js");
2966
3850
  const opening = createRuntime(config, {
2967
3851
  repositoryRoot,
2968
3852
  startWorker: false,
2969
3853
  connectTimeoutSeconds: 2
2970
3854
  });
2971
3855
  try {
2972
- return await pTimeout(opening, { milliseconds: CONNECT_BUDGET_MS });
3856
+ return await pTimeout2(opening, { milliseconds: CONNECT_BUDGET_MS });
2973
3857
  } catch {
2974
3858
  void opening.then((handle) => handle.close()).catch(() => void 0);
2975
3859
  return null;
@@ -2997,22 +3881,22 @@ async function openHookSemantics(repositoryRoot) {
2997
3881
  }
2998
3882
 
2999
3883
  // src/hooks/staging.ts
3000
- import { mkdir as mkdir4, readFile as readFile6, rename as rename2, rm as rm3, stat as stat3 } from "fs/promises";
3001
- import { join as join6, resolve as resolve8 } from "path";
3884
+ import { mkdir as mkdir6, readdir as readdir5, readFile as readFile9, rename as rename2, rm as rm4, stat as stat3 } from "fs/promises";
3885
+ import { join as join8, resolve as resolve9 } from "path";
3002
3886
  var STAGING_LOCK_STALE_MS = 1e4;
3003
3887
  var HookStagingStore = class {
3004
3888
  directory;
3005
3889
  operations = Promise.resolve();
3006
3890
  constructor(repositoryRoot = process.cwd()) {
3007
- this.directory = join6(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
3891
+ this.directory = join8(resolve9(repositoryRoot), ".codebuddy", "cache", "hooks");
3008
3892
  }
3009
- async add(sessionId, paths) {
3893
+ async add(sessionId, paths2) {
3010
3894
  await this.serialize(async () => {
3011
- const clean = paths.map(normalize2).filter(Boolean);
3012
- if (clean.length === 0) return;
3013
- await mkdir4(this.directory, { recursive: true, mode: 448 });
3895
+ const clean2 = paths2.map(normalize2).filter(Boolean);
3896
+ if (clean2.length === 0) return;
3897
+ await mkdir6(this.directory, { recursive: true, mode: 448 });
3014
3898
  await this.withFileLock(sessionId, async () => {
3015
- const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean])].sort();
3899
+ const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean2])].sort();
3016
3900
  await writeAtomic(
3017
3901
  this.filePath(sessionId),
3018
3902
  `${JSON.stringify({ paths: merged }, null, 2)}
@@ -3021,9 +3905,18 @@ var HookStagingStore = class {
3021
3905
  });
3022
3906
  });
3023
3907
  }
3908
+ /** Sessions with edits staged but not yet captured by a turn end. */
3909
+ async pending() {
3910
+ const names = await readdir5(this.directory).catch(() => []);
3911
+ const sessions = names.filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -5));
3912
+ const counts = await Promise.all(
3913
+ sessions.map(async (session) => ({ session, paths: (await this.read(session)).length }))
3914
+ );
3915
+ return counts.filter((entry) => entry.paths > 0);
3916
+ }
3024
3917
  async read(sessionId) {
3025
3918
  try {
3026
- const parsed = JSON.parse(await readFile6(this.filePath(sessionId), "utf8"));
3919
+ const parsed = JSON.parse(await readFile9(this.filePath(sessionId), "utf8"));
3027
3920
  return Array.isArray(parsed?.paths) ? parsed.paths.filter((p) => typeof p === "string") : [];
3028
3921
  } catch {
3029
3922
  return [];
@@ -3032,7 +3925,7 @@ var HookStagingStore = class {
3032
3925
  /** Return the staged paths and clear them. */
3033
3926
  async drain(sessionId) {
3034
3927
  return this.serialize(async () => {
3035
- await mkdir4(this.directory, { recursive: true, mode: 448 });
3928
+ await mkdir6(this.directory, { recursive: true, mode: 448 });
3036
3929
  return this.withFileLock(sessionId, async () => {
3037
3930
  const path = this.filePath(sessionId);
3038
3931
  const draining = `${path}.${process.pid}.${Date.now()}.drain`;
@@ -3043,27 +3936,27 @@ var HookStagingStore = class {
3043
3936
  throw error;
3044
3937
  }
3045
3938
  try {
3046
- const parsed = JSON.parse(await readFile6(draining, "utf8"));
3939
+ const parsed = JSON.parse(await readFile9(draining, "utf8"));
3047
3940
  return Array.isArray(parsed?.paths) ? parsed.paths.filter((item) => typeof item === "string") : [];
3048
3941
  } finally {
3049
- await rm3(draining, { force: true });
3942
+ await rm4(draining, { force: true });
3050
3943
  }
3051
3944
  });
3052
3945
  });
3053
3946
  }
3054
3947
  /** Claim a batch without consuming edits staged while capture runs. */
3055
3948
  async claim(sessionId) {
3056
- await mkdir4(this.directory, { recursive: true, mode: 448 });
3949
+ await mkdir6(this.directory, { recursive: true, mode: 448 });
3057
3950
  const captureLock = `${this.filePath(sessionId)}.capture-lock`;
3058
3951
  for (let attempt = 0; ; attempt++) {
3059
3952
  try {
3060
- await mkdir4(captureLock);
3953
+ await mkdir6(captureLock);
3061
3954
  break;
3062
3955
  } catch (error) {
3063
3956
  if (error.code !== "EEXIST") throw error;
3064
3957
  const age = Date.now() - (await stat3(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
3065
3958
  if (age > 6e4) {
3066
- await rm3(captureLock, { recursive: true, force: true });
3959
+ await rm4(captureLock, { recursive: true, force: true });
3067
3960
  continue;
3068
3961
  }
3069
3962
  if (attempt >= 200) return null;
@@ -3074,34 +3967,34 @@ var HookStagingStore = class {
3074
3967
  return await this.withFileLock(sessionId, async () => {
3075
3968
  const claimed = this.claimPath(sessionId);
3076
3969
  try {
3077
- return parsePaths(await readFile6(claimed, "utf8"));
3970
+ return parsePaths(await readFile9(claimed, "utf8"));
3078
3971
  } catch (error) {
3079
3972
  if (error.code !== "ENOENT") throw error;
3080
3973
  }
3081
3974
  try {
3082
3975
  await rename2(this.filePath(sessionId), claimed);
3083
- return parsePaths(await readFile6(claimed, "utf8"));
3976
+ return parsePaths(await readFile9(claimed, "utf8"));
3084
3977
  } catch (error) {
3085
3978
  if (error.code === "ENOENT") return [];
3086
3979
  throw error;
3087
3980
  }
3088
3981
  });
3089
3982
  } catch (error) {
3090
- await rm3(captureLock, { recursive: true, force: true });
3983
+ await rm4(captureLock, { recursive: true, force: true });
3091
3984
  throw error;
3092
3985
  }
3093
3986
  }
3094
3987
  /** Acknowledge only the claimed batch; later edits remain staged. */
3095
3988
  async acknowledge(sessionId) {
3096
3989
  await this.withFileLock(sessionId, async () => {
3097
- await rm3(this.claimPath(sessionId), { force: true });
3990
+ await rm4(this.claimPath(sessionId), { force: true });
3098
3991
  });
3099
- await rm3(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
3992
+ await rm4(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
3100
3993
  }
3101
3994
  /** Restore the claimed batch for retry, merging edits that arrived during capture. */
3102
3995
  async rollback(sessionId) {
3103
3996
  await this.withFileLock(sessionId, async () => {
3104
- const claimed = await readFile6(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
3997
+ const claimed = await readFile9(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
3105
3998
  const merged = [.../* @__PURE__ */ new Set([...claimed, ...await this.read(sessionId)])].sort();
3106
3999
  if (merged.length > 0)
3107
4000
  await writeAtomic(
@@ -3109,9 +4002,9 @@ var HookStagingStore = class {
3109
4002
  `${JSON.stringify({ paths: merged }, null, 2)}
3110
4003
  `
3111
4004
  );
3112
- await rm3(this.claimPath(sessionId), { force: true });
4005
+ await rm4(this.claimPath(sessionId), { force: true });
3113
4006
  });
3114
- await rm3(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
4007
+ await rm4(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
3115
4008
  }
3116
4009
  claimPath(sessionId) {
3117
4010
  return `${this.filePath(sessionId)}.claimed`;
@@ -3120,7 +4013,7 @@ var HookStagingStore = class {
3120
4013
  const lock = `${this.filePath(sessionId)}.lock`;
3121
4014
  for (let attempt = 0; ; attempt++) {
3122
4015
  try {
3123
- await mkdir4(lock);
4016
+ await mkdir6(lock);
3124
4017
  break;
3125
4018
  } catch (error) {
3126
4019
  if (error.code !== "EEXIST" || attempt >= 200) throw error;
@@ -3129,7 +4022,7 @@ var HookStagingStore = class {
3129
4022
  () => 0
3130
4023
  );
3131
4024
  if (age > STAGING_LOCK_STALE_MS) {
3132
- await rm3(lock, { recursive: true, force: true });
4025
+ await rm4(lock, { recursive: true, force: true });
3133
4026
  continue;
3134
4027
  }
3135
4028
  await new Promise((resolveWait) => setTimeout(resolveWait, 10));
@@ -3138,7 +4031,7 @@ var HookStagingStore = class {
3138
4031
  try {
3139
4032
  return await operation();
3140
4033
  } finally {
3141
- await rm3(lock, { recursive: true, force: true });
4034
+ await rm4(lock, { recursive: true, force: true });
3142
4035
  }
3143
4036
  }
3144
4037
  async serialize(operation) {
@@ -3156,7 +4049,7 @@ var HookStagingStore = class {
3156
4049
  }
3157
4050
  filePath(sessionId) {
3158
4051
  const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "default";
3159
- return join6(this.directory, `${safe}.json`);
4052
+ return join8(this.directory, `${safe}.json`);
3160
4053
  }
3161
4054
  };
3162
4055
  function normalize2(path) {
@@ -3169,39 +4062,44 @@ function parsePaths(raw) {
3169
4062
 
3170
4063
  // src/hooks/runtime.ts
3171
4064
  var CONTEXT_BLOCK_MAX_CHARS = 1800;
4065
+ var STANDALONE_FREE = /* @__PURE__ */ new Set(["session", "test-command"]);
3172
4066
  var HOOK_TASK_FILES = 2;
3173
4067
  var PROMPT_FACTS = 3;
4068
+ var HOOK_SPANS_PER_FILE = 2;
3174
4069
  var CONTEXT_HOOK_BUDGET_MS = 15e3;
3175
4070
  async function resolveNamespace(repositoryRoot) {
3176
4071
  if (process.env.CODEBUDDY_NAMESPACE) return process.env.CODEBUDDY_NAMESPACE;
3177
- const file = await readConfigFile(repositoryRoot);
3178
- return file.namespace ?? basename(repositoryRoot);
4072
+ const file2 = await readConfigFile(repositoryRoot);
4073
+ return file2.namespace ?? basename(repositoryRoot);
3179
4074
  }
3180
4075
  async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
4076
+ if (skipPromptRetrieval(input.prompt)) return "";
3181
4077
  const started = Date.now();
3182
4078
  const repositoryRoot = hookRoot(input.cwd);
3183
4079
  const namespace = await resolveNamespace(repositoryRoot);
3184
4080
  const bootstrap = await buildBootstrapContext({ repositoryRoot, namespace });
3185
- let beforeEdit = null;
3186
- const semantics = input.prompt ? await openHookSemantics(repositoryRoot) : null;
3187
- try {
3188
- beforeEdit = await buildBeforeEditContext({
3189
- repositoryRoot,
3190
- namespace,
3191
- // Git changes are the targets only without a prompt; a prompt that
3192
- // matches no file gets none (an explicit useGit would force them back).
3193
- ...input.prompt ? { task: input.prompt } : {},
3194
- ...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
3195
- ...semantics?.reranker ? { reranker: semantics.reranker } : {}
3196
- });
3197
- } catch {
3198
- beforeEdit = null;
3199
- } finally {
3200
- await semantics?.close();
3201
- }
3202
4081
  const ledger = new SessionCapsuleLedger(repositoryRoot);
3203
4082
  const sessionId = input.session_id ?? "default";
3204
4083
  if (input.ephemeral_context) await ledger.reset(sessionId);
4084
+ const followUp = input.prompt !== void 0 && input.session_id !== void 0 && isFollowUpPrompt(input.prompt) && await ledger.delivered(sessionId);
4085
+ let beforeEdit = null;
4086
+ const semantics = input.prompt && !followUp ? await openHookSemantics(repositoryRoot) : null;
4087
+ if (!followUp)
4088
+ try {
4089
+ beforeEdit = await buildBeforeEditContext({
4090
+ repositoryRoot,
4091
+ namespace,
4092
+ // Git changes are the targets only without a prompt; a prompt that
4093
+ // matches no file gets none (an explicit useGit would force them back).
4094
+ ...input.prompt ? { task: input.prompt } : {},
4095
+ ...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
4096
+ ...semantics?.reranker ? { reranker: semantics.reranker } : {}
4097
+ });
4098
+ } catch {
4099
+ beforeEdit = null;
4100
+ } finally {
4101
+ await semantics?.close();
4102
+ }
3205
4103
  const { text, emitted } = await renderContextBlock(
3206
4104
  bootstrap,
3207
4105
  beforeEdit,
@@ -3219,6 +4117,10 @@ async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
3219
4117
  await ledger.markSent(sessionId, emitted);
3220
4118
  return text;
3221
4119
  }
4120
+ function isFollowUpPrompt(prompt) {
4121
+ const words2 = prompt.trim().split(/\s+/).filter(Boolean);
4122
+ return words2.length <= 6 && taskTerms(prompt).length <= 3;
4123
+ }
3222
4124
  function recallTrace(input, beforeEdit, emitted, text, started, late) {
3223
4125
  const shown = new Set(
3224
4126
  emitted.filter((c) => c.key.startsWith("fact:")).map((c) => c.key.slice(5))
@@ -3234,25 +4136,43 @@ function recallTrace(input, beforeEdit, emitted, text, started, late) {
3234
4136
  tokensOut: estimateTokens(text),
3235
4137
  ...input.session_id ? { session: input.session_id } : {},
3236
4138
  detail: {
3237
- prompt: clip2(prompt, 120),
4139
+ prompt: clip3(prompt, 120),
3238
4140
  facts: facts.map((fact) => fact.summary.slice(0, 100)),
3239
4141
  dense: facts.filter((fact) => fact.reason.includes("semantic")).length,
3240
4142
  lexical: facts.filter((fact) => /task term|target path/.test(fact.reason)).length,
3241
4143
  files: (beforeEdit?.targetPaths ?? []).slice(0, 3),
3242
4144
  sections: emitted.length,
3243
- ...text.trim() ? { block: clip2(redactSensitive(text.trim()), 2e3) } : {},
4145
+ ...text.trim() ? { block: clip3(redactSensitive(text.trim()), 2e3) } : {},
3244
4146
  ...late ? { dropped: "built after the hook budget" } : {}
3245
4147
  }
3246
4148
  };
3247
4149
  }
3248
- function clip2(text, max) {
4150
+ function clip3(text, max) {
3249
4151
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
3250
4152
  }
3251
4153
  async function runStageHook(input) {
3252
4154
  const repositoryRoot = hookRoot(input.cwd);
3253
- const paths = extractEditedPaths(input.tool_input).map((path) => toRepoRelative(repositoryRoot, path)).filter((path) => path !== null);
3254
- await new HookStagingStore(repositoryRoot).add(input.session_id ?? "default", paths);
3255
- return paths;
4155
+ const paths2 = extractEditedPaths(input.tool_input).map((path) => toRepoRelative(repositoryRoot, path)).filter((path) => path !== null);
4156
+ await new HookStagingStore(repositoryRoot).add(input.session_id ?? "default", paths2);
4157
+ if (input.journal_event_id)
4158
+ await appendHostEvents(
4159
+ repositoryRoot,
4160
+ input.session_id ?? "default",
4161
+ paths2.map((path, index) => ({
4162
+ id: `${input.journal_event_id}:${index}`,
4163
+ kind: "edit",
4164
+ path
4165
+ }))
4166
+ );
4167
+ if (input.journal_events?.length) await runCheckpointHook(input).catch(() => null);
4168
+ if (input.transcript_path) {
4169
+ await ingestTranscript(
4170
+ repositoryRoot,
4171
+ input.session_id ?? "default",
4172
+ input.transcript_path
4173
+ ).catch(() => null);
4174
+ }
4175
+ return paths2;
3256
4176
  }
3257
4177
  async function llmMemoryOptions(repositoryRoot) {
3258
4178
  try {
@@ -3268,15 +4188,20 @@ async function runCaptureHook(input) {
3268
4188
  const repositoryRoot = hookRoot(input.cwd);
3269
4189
  const staging = new HookStagingStore(repositoryRoot);
3270
4190
  const sessionId = input.session_id ?? "default";
4191
+ if (input.journal_events?.length) await runCheckpointHook(input).catch(() => null);
4192
+ if (input.transcript_path) {
4193
+ await ingestTranscript(repositoryRoot, sessionId, input.transcript_path).catch(() => null);
4194
+ }
3271
4195
  const claimed = await staging.claim(sessionId);
3272
4196
  if (claimed === null) return { changedFiles: [], result: null };
3273
4197
  const changedFiles = claimed;
3274
4198
  try {
3275
- const prompts = input.transcript_path ? await turnUserPrompts(input.transcript_path) : [];
4199
+ const transcript = input.transcript_path ? await readCaptureTurn(input.transcript_path) : null;
4200
+ const prompts = transcript?.prompts ?? input.user_prompts ?? [];
3276
4201
  const { rules, revocations, policies } = extractUserRules(prompts);
3277
4202
  let investigation = false;
3278
4203
  if (changedFiles.length === 0) {
3279
- const assistant = input.transcript_path ? await lastAssistantMessage(input.transcript_path) : input.last_assistant_message?.trim() ?? "";
4204
+ const assistant = transcript?.assistant ?? input.last_assistant_message?.trim() ?? "";
3280
4205
  investigation = assistant.length >= INVESTIGATION_MIN_CHARS && NAMES_CODE.test(assistant);
3281
4206
  if (!investigation && rules.length === 0 && revocations.length === 0 && policies.length === 0) {
3282
4207
  await staging.acknowledge(sessionId);
@@ -3285,11 +4210,11 @@ async function runCaptureHook(input) {
3285
4210
  }
3286
4211
  const namespace = await resolveNamespace(repositoryRoot);
3287
4212
  const summary = await deriveSummary(
3288
- input.transcript_path,
4213
+ void 0,
3289
4214
  changedFiles,
3290
- input.last_assistant_message
4215
+ transcript?.assistant ?? input.last_assistant_message
3291
4216
  );
3292
- const verification = input.transcript_path ? await turnVerification(input.transcript_path) : verificationLines(
4217
+ const verification = transcript ? transcript.verification : verificationLines(
3293
4218
  (input.shell_commands ?? []).map(({ command, exit_code }) => ({
3294
4219
  command,
3295
4220
  ok: typeof exit_code === "number" ? exit_code === 0 : void 0
@@ -3339,26 +4264,26 @@ var SYNC_BUDGET_MS = 5 * 6e4;
3339
4264
  async function runSyncHook(repositoryRoot, options = {}) {
3340
4265
  const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
3341
4266
  if (!config.postgresUrl) return null;
3342
- const lock = join7(repositoryRoot, ".codebuddy", "cache", "sync.lock");
3343
- const dirty = join7(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
3344
- await mkdir5(dirname5(lock), { recursive: true });
4267
+ const lock = join9(repositoryRoot, ".codebuddy", "cache", "sync.lock");
4268
+ const dirty = join9(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
4269
+ await mkdir7(dirname6(lock), { recursive: true });
3345
4270
  if (!await takeLock(lock)) {
3346
4271
  await writeFile(dirty, "").catch(() => void 0);
3347
4272
  return null;
3348
4273
  }
3349
- const { createRuntime } = await import("./operations-WJR7AZZF.js");
4274
+ const { createRuntime } = await import("./operations-6Z6ZDVBP.js");
3350
4275
  const runtime = await createRuntime(config, { repositoryRoot }).catch(async (error) => {
3351
- await rm4(lock, { recursive: true, force: true });
4276
+ await rm5(lock, { recursive: true, force: true });
3352
4277
  throw error;
3353
4278
  });
3354
4279
  try {
3355
4280
  let result;
3356
4281
  let round = 0;
3357
4282
  do {
3358
- await rm4(dirty, { force: true });
4283
+ await rm5(dirty, { force: true });
3359
4284
  const started = Date.now();
3360
4285
  const reindex = await runtime.memory.reindexMemory(options.respace ? {} : { respace: false });
3361
- const drained = await pTimeout2(runtime.memory.getWorker().drain(), {
4286
+ const drained = await pTimeout3(runtime.memory.getWorker().drain(), {
3362
4287
  milliseconds: SYNC_BUDGET_MS
3363
4288
  }).then(
3364
4289
  () => true,
@@ -3384,7 +4309,9 @@ async function runSyncHook(repositoryRoot, options = {}) {
3384
4309
  ));
3385
4310
  if (config.provider.type === "local") {
3386
4311
  const started = Date.now();
4312
+ const vectorStarted = Date.now();
3387
4313
  const vectors = await syncFileVectors(repositoryRoot, SYNC_BUDGET_MS);
4314
+ await syncReuseVectors(repositoryRoot, SYNC_BUDGET_MS - (Date.now() - vectorStarted));
3388
4315
  if (vectors) {
3389
4316
  await recordTrace(repositoryRoot, {
3390
4317
  kind: "sync",
@@ -3398,13 +4325,13 @@ async function runSyncHook(repositoryRoot, options = {}) {
3398
4325
  return result;
3399
4326
  } finally {
3400
4327
  await runtime.close();
3401
- await rm4(lock, { recursive: true, force: true });
4328
+ await rm5(lock, { recursive: true, force: true });
3402
4329
  }
3403
4330
  }
3404
4331
  var SYNC_LOCK_STALE_MS = SYNC_BUDGET_MS + 6e4;
3405
4332
  async function takeLock(path) {
3406
4333
  try {
3407
- await mkdir5(path);
4334
+ await mkdir7(path);
3408
4335
  return true;
3409
4336
  } catch {
3410
4337
  const age = await stat4(path).then(
@@ -3412,8 +4339,8 @@ async function takeLock(path) {
3412
4339
  () => 0
3413
4340
  );
3414
4341
  if (age < SYNC_LOCK_STALE_MS) return false;
3415
- await rm4(path, { recursive: true, force: true });
3416
- return mkdir5(path).then(
4342
+ await rm5(path, { recursive: true, force: true });
4343
+ return mkdir7(path).then(
3417
4344
  () => true,
3418
4345
  () => false
3419
4346
  );
@@ -3427,22 +4354,56 @@ async function runResetHook(input, emit) {
3427
4354
  if (input.hook_event_name !== "SessionStart") return "";
3428
4355
  const namespace = await resolveNamespace(repositoryRoot);
3429
4356
  const bootstrap = await buildBootstrapContext({ repositoryRoot, namespace });
3430
- const { text, emitted } = await renderContextBlock(
4357
+ const { text: block, emitted } = await renderContextBlock(
3431
4358
  bootstrap,
3432
4359
  null,
3433
4360
  ledger,
3434
4361
  sessionId,
3435
4362
  input.session_id !== void 0
3436
4363
  );
4364
+ let task = "";
4365
+ if (input.source === "compact") {
4366
+ if (input.transcript_path) {
4367
+ await ingestTranscript(repositoryRoot, sessionId, input.transcript_path).catch(() => null);
4368
+ }
4369
+ const checkpoint = await readCheckpoint(repositoryRoot, sessionId).catch(() => null);
4370
+ if (checkpoint?.goal) {
4371
+ task = await renderCheckpointNow(
4372
+ repositoryRoot,
4373
+ checkpoint,
4374
+ "this session, before compaction"
4375
+ );
4376
+ }
4377
+ }
4378
+ const text = [block, task].filter(Boolean).join("\n");
3437
4379
  if (text.trim()) await emit?.(text);
3438
4380
  await ledger.markSent(sessionId, emitted);
3439
4381
  return text;
3440
4382
  }
4383
+ async function runCheckpointHook(input) {
4384
+ const repositoryRoot = hookRoot(input.cwd);
4385
+ if (input.transcript_path)
4386
+ await ingestTranscript(repositoryRoot, input.session_id ?? "default", input.transcript_path);
4387
+ if (input.journal_events?.length)
4388
+ await appendHostEvents(repositoryRoot, input.session_id ?? "default", input.journal_events);
4389
+ }
4390
+ async function runResumeHook(input, emit) {
4391
+ const repositoryRoot = hookRoot(input.cwd);
4392
+ const checkpoint = await latestUnfinished(repositoryRoot, input.session_id);
4393
+ if (!checkpoint) return "";
4394
+ const text = await renderCheckpointNow(
4395
+ repositoryRoot,
4396
+ checkpoint,
4397
+ `unfinished, from session ${checkpoint.session}`
4398
+ );
4399
+ await emit?.(text);
4400
+ return text;
4401
+ }
3441
4402
  function extractEditedPaths(toolInput) {
3442
4403
  if (!toolInput) return [];
3443
- const paths = [];
4404
+ const paths2 = [];
3444
4405
  const push = (value) => {
3445
- if (typeof value === "string" && value.trim()) paths.push(value);
4406
+ if (typeof value === "string" && value.trim()) paths2.push(value);
3446
4407
  };
3447
4408
  push(toolInput.file_path);
3448
4409
  push(toolInput.filePath);
@@ -3460,12 +4421,12 @@ function extractEditedPaths(toolInput) {
3460
4421
  if (edit && typeof edit === "object") push(edit.file_path);
3461
4422
  }
3462
4423
  }
3463
- return [...new Set(paths)];
4424
+ return [...new Set(paths2)];
3464
4425
  }
3465
4426
  function toRepoRelative(repositoryRoot, path) {
3466
- const absolute = isAbsolute(path) ? resolve9(path) : resolve9(repositoryRoot, path);
3467
- const rel = relative5(repositoryRoot, absolute);
3468
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep5}`) || isAbsolute(rel)) return null;
4427
+ const absolute = isAbsolute2(path) ? resolve10(path) : resolve10(repositoryRoot, path);
4428
+ const rel = relative6(repositoryRoot, absolute);
4429
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute2(rel)) return null;
3469
4430
  return rel.replace(/\\/g, "/").replace(/^\.\//, "");
3470
4431
  }
3471
4432
  async function deriveSummary(transcriptPath, changedFiles, lastAssistantText) {
@@ -3473,71 +4434,36 @@ async function deriveSummary(transcriptPath, changedFiles, lastAssistantText) {
3473
4434
  const assistantText = transcriptPath ? await lastAssistantMessage(transcriptPath) : lastAssistantText?.trim().slice(0, 2e3) ?? "";
3474
4435
  return [assistantText, fileList].filter(Boolean).join("\n\n");
3475
4436
  }
3476
- var VERIFICATION_SEGMENT = /^\s*(?:\w+=\S*\s+)*(?:npm|pnpm|yarn|bun|npx|cargo|go|make|mvn|gradle|\.\/gradlew|tsc|vitest|jest|pytest|python3? -m pytest|ruff|mypy|eslint|biome)\b.*\b(?:test|tests|lint|typecheck|tsc|build|check|vet|clippy|vitest|jest|pytest|ruff|mypy|eslint|biome)\b/;
3477
- function isVerificationCommand(command) {
3478
- return command.split(/&&|\|\||;/).some((segment) => VERIFICATION_SEGMENT.test(segment));
3479
- }
3480
4437
  async function turnVerification(transcriptPath) {
3481
- let raw;
3482
- try {
3483
- raw = await readFile7(transcriptPath, "utf8");
3484
- } catch {
3485
- return [];
3486
- }
3487
- let commands = /* @__PURE__ */ new Map();
3488
- const outcomes = /* @__PURE__ */ new Map();
3489
- for (const line of raw.split("\n")) {
3490
- let entry;
3491
- try {
3492
- entry = JSON.parse(line);
3493
- } catch {
3494
- continue;
3495
- }
3496
- const message = entry.message ?? {};
3497
- const content = message.content;
3498
- if (message.role === "user") {
3499
- const isPrompt = typeof content === "string" || Array.isArray(content) && content.some((block) => block?.type === "text");
3500
- if (isPrompt) {
3501
- commands = /* @__PURE__ */ new Map();
3502
- outcomes.clear();
3503
- }
3504
- for (const block of Array.isArray(content) ? content : []) {
3505
- if (block?.type === "tool_result" && typeof block.tool_use_id === "string") {
3506
- outcomes.set(block.tool_use_id, block.is_error !== true);
3507
- }
3508
- }
3509
- continue;
3510
- }
3511
- for (const block of Array.isArray(content) ? content : []) {
3512
- const command = block?.input?.command;
3513
- if (block?.type !== "tool_use" || block.name !== "Bash" || typeof command !== "string") {
3514
- continue;
3515
- }
3516
- if (isVerificationCommand(command)) commands.set(block.id, command);
3517
- }
3518
- }
3519
- return verificationLines(
3520
- [...commands].map(([id, command]) => ({ command, ok: outcomes.get(id) }))
3521
- );
4438
+ return (await readCaptureTurn(transcriptPath)).verification;
3522
4439
  }
3523
4440
  function verificationLines(runs) {
3524
- const lines = runs.filter((run) => isVerificationCommand(run.command)).map(({ command, ok }) => {
4441
+ const lines = runs.filter((run2) => isVerificationCommand(run2.command)).map(({ command, ok }) => {
3525
4442
  const label = ok === void 0 ? "no result" : ok ? "passed" : "failed";
3526
4443
  return `${command.replace(/\s+/g, " ").trim().slice(0, 120)}: ${label}`;
3527
4444
  });
3528
4445
  return lines.slice(-10);
3529
4446
  }
3530
4447
  var MAX_RULE_PROMPT_CHARS = 1500;
3531
- var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
4448
+ var TAGGED_BLOCK2 = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
3532
4449
  async function turnUserPrompts(transcriptPath) {
4450
+ return (await readCaptureTurn(transcriptPath)).prompts;
4451
+ }
4452
+ async function lastAssistantMessage(transcriptPath) {
4453
+ return (await readCaptureTurn(transcriptPath)).assistant;
4454
+ }
4455
+ async function readCaptureTurn(transcriptPath) {
3533
4456
  let raw;
3534
4457
  try {
3535
- raw = await readFile7(transcriptPath, "utf8");
4458
+ raw = await readFile10(transcriptPath, "utf8");
3536
4459
  } catch {
3537
- return [];
4460
+ return { prompts: [], assistant: "", verification: [] };
3538
4461
  }
3539
4462
  let prompts = [];
3540
4463
  let answered = false;
4464
+ let assistant = "";
4465
+ const commands = /* @__PURE__ */ new Map();
4466
+ const outcomes = /* @__PURE__ */ new Map();
3541
4467
  for (const line of raw.split("\n")) {
3542
4468
  let entry;
3543
4469
  try {
@@ -3545,43 +4471,42 @@ async function turnUserPrompts(transcriptPath) {
3545
4471
  } catch {
3546
4472
  continue;
3547
4473
  }
3548
- const message = entry.message ?? {};
3549
- if (message.role === "assistant") {
4474
+ const message = entry.message ?? entry;
4475
+ const role = message.role ?? entry.type;
4476
+ const content = message.content;
4477
+ const blocks = Array.isArray(content) ? content : [];
4478
+ if (role === "assistant") {
3550
4479
  answered = true;
3551
- continue;
3552
- }
3553
- if (message.role !== "user" || entry.isMeta === true) continue;
3554
- const text = extractText(message.content).replace(TAGGED_BLOCK, " ").trim();
3555
- if (!text) continue;
3556
- if (answered) {
3557
- prompts = [];
3558
- answered = false;
3559
- }
3560
- if (text.length <= MAX_RULE_PROMPT_CHARS) prompts.push(text);
3561
- }
3562
- return prompts;
3563
- }
3564
- async function lastAssistantMessage(transcriptPath) {
3565
- try {
3566
- const raw = await readFile7(transcriptPath, "utf8");
3567
- let latest = "";
3568
- for (const line of raw.split("\n")) {
3569
- const trimmed = line.trim();
3570
- if (!trimmed) continue;
3571
- try {
3572
- const entry = JSON.parse(trimmed);
3573
- const message = entry.message ?? entry;
3574
- const role = message.role ?? entry.type;
3575
- if (role !== "assistant") continue;
3576
- const text = extractText(message.content);
3577
- if (text) latest = text;
3578
- } catch {
4480
+ const text = extractText(content);
4481
+ if (text) assistant = text.slice(0, 2e3);
4482
+ for (const block of blocks) {
4483
+ if (block?.type === "tool_use" && block.name === "Bash" && typeof block.id === "string" && typeof block.input?.command === "string" && isVerificationCommand(block.input.command))
4484
+ commands.set(block.id, block.input.command);
4485
+ }
4486
+ } else if (role === "user") {
4487
+ const text = entry.isMeta === true ? "" : extractText(content).replace(TAGGED_BLOCK2, " ").trim();
4488
+ if (text) {
4489
+ commands.clear();
4490
+ outcomes.clear();
4491
+ if (answered) {
4492
+ prompts = [];
4493
+ answered = false;
4494
+ }
4495
+ if (text.length <= MAX_RULE_PROMPT_CHARS) prompts.push(text);
4496
+ }
4497
+ for (const block of blocks) {
4498
+ if (block?.type === "tool_result" && typeof block.tool_use_id === "string")
4499
+ outcomes.set(block.tool_use_id, block.is_error !== true);
3579
4500
  }
3580
4501
  }
3581
- return latest.slice(0, 2e3);
3582
- } catch {
3583
- return "";
3584
4502
  }
4503
+ return {
4504
+ prompts,
4505
+ assistant,
4506
+ verification: verificationLines(
4507
+ [...commands].map(([id, command]) => ({ command, ok: outcomes.get(id) }))
4508
+ )
4509
+ };
3585
4510
  }
3586
4511
  function extractText(content) {
3587
4512
  if (typeof content === "string") return content;
@@ -3607,15 +4532,17 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
3607
4532
  }
3608
4533
  }
3609
4534
  ] : [],
3610
- { priority: 4, capsule: { key: "project", text: `project: ${bootstrap.project.namespace}` } },
3611
- {
3612
- priority: 4,
3613
- capsule: {
3614
- key: "plan",
3615
- text: `plan: ${bootstrap.plan ? `${bootstrap.plan.title} (${bootstrap.plan.status})` : "none"}`
4535
+ // Benchmark v3: "project: <name>", "plan: none" and the architecture size
4536
+ // were re-read every turn and never acted on, so only an active plan is shown.
4537
+ ...bootstrap.plan ? [
4538
+ {
4539
+ priority: 4,
4540
+ capsule: {
4541
+ key: "plan",
4542
+ text: `plan: ${bootstrap.plan.title} (${bootstrap.plan.status}); policy: ${bootstrap.policy}`
4543
+ }
3616
4544
  }
3617
- },
3618
- { priority: 4, capsule: { key: "plan-policy", text: `plan policy: ${bootstrap.policy}` } }
4545
+ ] : []
3619
4546
  ];
3620
4547
  const policyRules = beforeEdit?.policyRules ?? [];
3621
4548
  if (policyRules.length > 0) {
@@ -3673,58 +4600,61 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
3673
4600
  capsule: { key: "test-command", text: `test command: ${beforeEdit.testCommand}` }
3674
4601
  });
3675
4602
  }
3676
- capsules.push({
3677
- priority: 8,
3678
- capsule: {
3679
- key: "architecture",
3680
- text: `architecture: ${bootstrap.architecture.moduleCount} modules, ${bootstrap.architecture.edgeCount} edges`
3681
- }
3682
- });
3683
- const seen = await ledger.seen(
3684
- sessionId,
3685
- capsules.map((entry) => entry.capsule)
3686
- );
3687
- const items = [
3688
- { priority: 0, text: "## CodeBuddy project context (auto-recalled)" }
3689
- ];
3690
- const unchanged = [];
3691
- let newFacts = 0;
3692
- for (const { priority, capsule } of capsules) {
3693
- if (seen.has(capsule.key)) unchanged.push(capsule);
3694
- else if (ranked && capsule.key.startsWith("fact:") && ++newFacts > PROMPT_FACTS) continue;
3695
- else items.push({ priority, text: capsule.text, capsule });
3696
- }
3697
- if (unchanged.length > 0) {
3698
- const avoided = unchanged.reduce((sum, capsule) => sum + estimateTokens(capsule.text), 0);
3699
- items.push({
3700
- priority: 10,
3701
- text: `unchanged since earlier in this session (not resent, ~${avoided} tokens): ${describeKeys(unchanged.map((c) => c.key))}`
3702
- });
3703
- }
3704
4603
  if (beforeEdit && beforeEdit.targetPaths.length > 0) {
3705
4604
  const targetLabel = beforeEdit.pathSource === "git" ? "current changes" : "task files";
3706
4605
  const tests = (beforeEdit.tests ?? []).map((entry) => entry.test);
3707
- items.push({
4606
+ const ranges = new Map(
4607
+ (beforeEdit.symbols ?? []).map((file2) => [
4608
+ file2.path,
4609
+ (file2.spans ?? []).filter((span) => span.name).slice(0, HOOK_SPANS_PER_FILE).map((span) => `${span.name} L${span.startLine}-${span.endLine}`)
4610
+ ])
4611
+ );
4612
+ const describe = (path) => {
4613
+ const spans = ranges.get(path) ?? [];
4614
+ return spans.length > 0 ? `${path} (${spans.join(", ")})` : path;
4615
+ };
4616
+ capsules.push({
3708
4617
  priority: 3,
3709
- text: bound(
3710
- `${targetLabel}: ${beforeEdit.targetPaths.slice(0, beforeEdit.pathSource === "task" ? HOOK_TASK_FILES : 8).join(", ")}` + (tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""),
3711
- 500
3712
- )
4618
+ capsule: {
4619
+ key: "task-files",
4620
+ text: bound(
4621
+ `${targetLabel}: ${beforeEdit.targetPaths.slice(0, beforeEdit.pathSource === "task" ? HOOK_TASK_FILES : 8).map(describe).join(", ")}${tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""}`,
4622
+ 500
4623
+ )
4624
+ }
3713
4625
  });
3714
4626
  const targets = new Set(beforeEdit.targetPaths);
3715
4627
  const risks = beforeEdit.risks.items.filter(
3716
4628
  (item) => targets.has(item.path) && item.score >= RISK_LINE_MIN_SCORE
3717
4629
  );
3718
4630
  if (risks.length > 0) {
3719
- items.push({
4631
+ capsules.push({
3720
4632
  priority: 1,
3721
- text: bound(
3722
- `risk: ${risks.slice(0, 3).map((item) => `${item.path}=${item.score}`).join(", ")}`,
3723
- 500
3724
- )
4633
+ capsule: {
4634
+ key: "risk",
4635
+ text: bound(
4636
+ `risk: ${risks.slice(0, 3).map((item) => `${item.path}=${item.score}`).join(", ")}`,
4637
+ 500
4638
+ )
4639
+ }
3725
4640
  });
3726
4641
  }
3727
4642
  }
4643
+ const seen = await ledger.seen(
4644
+ sessionId,
4645
+ capsules.map((entry) => entry.capsule)
4646
+ );
4647
+ const items = [
4648
+ { priority: 0, text: "## CodeBuddy project context (auto-recalled)" }
4649
+ ];
4650
+ let newFacts = 0;
4651
+ for (const { priority, capsule } of capsules) {
4652
+ if (seen.has(capsule.key)) continue;
4653
+ if (ranked && capsule.key.startsWith("fact:") && ++newFacts > PROMPT_FACTS) continue;
4654
+ else items.push({ priority, text: capsule.text, capsule });
4655
+ }
4656
+ if (!items.some((item) => item.capsule && !STANDALONE_FREE.has(item.capsule.key)))
4657
+ return { text: "", emitted: [] };
3728
4658
  const chosen = /* @__PURE__ */ new Set();
3729
4659
  let used = -1;
3730
4660
  const byPriority = items.map((item, index) => ({ item, index })).sort((a, b) => a.item.priority - b.item.priority || a.index - b.index);
@@ -3740,15 +4670,6 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
3740
4670
  emitted: packed.flatMap((item) => item.capsule ? [item.capsule] : [])
3741
4671
  };
3742
4672
  }
3743
- function describeKeys(keys) {
3744
- const counts = /* @__PURE__ */ new Map();
3745
- for (const key of keys) {
3746
- const kind = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
3747
- counts.set(kind, (counts.get(kind) ?? 0) + 1);
3748
- }
3749
- return [...counts].map(([kind, count]) => count > 1 || kinds.has(kind) ? `${count} ${kind}` : kind).join(", ");
3750
- }
3751
- var kinds = /* @__PURE__ */ new Set(["fact", "incident", "handoff", "rule", "procedure"]);
3752
4673
 
3753
4674
  export {
3754
4675
  PLAN_STATUSES,
@@ -3767,28 +4688,45 @@ export {
3767
4688
  PlanNotFoundError,
3768
4689
  ActivePlanExistsError,
3769
4690
  PlanLifecycle,
3770
- enforceContextBudget,
4691
+ appendHostEvents,
4692
+ journalLag,
4693
+ forgetJournal,
4694
+ readCheckpoint,
4695
+ listJournals,
4696
+ findCodexRollouts,
4697
+ importCodexRollout,
4698
+ renderCheckpointNow,
4699
+ isUnfinished,
4700
+ latestUnfinished,
3771
4701
  WorklogStore,
4702
+ enforceContextBudget,
3772
4703
  buildBootstrapContext,
3773
4704
  buildBeforeEditContext,
4705
+ syncReuseVectors,
4706
+ rankSemanticReuse,
3774
4707
  ReviewStore,
3775
4708
  captureAfterTurn,
3776
4709
  approveReviewItem,
3777
4710
  configuredReranker,
3778
4711
  openHookSemantics,
4712
+ HookStagingStore,
3779
4713
  CONTEXT_HOOK_BUDGET_MS,
3780
4714
  runContextHook,
4715
+ isFollowUpPrompt,
3781
4716
  runStageHook,
3782
4717
  llmMemoryOptions,
3783
4718
  runCaptureHook,
3784
4719
  runSyncHook,
3785
4720
  runResetHook,
4721
+ runCheckpointHook,
4722
+ runResumeHook,
3786
4723
  extractEditedPaths,
3787
4724
  deriveSummary,
3788
4725
  turnVerification,
3789
4726
  verificationLines,
3790
4727
  turnUserPrompts,
3791
4728
  lastAssistantMessage,
4729
+ readCaptureTurn,
3792
4730
  renderContextBlock
3793
4731
  };
3794
- //# sourceMappingURL=chunk-ZNLSCV3J.js.map
4732
+ //# sourceMappingURL=chunk-XSNW6Y3X.js.map