@orquentra/codebuddy 2.9.0 → 2.15.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 (117) hide show
  1. package/ARCHITECTURE.md +98 -0
  2. package/CLAUDE_CODEX.md +33 -0
  3. package/README.md +114 -9
  4. package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
  5. package/dist/{chunk-GXOML4UZ.js → chunk-4BSO3HST.js} +126 -18
  6. package/dist/chunk-4BSO3HST.js.map +1 -0
  7. package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
  8. package/dist/chunk-57642NGU.js.map +1 -0
  9. package/dist/{chunk-ESZXCZIN.js → chunk-6YI2D5F5.js} +33 -5
  10. package/dist/chunk-6YI2D5F5.js.map +1 -0
  11. package/dist/{chunk-GKNYMNFM.js → chunk-7IPSDDO5.js} +10 -3
  12. package/dist/chunk-7IPSDDO5.js.map +1 -0
  13. package/dist/chunk-BTTQWSDS.js +372 -0
  14. package/dist/chunk-BTTQWSDS.js.map +1 -0
  15. package/dist/{chunk-2WH2Q4TA.js → chunk-D5E7SGI5.js} +304 -77
  16. package/dist/chunk-D5E7SGI5.js.map +1 -0
  17. package/dist/chunk-EMWGLKPR.js +45 -0
  18. package/dist/chunk-EMWGLKPR.js.map +1 -0
  19. package/dist/chunk-GQKWUHMA.js +20 -0
  20. package/dist/chunk-GQKWUHMA.js.map +1 -0
  21. package/dist/chunk-ITXE27SZ.js +358 -0
  22. package/dist/chunk-ITXE27SZ.js.map +1 -0
  23. package/dist/{chunk-UK6XTS2Y.js → chunk-JJQDVGF7.js} +23 -23
  24. package/dist/chunk-JJQDVGF7.js.map +1 -0
  25. package/dist/{chunk-F4BJ3DXW.js → chunk-KQCP3ZYH.js} +1273 -331
  26. package/dist/chunk-KQCP3ZYH.js.map +1 -0
  27. package/dist/{chunk-QN76UJP7.js → chunk-KYRM3HPP.js} +89 -21
  28. package/dist/chunk-KYRM3HPP.js.map +1 -0
  29. package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
  30. package/dist/chunk-M75SN4D4.js.map +1 -0
  31. package/dist/{chunk-J4T6HHI4.js → chunk-RZ266NHE.js} +538 -317
  32. package/dist/chunk-RZ266NHE.js.map +1 -0
  33. package/dist/chunk-U7KDM62P.js +43 -0
  34. package/dist/chunk-U7KDM62P.js.map +1 -0
  35. package/dist/chunk-UATJCD2T.js +368 -0
  36. package/dist/chunk-UATJCD2T.js.map +1 -0
  37. package/dist/{chunk-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
  38. package/dist/chunk-UOTKHSQ5.js.map +1 -0
  39. package/dist/{chunk-TLWGZVPC.js → chunk-UR3DF74I.js} +1007 -132
  40. package/dist/chunk-UR3DF74I.js.map +1 -0
  41. package/dist/chunk-UTPC66MV.js +128 -0
  42. package/dist/chunk-UTPC66MV.js.map +1 -0
  43. package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
  44. package/dist/chunk-V7PT6ER5.js.map +1 -0
  45. package/dist/chunk-WHKXUXHE.js +156 -0
  46. package/dist/chunk-WHKXUXHE.js.map +1 -0
  47. package/dist/{chunk-SRA4MDVG.js → chunk-YMXFSHXT.js} +978 -950
  48. package/dist/chunk-YMXFSHXT.js.map +1 -0
  49. package/dist/cli/index.js +23 -9
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
  52. package/dist/config-TXQJCCVI.js +15 -0
  53. package/dist/expand-KWYGNCKZ.js +98 -0
  54. package/dist/expand-KWYGNCKZ.js.map +1 -0
  55. package/dist/file-vectors-MKXTSGGH.js +19 -0
  56. package/dist/index-facts-4MTEHKMN.js +38 -0
  57. package/dist/index-pass-TBDUEZCB.js +14 -0
  58. package/dist/index.d.ts +6 -4
  59. package/dist/index.js +25 -23
  60. package/dist/install-E3JWINTN.js +31 -0
  61. package/dist/langgraph/index.d.ts +1 -1
  62. package/dist/lean-MLTAFS5L.js +29 -0
  63. package/dist/local-75DJECVR.js +48 -0
  64. package/dist/local-llm-MECVJTVU.js +19 -0
  65. package/dist/{main-YJKSYFPT.js → main-RLMKQZW4.js} +999 -328
  66. package/dist/main-RLMKQZW4.js.map +1 -0
  67. package/dist/{memory-TGHVTBME.js → memory-PFFVMTCH.js} +9 -6
  68. package/dist/{memory-TGHVTBME.js.map → memory-PFFVMTCH.js.map} +1 -1
  69. package/dist/{operations-OG4ISWIM.js → operations-STR7S5UO.js} +8 -7
  70. package/dist/operations-STR7S5UO.js.map +1 -0
  71. package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
  72. package/dist/pretool-command-B5GY5T6E.js.map +1 -0
  73. package/dist/prompt-gate-TDTFFQW2.js +8 -0
  74. package/dist/prompt-gate-TDTFFQW2.js.map +1 -0
  75. package/dist/{rerank-IQAEWLQR.js → rerank-XXKWDI5Q.js} +7 -4
  76. package/dist/{rerank-IQAEWLQR.js.map → rerank-XXKWDI5Q.js.map} +1 -1
  77. package/dist/{runtime-GCSEIAVD.js → runtime-SWFWGTQ4.js} +18 -9
  78. package/dist/runtime-SWFWGTQ4.js.map +1 -0
  79. package/dist/summaries-QXWFFDUZ.js +31 -0
  80. package/dist/summaries-QXWFFDUZ.js.map +1 -0
  81. package/dist/{zvec-XDZNZ2OL.js → zvec-F73IBWO5.js} +13 -6
  82. package/dist/zvec-F73IBWO5.js.map +1 -0
  83. package/package.json +13 -3
  84. package/dist/chunk-2WH2Q4TA.js.map +0 -1
  85. package/dist/chunk-6NZEZJYD.js +0 -206
  86. package/dist/chunk-6NZEZJYD.js.map +0 -1
  87. package/dist/chunk-CAEPHLHC.js.map +0 -1
  88. package/dist/chunk-ENKDFLOO.js.map +0 -1
  89. package/dist/chunk-ESZXCZIN.js.map +0 -1
  90. package/dist/chunk-F4BJ3DXW.js.map +0 -1
  91. package/dist/chunk-GKNYMNFM.js.map +0 -1
  92. package/dist/chunk-GXOML4UZ.js.map +0 -1
  93. package/dist/chunk-ITQO632W.js +0 -673
  94. package/dist/chunk-ITQO632W.js.map +0 -1
  95. package/dist/chunk-J4T6HHI4.js.map +0 -1
  96. package/dist/chunk-KNR2P2UY.js.map +0 -1
  97. package/dist/chunk-QN76UJP7.js.map +0 -1
  98. package/dist/chunk-SFMIMLXR.js +0 -63
  99. package/dist/chunk-SFMIMLXR.js.map +0 -1
  100. package/dist/chunk-SRA4MDVG.js.map +0 -1
  101. package/dist/chunk-TLWGZVPC.js.map +0 -1
  102. package/dist/chunk-UK6XTS2Y.js.map +0 -1
  103. package/dist/chunk-W7GAXL6C.js.map +0 -1
  104. package/dist/config-GEZFLXK6.js +0 -12
  105. package/dist/install-BXM56QCD.js +0 -19
  106. package/dist/lean-command-7DFW32MA.js +0 -19
  107. package/dist/main-YJKSYFPT.js.map +0 -1
  108. package/dist/summaries-ML53R7EL.js +0 -21
  109. /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
  110. /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
  111. /package/dist/{install-BXM56QCD.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
  112. /package/dist/{lean-command-7DFW32MA.js.map → index-facts-4MTEHKMN.js.map} +0 -0
  113. /package/dist/{operations-OG4ISWIM.js.map → index-pass-TBDUEZCB.js.map} +0 -0
  114. /package/dist/{pretool-command-PRFOIHMW.js.map → install-E3JWINTN.js.map} +0 -0
  115. /package/dist/{runtime-GCSEIAVD.js.map → lean-MLTAFS5L.js.map} +0 -0
  116. /package/dist/{summaries-ML53R7EL.js.map → local-75DJECVR.js.map} +0 -0
  117. /package/dist/{zvec-XDZNZ2OL.js.map → local-llm-MECVJTVU.js.map} +0 -0
@@ -1,48 +1,52 @@
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-YN7TEIUH.js";
9
+ } from "./chunk-7IPSDDO5.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
16
19
  } from "./chunk-DFV2SK6C.js";
17
20
  import {
18
- INCIDENT_SEVERITIES,
19
- MemoryFileStore,
20
- admitsLexically,
21
- isEligibleMemory,
22
- lexicalTerms,
23
- termOverlap
24
- } from "./chunk-SRA4MDVG.js";
25
- import {
26
- generateEntityId
27
- } from "./chunk-CAEPHLHC.js";
28
- import {
21
+ NON_SOURCE_PATH,
29
22
  TASK_PATH_LIMIT,
23
+ TEST_PATH,
30
24
  countTerms,
31
25
  discoverTaskPaths,
26
+ hashContent,
32
27
  identifierTokens,
28
+ loadFreshIndex,
33
29
  loadProjectArchitectureMap,
34
30
  namedSegments,
35
31
  readBoundedSource,
36
32
  syncFileVectors,
37
33
  taskTerms
38
- } from "./chunk-TLWGZVPC.js";
39
- import {
40
- loadFreshIndex
41
- } from "./chunk-ITQO632W.js";
34
+ } from "./chunk-UR3DF74I.js";
42
35
  import {
43
36
  extractSymbolTable,
44
37
  supportsSymbols
45
38
  } from "./chunk-Y7D7PUBE.js";
39
+ import {
40
+ INCIDENT_SEVERITIES,
41
+ MemoryFileStore,
42
+ admitsLexically,
43
+ isEligibleMemory,
44
+ lexicalTerms,
45
+ termOverlap
46
+ } from "./chunk-YMXFSHXT.js";
47
+ import {
48
+ generateEntityId
49
+ } from "./chunk-EMWGLKPR.js";
46
50
  import {
47
51
  REDACTED_LINE,
48
52
  claimText,
@@ -54,30 +58,31 @@ import {
54
58
  scanFields,
55
59
  scanSensitive
56
60
  } from "./chunk-AL6VBTNA.js";
57
- import {
58
- DEFAULT_RERANK_MODELS
59
- } from "./chunk-HPOR6YXA.js";
60
61
  import {
61
62
  loadPlanPolicy,
62
63
  loadRuntimeConfig,
63
64
  readConfigFile
64
- } from "./chunk-KNR2P2UY.js";
65
+ } from "./chunk-M75SN4D4.js";
65
66
  import {
66
67
  assertNoSymlinkBetween,
67
68
  composeMarkdown,
68
69
  parseFrontMatter,
69
70
  writeAtomic
70
71
  } from "./chunk-CG5B3LYG.js";
72
+ import {
73
+ DEFAULT_RERANK_MODELS,
74
+ LOCAL_EMBEDDING_MODEL
75
+ } from "./chunk-HPOR6YXA.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);
@@ -1412,7 +1474,7 @@ async function buildBeforeEditContext(input) {
1412
1474
  ...hasExplicitTargets ? { useGit: false } : input.useGit !== void 0 ? { useGit: input.useGit } : {}
1413
1475
  });
1414
1476
  const mapPromise = loadProjectArchitectureMap(repositoryRoot, manifest);
1415
- const discovery = !hasExplicitTargets && input.task ? await discoverTaskPaths(repositoryRoot, input.task, manifest, mapPromise) : { paths: [], hits: [], explain: [] };
1477
+ const discovery = !hasExplicitTargets && input.task ? await discoverTaskPaths(repositoryRoot, input.task, manifest, mapPromise) : { paths: [], hits: [], explain: [], confidence: void 0 };
1416
1478
  const taskPaths = discovery.paths;
1417
1479
  const askedTask = Boolean(input.task?.trim());
1418
1480
  const targetPaths = hasExplicitTargets ? resolvedPaths : taskPaths.length > 0 ? taskPaths : askedTask && input.useGit !== true ? [] : resolvedPaths;
@@ -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
  );
@@ -1530,11 +1592,15 @@ async function buildBeforeEditContext(input) {
1530
1592
  tests,
1531
1593
  testCommand,
1532
1594
  diffs,
1533
- explain
1595
+ explain,
1596
+ ...discovery.confidence && !hasExplicitTargets ? { confidence: discovery.confidence } : {},
1597
+ // A plain abstention: no guessed files, and the agent is told to search itself.
1598
+ ...askedTask && !hasExplicitTargets && targetPaths.length === 0 ? { note: NO_MATCH_NOTE } : {}
1534
1599
  };
1535
1600
  const tokens = buildTokenStats(assembled, input.tokenBudget);
1536
1601
  return enforceContextBudget({ ...assembled, tokens });
1537
1602
  }
1603
+ var NO_MATCH_NOTE = "No confident file match for this task; search the repository normally (grep/glob) instead of relying on CodeBuddy targets.";
1538
1604
  async function recallHandoffs(repositoryRoot, input) {
1539
1605
  try {
1540
1606
  return await new WorklogStore(repositoryRoot).recall(input);
@@ -1556,7 +1622,7 @@ function summarizePlan(plan) {
1556
1622
  title: plan.title,
1557
1623
  status: plan.status,
1558
1624
  brief: plan.brief,
1559
- filesToTouch: plan.filesToTouch.map((file) => file.path),
1625
+ filesToTouch: plan.filesToTouch.map((file2) => file2.path),
1560
1626
  risks: plan.risks
1561
1627
  };
1562
1628
  }
@@ -1596,9 +1662,9 @@ function summarizePolicyRules(rules) {
1596
1662
  weight: rule.weight
1597
1663
  }));
1598
1664
  }
1599
- function summarizeMatchingPolicyRules(rules, paths) {
1665
+ function summarizeMatchingPolicyRules(rules, paths2) {
1600
1666
  const matching = rules.filter(
1601
- (rule) => paths.some((path) => matchesPolicyGlob(path, rule.pattern))
1667
+ (rule) => paths2.some((path) => matchesPolicyGlob(path, rule.pattern))
1602
1668
  );
1603
1669
  return summarizePolicyRules(matching);
1604
1670
  }
@@ -1620,9 +1686,9 @@ function summarizeArchitecture(map) {
1620
1686
  }).slice(0, LIMITS2.hotspots);
1621
1687
  return { moduleCount: map.modules.length, edgeCount: map.edges.length, hotspots };
1622
1688
  }
1623
- function computeNeighbours(map, paths) {
1689
+ function computeNeighbours(map, paths2) {
1624
1690
  const result = [];
1625
- for (const path of paths.slice(0, LIMITS2.neighbourPaths)) {
1691
+ for (const path of paths2.slice(0, LIMITS2.neighbourPaths)) {
1626
1692
  const dependsOn = [
1627
1693
  ...new Set(map.edges.filter((edge) => edge.from === path).map((edge) => edge.to))
1628
1694
  ].slice(0, LIMITS2.neighbourEdges);
@@ -1744,8 +1810,8 @@ function headInSignature(lines, symbol) {
1744
1810
  const line = (lines[symbol.startLine - 1] ?? "").trim();
1745
1811
  return !head.endsWith("\u2026") && line.startsWith(head) && /^[\s{=>(]*$/.test(line.slice(head.length));
1746
1812
  }
1747
- async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1748
- if (paths.length === 0) return [];
1813
+ async function readGitDiffs(repositoryRoot, paths2, knownSecrets = []) {
1814
+ if (paths2.length === 0) return [];
1749
1815
  const sections = [];
1750
1816
  try {
1751
1817
  const { stdout } = await execFileAsync(
@@ -1758,7 +1824,7 @@ async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1758
1824
  "--no-ext-diff",
1759
1825
  "--unified=3",
1760
1826
  "--",
1761
- ...paths
1827
+ ...paths2
1762
1828
  ],
1763
1829
  { cwd: repositoryRoot, maxBuffer: 2e6 }
1764
1830
  );
@@ -1773,9 +1839,9 @@ async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1773
1839
  "--no-ext-diff",
1774
1840
  "--unified=3",
1775
1841
  "--",
1776
- ...paths
1842
+ ...paths2
1777
1843
  ],
1778
- ["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...paths]
1844
+ ["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...paths2]
1779
1845
  ]) {
1780
1846
  try {
1781
1847
  const { stdout } = await execFileAsync("git", args, {
@@ -1789,7 +1855,7 @@ async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
1789
1855
  }
1790
1856
  const parsed = parseGitDiff(sections.join("\n"), knownSecrets);
1791
1857
  const known = new Set(parsed.map((diff) => diff.path));
1792
- const untracked = await listUntrackedPaths(repositoryRoot, paths);
1858
+ const untracked = await listUntrackedPaths(repositoryRoot, paths2);
1793
1859
  for (const path of untracked) {
1794
1860
  if (known.has(path)) continue;
1795
1861
  const patch = isCredentialPath(path) ? withheldPatch(path) : await buildUntrackedPatch(repositoryRoot, path, knownSecrets);
@@ -1850,11 +1916,11 @@ function parseGitDiff(diff, knownSecrets = []) {
1850
1916
  ] : [];
1851
1917
  });
1852
1918
  }
1853
- async function listUntrackedPaths(repositoryRoot, paths) {
1919
+ async function listUntrackedPaths(repositoryRoot, paths2) {
1854
1920
  try {
1855
1921
  const { stdout } = await execFileAsync(
1856
1922
  "git",
1857
- ["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths],
1923
+ ["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths2],
1858
1924
  { cwd: repositoryRoot, maxBuffer: 1e6 }
1859
1925
  );
1860
1926
  return stdout.split("\0").filter(Boolean);
@@ -2045,9 +2111,831 @@ function truncate(value, max) {
2045
2111
  return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max - 1)}\u2026`;
2046
2112
  }
2047
2113
 
2048
- // src/memory-extract/engine.ts
2114
+ // src/context/reuse-vectors.ts
2115
+ import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
2116
+ import { dirname as dirname4, join as join5 } from "path";
2117
+ import pTimeout from "p-timeout";
2118
+ var MODEL = `${LOCAL_EMBEDDING_MODEL.id}@${LOCAL_EMBEDDING_MODEL.revision}:${LOCAL_EMBEDDING_MODEL.schema}`;
2119
+ var file = (root) => join5(root, ".codebuddy", "cache", "reuse-vectors.json");
2120
+ var kinds = /* @__PURE__ */ new Set(["function", "class", "method", "const"]);
2121
+ var valid = (vector) => Array.isArray(vector) && vector.length === LOCAL_EMBEDDING_MODEL.dimensions && vector.every(Number.isFinite) && vector.some((value) => value !== 0);
2122
+ async function embedLocal(texts) {
2123
+ const { LocalModelProvider } = await import("./local-ST4WMRCG.js");
2124
+ return (await new LocalModelProvider().embed({ input: texts })).vectors;
2125
+ }
2126
+ async function load(root) {
2127
+ try {
2128
+ const cache = JSON.parse(await readFile6(file(root), "utf8"));
2129
+ if (cache.version === 1 && cache.model === MODEL && Array.isArray(cache.entries))
2130
+ return {
2131
+ ...cache,
2132
+ entries: cache.entries.filter(
2133
+ (entry) => typeof entry.path === "string" && typeof entry.name === "string" && valid(entry.vector)
2134
+ )
2135
+ };
2136
+ } catch {
2137
+ }
2138
+ return { version: 1, model: MODEL, entries: [] };
2139
+ }
2140
+ async function refreshReuseVectors(root, manifest, deadlineAt, embed = embedLocal) {
2141
+ const cache = await load(root);
2142
+ const cached = new Map(cache.entries.map((entry) => [`${entry.path}\0${entry.name}`, entry]));
2143
+ const entries = [];
2144
+ const pending = [];
2145
+ for (const entry of Object.values(manifest.entries)) {
2146
+ if (NON_SOURCE_PATH.test(entry.path) || TEST_PATH.test(entry.path)) continue;
2147
+ const symbols = (entry.symbolTable ?? []).filter(
2148
+ (symbol) => symbol.exported && kinds.has(symbol.kind)
2149
+ );
2150
+ if (!symbols.length) continue;
2151
+ const source = await readBoundedSource(root, entry.path);
2152
+ if (source === null) continue;
2153
+ const lines = source.split("\n");
2154
+ for (const symbol of symbols) {
2155
+ const old = cached.get(`${entry.path}\0${symbol.name}`);
2156
+ const existing = old?.hash === entry.hash ? old : void 0;
2157
+ if (existing) {
2158
+ entries.push(existing);
2159
+ continue;
2160
+ }
2161
+ pending.push({
2162
+ path: entry.path,
2163
+ name: symbol.name,
2164
+ hash: entry.hash,
2165
+ text: `${entry.path}
2166
+ ${symbol.signature}
2167
+ ${lines.slice(Math.max(0, symbol.startLine - 4), symbol.endLine).join("\n")}`.slice(
2168
+ 0,
2169
+ 2e3
2170
+ )
2171
+ });
2172
+ }
2173
+ }
2174
+ let embedded = 0;
2175
+ for (let start = 0; start < pending.length && Date.now() < deadlineAt; start += 16) {
2176
+ const batch = pending.slice(start, start + 16);
2177
+ const vectors = await embed(batch.map((item) => item.text));
2178
+ if (vectors.length !== batch.length || vectors.some((vector) => !valid(vector)))
2179
+ throw new Error("Invalid reuse embedding batch");
2180
+ batch.forEach(({ text: _text, ...entry }, index) => {
2181
+ entries.push({ ...entry, vector: vectors[index] });
2182
+ });
2183
+ embedded += batch.length;
2184
+ }
2185
+ await mkdir3(dirname4(file(root)), { recursive: true });
2186
+ await writeAtomic(file(root), JSON.stringify({ version: 1, model: MODEL, entries }), {
2187
+ mode: 384
2188
+ });
2189
+ return { embedded, pending: pending.length - embedded };
2190
+ }
2191
+ async function syncReuseVectors(root, budgetMs) {
2192
+ if (budgetMs <= 0 || process.env.CODEBUDDY_SEMANTIC === "0") return null;
2193
+ try {
2194
+ const { manifest } = await loadFreshIndex(root);
2195
+ return await refreshReuseVectors(root, manifest, Date.now() + budgetMs);
2196
+ } catch {
2197
+ return null;
2198
+ }
2199
+ }
2200
+ async function rankSemanticReuse(root, task, manifest, embed = embedLocal) {
2201
+ if (process.env.CODEBUDDY_SEMANTIC === "0") return [];
2202
+ const cache = await load(root);
2203
+ const eligible = cache.entries.filter(
2204
+ (entry) => manifest.entries[entry.path]?.hash === entry.hash
2205
+ );
2206
+ if (!eligible.length) return [];
2207
+ try {
2208
+ const vectors = await pTimeout(embed([task]), { milliseconds: 2e3 });
2209
+ const query = vectors[0];
2210
+ if (!query || !valid(query)) return [];
2211
+ const norm = Math.sqrt(query.reduce((sum, value) => sum + value * value, 0));
2212
+ return eligible.map((entry) => {
2213
+ const denominator = norm * Math.sqrt(entry.vector.reduce((sum, value) => sum + value * value, 0));
2214
+ return {
2215
+ path: entry.path,
2216
+ name: entry.name,
2217
+ score: entry.vector.reduce((sum, value, index) => sum + value * (query[index] ?? 0), 0) / denominator
2218
+ };
2219
+ }).sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
2220
+ } catch {
2221
+ return [];
2222
+ }
2223
+ }
2224
+
2225
+ // src/journal/journal.ts
2049
2226
  import { createHash as createHash2 } from "crypto";
2050
- import { resolve as resolve7 } from "path";
2227
+ import {
2228
+ appendFile,
2229
+ chmod,
2230
+ mkdir as mkdir4,
2231
+ open as open2,
2232
+ readdir as readdir3,
2233
+ readFile as readFile7,
2234
+ realpath as realpath3,
2235
+ rm as rm2,
2236
+ stat as stat2
2237
+ } from "fs/promises";
2238
+ import { homedir } from "os";
2239
+ import { isAbsolute, join as join6, relative as relative4, sep as sep4 } from "path";
2240
+ var JOURNAL_VERSION = 1;
2241
+ var JOURNAL_LIMITS = {
2242
+ eventChars: 1e3,
2243
+ journalBytes: 1e6,
2244
+ /** Largest transcript delta read per ingest; the rest is read by the next one. */
2245
+ chunkBytes: 4e6,
2246
+ maxAgeDays: 30,
2247
+ findings: 5,
2248
+ findingChars: 300,
2249
+ verification: 5,
2250
+ constraints: 5,
2251
+ editedPaths: 100
2252
+ };
2253
+ function journalDir(root) {
2254
+ return join6(root, ".codebuddy", "cache", "journal");
2255
+ }
2256
+ function safeSession(session) {
2257
+ const clean2 = session.replace(/[^A-Za-z0-9_.-]/g, "_");
2258
+ if (clean2 === session && clean2.length <= 120 && clean2 !== "." && clean2 !== ".." && clean2 !== "")
2259
+ return clean2;
2260
+ const name = clean2.slice(0, 100);
2261
+ return `${name.replace(/^\.+$/, "session") || "session"}-${createHash2("sha256").update(session).digest("hex").slice(0, 16)}`;
2262
+ }
2263
+ var paths = (root, session) => {
2264
+ const base = join6(journalDir(root), safeSession(session));
2265
+ return {
2266
+ journal: `${base}.jsonl`,
2267
+ cursor: `${base}.cursor.json`,
2268
+ checkpoint: `${base}.checkpoint.json`,
2269
+ lock: `${base}.lock`
2270
+ };
2271
+ };
2272
+ 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/;
2273
+ function isVerificationCommand(command) {
2274
+ return command.split(/&&|\|\||;/).some((segment) => VERIFICATION_SEGMENT.test(segment));
2275
+ }
2276
+ var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
2277
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
2278
+ var clip2 = (text, max) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
2279
+ function clean(text) {
2280
+ const trimmed = text.replace(TAGGED_BLOCK, " ").replace(/\s+/g, " ").trim();
2281
+ return trimmed ? clip2(redactSensitive(trimmed), JOURNAL_LIMITS.eventChars) : null;
2282
+ }
2283
+ function eventsFromLine(line, offset) {
2284
+ let entry;
2285
+ try {
2286
+ entry = JSON.parse(line);
2287
+ } catch {
2288
+ return [];
2289
+ }
2290
+ const message = entry.message ?? {};
2291
+ const at = typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date(0)).toISOString();
2292
+ const lineId = typeof entry.uuid === "string" ? entry.uuid : createHash2("sha1").update(line).digest("hex").slice(0, 16);
2293
+ const blocks = typeof message.content === "string" ? [{ type: "text", text: message.content }] : Array.isArray(message.content) ? message.content : [];
2294
+ const events = [];
2295
+ blocks.forEach((block, index) => {
2296
+ const id = `${lineId}:${index}`;
2297
+ if (message.role === "user") {
2298
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
2299
+ events.push({
2300
+ id,
2301
+ at,
2302
+ kind: "result",
2303
+ ref: block.tool_use_id,
2304
+ ok: block.is_error !== true,
2305
+ offset
2306
+ });
2307
+ } else if (block.type === "text" && entry.isMeta !== true) {
2308
+ const text = clean(String(block.text ?? ""));
2309
+ if (text) events.push({ id, at, kind: "user", text, offset });
2310
+ }
2311
+ return;
2312
+ }
2313
+ if (message.role !== "assistant") return;
2314
+ if (block.type === "text") {
2315
+ const text = clean(String(block.text ?? ""));
2316
+ if (text) events.push({ id, at, kind: "assistant", text, offset });
2317
+ return;
2318
+ }
2319
+ if (block.type !== "tool_use") return;
2320
+ const input = block.input ?? {};
2321
+ const name = String(block.name ?? "");
2322
+ const path = input.file_path ?? input.notebook_path;
2323
+ if (EDIT_TOOLS.has(name) && typeof path === "string") {
2324
+ events.push({ id, at, kind: "edit", path: clip2(path, 500), offset });
2325
+ } else if (name === "Bash" && typeof input.command === "string") {
2326
+ const text = clean(input.command);
2327
+ if (text) {
2328
+ events.push({
2329
+ id,
2330
+ at,
2331
+ kind: "command",
2332
+ text,
2333
+ ...typeof block.id === "string" ? { ref: block.id } : {},
2334
+ offset
2335
+ });
2336
+ }
2337
+ }
2338
+ });
2339
+ return events;
2340
+ }
2341
+ async function readJournal(root, session) {
2342
+ let raw;
2343
+ try {
2344
+ raw = await readFile7(paths(root, session).journal, "utf8");
2345
+ } catch {
2346
+ return [];
2347
+ }
2348
+ const events = [];
2349
+ for (const line of raw.split("\n")) {
2350
+ if (!line) continue;
2351
+ try {
2352
+ const event = JSON.parse(line);
2353
+ if (event.v === JOURNAL_VERSION && typeof event.id === "string") events.push(event);
2354
+ } catch {
2355
+ }
2356
+ }
2357
+ return events;
2358
+ }
2359
+ async function readCursor(path) {
2360
+ try {
2361
+ const cursor = JSON.parse(await readFile7(path, "utf8"));
2362
+ return validCursor(cursor) ? cursor : null;
2363
+ } catch {
2364
+ return null;
2365
+ }
2366
+ }
2367
+ function validCursor(cursor) {
2368
+ 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);
2369
+ }
2370
+ async function headHash(path, length = 256) {
2371
+ const handle = await open2(path, "r");
2372
+ try {
2373
+ const buffer = Buffer.alloc(length);
2374
+ const { bytesRead } = await handle.read(buffer, 0, length, 0);
2375
+ return createHash2("sha1").update(buffer.subarray(0, bytesRead)).digest("hex");
2376
+ } finally {
2377
+ await handle.close();
2378
+ }
2379
+ }
2380
+ async function withLock(lock, run2, waitMs = 0) {
2381
+ const deadline = Date.now() + waitMs;
2382
+ for (; ; ) {
2383
+ try {
2384
+ await mkdir4(lock);
2385
+ break;
2386
+ } catch (error) {
2387
+ if (error.code !== "EEXIST") throw error;
2388
+ const age = Date.now() - await stat2(lock).then(
2389
+ (value) => value.mtimeMs,
2390
+ () => Date.now()
2391
+ );
2392
+ if (age >= 6e4) {
2393
+ await rm2(lock, { recursive: true, force: true });
2394
+ continue;
2395
+ }
2396
+ if (Date.now() >= deadline) return null;
2397
+ await new Promise((resolve11) => setTimeout(resolve11, 10));
2398
+ }
2399
+ }
2400
+ try {
2401
+ return await run2();
2402
+ } finally {
2403
+ await rm2(lock, { recursive: true, force: true });
2404
+ }
2405
+ }
2406
+ async function ingestTranscript(root, session, transcript, now = /* @__PURE__ */ new Date(), parse = eventsFromLine) {
2407
+ const files = paths(root, session);
2408
+ await mkdir4(journalDir(root), { recursive: true, mode: 448 });
2409
+ return withLock(files.lock, async () => {
2410
+ const size = (await stat2(transcript)).size;
2411
+ const previous = await readCheckpoint(root, session);
2412
+ const storedCursor = await readCursor(files.cursor);
2413
+ const cursor = storedCursor ?? (previous?.sourceCursor && validCursor(previous.sourceCursor) ? previous.sourceCursor : null);
2414
+ const headBytes = Math.min(size, 256);
2415
+ const head = size > 0 ? await headHash(transcript, headBytes) : "";
2416
+ const priorHead = cursor?.headBytes !== void 0 ? cursor.headBytes === 0 ? "" : await headHash(transcript, Math.min(size, cursor.headBytes)) : head;
2417
+ const rotated = cursor !== null && (cursor.transcript !== transcript || cursor.offset > size || cursor.head !== priorHead);
2418
+ let offset = cursor && !rotated ? cursor.offset : 0;
2419
+ if (cursor && previous && !rotated && cursor.offset === size && (previous.evidence?.toSeq ?? 0) === cursor.seq) {
2420
+ if (!storedCursor)
2421
+ await writeAtomic(files.cursor, `${JSON.stringify(cursor)}
2422
+ `, { mode: 384 });
2423
+ return { added: 0, offset: cursor.offset, rotated: false, checkpoint: previous };
2424
+ }
2425
+ const existing = await readJournal(root, session);
2426
+ const appended = [];
2427
+ const seen = new Set(existing.map((event) => event.id));
2428
+ let seq = Math.max(existing.at(-1)?.seq ?? 0, cursor?.seq ?? 0, previous?.evidence?.toSeq ?? 0);
2429
+ let added = 0;
2430
+ const handle = await open2(transcript, "r");
2431
+ try {
2432
+ while (offset < size) {
2433
+ const length = Math.min(size - offset, JOURNAL_LIMITS.chunkBytes);
2434
+ const buffer = Buffer.alloc(length);
2435
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
2436
+ const end = buffer.subarray(0, bytesRead).lastIndexOf(10);
2437
+ if (end === -1) {
2438
+ let scan = offset + bytesRead;
2439
+ let boundary = -1;
2440
+ while (scan < size) {
2441
+ const chunk = Buffer.alloc(Math.min(size - scan, JOURNAL_LIMITS.chunkBytes));
2442
+ const read = await handle.read(chunk, 0, chunk.length, scan);
2443
+ if (read.bytesRead === 0) break;
2444
+ const newline = chunk.subarray(0, read.bytesRead).indexOf(10);
2445
+ if (newline !== -1) {
2446
+ boundary = scan + newline;
2447
+ break;
2448
+ }
2449
+ scan += read.bytesRead;
2450
+ }
2451
+ if (boundary === -1) break;
2452
+ const id = `gap:${createHash2("sha256").update(`${transcript}:${offset}:${boundary}`).digest("hex").slice(0, 24)}`;
2453
+ if (!seen.has(id)) {
2454
+ const gap = {
2455
+ v: JOURNAL_VERSION,
2456
+ seq: ++seq,
2457
+ id,
2458
+ at: now.toISOString(),
2459
+ kind: "gap",
2460
+ offset
2461
+ };
2462
+ const torn = !await endsWithNewline(files.journal);
2463
+ await appendFile(files.journal, `${torn ? "\n" : ""}${JSON.stringify(gap)}
2464
+ `, {
2465
+ mode: 384
2466
+ });
2467
+ await chmod(files.journal, 384);
2468
+ seen.add(id);
2469
+ appended.push(gap);
2470
+ added++;
2471
+ }
2472
+ offset = boundary + 1;
2473
+ continue;
2474
+ }
2475
+ const lines = [];
2476
+ let lineStart = 0;
2477
+ for (const line of buffer.subarray(0, end).toString("utf8").split("\n")) {
2478
+ const lineOffset = offset + lineStart;
2479
+ lineStart += Buffer.byteLength(line) + 1;
2480
+ for (const event of parse(line, lineOffset)) {
2481
+ if (seen.has(event.id)) continue;
2482
+ seen.add(event.id);
2483
+ const saved = { ...event, v: JOURNAL_VERSION, seq: ++seq };
2484
+ appended.push(saved);
2485
+ lines.push(JSON.stringify(saved));
2486
+ }
2487
+ }
2488
+ if (lines.length > 0) {
2489
+ const torn = !await endsWithNewline(files.journal);
2490
+ await appendFile(files.journal, `${torn ? "\n" : ""}${lines.join("\n")}
2491
+ `, {
2492
+ mode: 384
2493
+ });
2494
+ await chmod(files.journal, 384);
2495
+ }
2496
+ added += lines.length;
2497
+ offset += end + 1;
2498
+ }
2499
+ } finally {
2500
+ await handle.close();
2501
+ }
2502
+ const next = { v: JOURNAL_VERSION, transcript, offset, head, headBytes, seq };
2503
+ await writeAtomic(files.cursor, `${JSON.stringify(next)}
2504
+ `, { mode: 384 });
2505
+ const events = [...existing, ...appended];
2506
+ const fresh = previous ? events.filter((event) => event.seq > (previous.evidence?.toSeq ?? 0)) : events;
2507
+ if (previous && fresh.length === 0) {
2508
+ await enforceRetention(files.journal);
2509
+ return { added, offset, rotated, checkpoint: previous };
2510
+ }
2511
+ const checkpoint = await persistCheckpoint(root, session, fresh, previous, now, {
2512
+ sourceCursor: next
2513
+ });
2514
+ await enforceRetention(files.journal);
2515
+ void pruneOldJournals(root, now).catch(() => void 0);
2516
+ return { added, offset, rotated, checkpoint };
2517
+ });
2518
+ }
2519
+ async function persistCheckpoint(root, session, fresh, previous, now, metadata = {}) {
2520
+ const derived = deriveCheckpoint(session, fresh, now, previous ?? void 0);
2521
+ const editedPaths = [...new Set(derived.editedPaths.map((path) => repoRelative(root, path)))];
2522
+ const sameTask = previous && derived.evidence?.fromSeq === previous.evidence?.fromSeq;
2523
+ const newlyEdited = fresh.filter((event) => event.kind === "edit" && event.path).map((event) => repoRelative(root, event.path));
2524
+ const hashes = {
2525
+ ...sameTask ? previous.fileHashes : {},
2526
+ ...await fileHashes(root, sameTask ? newlyEdited : editedPaths)
2527
+ };
2528
+ const checkpoint = {
2529
+ ...derived,
2530
+ editedPaths,
2531
+ identity: sameTask && previous.identity || await repoIdentity(root),
2532
+ fileHashes: Object.fromEntries(editedPaths.map((path) => [path, hashes[path] ?? null])),
2533
+ ...metadata
2534
+ };
2535
+ await writeAtomic(paths(root, session).checkpoint, `${JSON.stringify(checkpoint, null, 2)}
2536
+ `, {
2537
+ mode: 384
2538
+ });
2539
+ return checkpoint;
2540
+ }
2541
+ async function appendHostEvents(root, session, input, now = /* @__PURE__ */ new Date()) {
2542
+ if (!input.length) return null;
2543
+ const files = paths(root, session);
2544
+ await mkdir4(journalDir(root), { recursive: true, mode: 448 });
2545
+ return withLock(
2546
+ files.lock,
2547
+ async () => {
2548
+ const previous = await readCheckpoint(root, session);
2549
+ const existing = await readJournal(root, session);
2550
+ const pending = previous ? existing.filter((event) => event.seq > (previous.evidence?.toSeq ?? 0)) : existing;
2551
+ const seen = /* @__PURE__ */ new Set([
2552
+ ...existing.map((event) => event.id),
2553
+ ...previous?.recentHostEventIds ?? []
2554
+ ]);
2555
+ let seq = Math.max(existing.at(-1)?.seq ?? 0, previous?.evidence?.toSeq ?? 0);
2556
+ const fresh = [];
2557
+ for (const candidate of input.slice(0, 100)) {
2558
+ if (!candidate || typeof candidate.id !== "string" || !candidate.id || !["user", "assistant", "edit", "command", "result"].includes(candidate.kind))
2559
+ continue;
2560
+ const id = `host:${createHash2("sha256").update(candidate.id).digest("hex").slice(0, 32)}`;
2561
+ if (seen.has(id)) continue;
2562
+ const text = typeof candidate.text === "string" ? clean(candidate.text) : null;
2563
+ const path = typeof candidate.path === "string" ? clean(candidate.path) : null;
2564
+ if ((candidate.kind === "user" || candidate.kind === "assistant" || candidate.kind === "command") && !text)
2565
+ continue;
2566
+ if (candidate.kind === "edit" && !path) continue;
2567
+ if (candidate.kind === "result" && typeof candidate.ref !== "string") continue;
2568
+ fresh.push({
2569
+ v: JOURNAL_VERSION,
2570
+ seq: ++seq,
2571
+ id,
2572
+ at: now.toISOString(),
2573
+ kind: candidate.kind,
2574
+ offset: -1,
2575
+ ...text ? { text } : {},
2576
+ ...path ? { path: clip2(path, 500) } : {},
2577
+ ...typeof candidate.ref === "string" ? { ref: clip2(candidate.ref, 500) } : {},
2578
+ ...typeof candidate.ok === "boolean" ? { ok: candidate.ok } : {}
2579
+ });
2580
+ seen.add(id);
2581
+ }
2582
+ if (!fresh.length && !pending.length && previous) return { added: 0, checkpoint: previous };
2583
+ if (fresh.length) {
2584
+ const torn = !await endsWithNewline(files.journal);
2585
+ await appendFile(
2586
+ files.journal,
2587
+ `${torn ? "\n" : ""}${fresh.map((event) => JSON.stringify(event)).join("\n")}
2588
+ `,
2589
+ { mode: 384 }
2590
+ );
2591
+ await chmod(files.journal, 384);
2592
+ }
2593
+ const checkpoint = await persistCheckpoint(
2594
+ root,
2595
+ session,
2596
+ [...pending, ...fresh],
2597
+ previous,
2598
+ now,
2599
+ {
2600
+ recentHostEventIds: [
2601
+ .../* @__PURE__ */ new Set([
2602
+ ...previous?.recentHostEventIds ?? [],
2603
+ ...fresh.map((event) => event.id)
2604
+ ])
2605
+ ].slice(-256)
2606
+ }
2607
+ );
2608
+ await enforceRetention(files.journal);
2609
+ void pruneOldJournals(root, now).catch(() => void 0);
2610
+ return { added: fresh.length, checkpoint };
2611
+ },
2612
+ 2e3
2613
+ );
2614
+ }
2615
+ async function endsWithNewline(path) {
2616
+ try {
2617
+ const handle = await open2(path, "r");
2618
+ try {
2619
+ const { size } = await handle.stat();
2620
+ if (size === 0) return true;
2621
+ const byte = Buffer.alloc(1);
2622
+ await handle.read(byte, 0, 1, size - 1);
2623
+ return byte[0] === 10;
2624
+ } finally {
2625
+ await handle.close();
2626
+ }
2627
+ } catch {
2628
+ return true;
2629
+ }
2630
+ }
2631
+ function repoRelative(root, path) {
2632
+ if (!isAbsolute(path)) return path;
2633
+ const rel = relative4(root, path);
2634
+ return rel && rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute(rel) ? rel : path;
2635
+ }
2636
+ async function enforceRetention(journal) {
2637
+ const size = await stat2(journal).then(
2638
+ (s) => s.size,
2639
+ () => 0
2640
+ );
2641
+ if (size <= JOURNAL_LIMITS.journalBytes) return;
2642
+ const lines = (await readFile7(journal, "utf8")).split("\n").filter(Boolean);
2643
+ let bytes = 0;
2644
+ let cut = lines.length;
2645
+ const budget = Math.floor(JOURNAL_LIMITS.journalBytes / 2);
2646
+ while (cut > 0) {
2647
+ const length = Buffer.byteLength(lines[cut - 1]) + 1;
2648
+ if (bytes + length > budget) break;
2649
+ bytes += length;
2650
+ cut--;
2651
+ }
2652
+ const kept = lines.slice(cut);
2653
+ await writeAtomic(journal, `${kept.join("\n")}
2654
+ `, { mode: 384 });
2655
+ }
2656
+ async function pruneOldJournals(root, now) {
2657
+ const cutoff = now.getTime() - JOURNAL_LIMITS.maxAgeDays * 864e5;
2658
+ let removed = 0;
2659
+ for (const name of await readdir3(journalDir(root)).catch(() => [])) {
2660
+ const path = join6(journalDir(root), name);
2661
+ if (await stat2(path).then(
2662
+ (s) => s.mtimeMs,
2663
+ () => now.getTime()
2664
+ ) < cutoff) {
2665
+ await rm2(path, { recursive: true, force: true });
2666
+ removed++;
2667
+ }
2668
+ }
2669
+ return removed;
2670
+ }
2671
+ async function journalLag(root) {
2672
+ const names = await readdir3(journalDir(root)).catch(() => []);
2673
+ return Promise.all(
2674
+ names.filter((name) => name.endsWith(".cursor.json")).map(async (name) => {
2675
+ const cursor = await readCursor(join6(journalDir(root), name));
2676
+ const size = cursor ? await stat2(cursor.transcript).then(
2677
+ (s) => s.size,
2678
+ () => null
2679
+ ) : null;
2680
+ return {
2681
+ session: name.replace(/\.cursor\.json$/, ""),
2682
+ // null: the transcript is gone, so nothing more can be journaled.
2683
+ behindBytes: cursor && size !== null ? Math.max(0, size - cursor.offset) : null
2684
+ };
2685
+ })
2686
+ );
2687
+ }
2688
+ async function forgetJournal(root, session) {
2689
+ if (session === void 0) {
2690
+ await rm2(journalDir(root), { recursive: true, force: true });
2691
+ return;
2692
+ }
2693
+ const files = paths(root, session);
2694
+ await Promise.all(
2695
+ [files.journal, files.cursor, files.checkpoint].map((path) => rm2(path, { force: true }))
2696
+ );
2697
+ }
2698
+ async function readCheckpoint(root, session) {
2699
+ try {
2700
+ const checkpoint = JSON.parse(
2701
+ await readFile7(paths(root, session).checkpoint, "utf8")
2702
+ );
2703
+ return validCheckpoint(checkpoint) && checkpoint.session === session ? checkpoint : null;
2704
+ } catch {
2705
+ return null;
2706
+ }
2707
+ }
2708
+ function validCheckpoint(value) {
2709
+ 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(
2710
+ (items) => Array.isArray(items) && items.every((item) => typeof item === "string")
2711
+ ) && Array.isArray(value.verification) && value.verification.every(
2712
+ (run2) => run2 !== null && typeof run2 === "object" && typeof run2.command === "string" && (run2.ok === null || typeof run2.ok === "boolean") && typeof run2.stale === "boolean"
2713
+ ) && (value.nextAction === null || typeof value.nextAction === "string");
2714
+ }
2715
+ async function listJournals(root) {
2716
+ const names = await readdir3(journalDir(root)).catch(() => []);
2717
+ const sessions = await Promise.all(
2718
+ names.filter((name) => name.endsWith(".checkpoint.json")).map(async (name) => {
2719
+ try {
2720
+ const checkpoint = JSON.parse(
2721
+ await readFile7(join6(journalDir(root), name), "utf8")
2722
+ );
2723
+ return validCheckpoint(checkpoint) ? { session: checkpoint.session, updatedAt: checkpoint.updatedAt } : null;
2724
+ } catch {
2725
+ return null;
2726
+ }
2727
+ })
2728
+ );
2729
+ return sessions.filter((item) => item !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2730
+ }
2731
+ function eventsFromCodexLine(line, offset) {
2732
+ let entry;
2733
+ try {
2734
+ entry = JSON.parse(line);
2735
+ } catch {
2736
+ return [];
2737
+ }
2738
+ const payload = entry.payload ?? {};
2739
+ if (entry.type !== "response_item") return [];
2740
+ const at = typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date(0)).toISOString();
2741
+ const base = typeof payload.id === "string" ? payload.id : createHash2("sha1").update(line).digest("hex").slice(0, 16);
2742
+ if (payload.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
2743
+ const kind = payload.role === "user" ? "user" : "assistant";
2744
+ return (payload.content ?? []).flatMap((item, index) => {
2745
+ if (item.type !== "input_text" && item.type !== "output_text") return [];
2746
+ const text = clean(String(item.text ?? ""));
2747
+ return text ? [{ id: `${base}:${index}`, at, kind, text, offset }] : [];
2748
+ });
2749
+ }
2750
+ if (payload.type === "custom_tool_call" && payload.name === "apply_patch") {
2751
+ const patch = String(payload.input ?? "");
2752
+ const files = [...patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)];
2753
+ return files.map((match, index) => ({
2754
+ id: `${base}:${index}`,
2755
+ at,
2756
+ kind: "edit",
2757
+ path: clip2((match[1] ?? "").trim(), 500),
2758
+ offset
2759
+ }));
2760
+ }
2761
+ return [];
2762
+ }
2763
+ async function firstLine(path) {
2764
+ const handle = await open2(path, "r");
2765
+ try {
2766
+ const buffer = Buffer.alloc(2e6);
2767
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
2768
+ const end = buffer.subarray(0, bytesRead).indexOf(10);
2769
+ return buffer.subarray(0, end === -1 ? bytesRead : end).toString("utf8");
2770
+ } finally {
2771
+ await handle.close();
2772
+ }
2773
+ }
2774
+ async function findCodexRollouts(root, codexHome = join6(homedir(), ".codex"), limit = 300) {
2775
+ const want = await realpath3(root).catch(() => root);
2776
+ const files = [];
2777
+ const walk = async (dir, depth) => {
2778
+ for (const name of await readdir3(dir).catch(() => [])) {
2779
+ const path = join6(dir, name);
2780
+ if (depth < 3) await walk(path, depth + 1);
2781
+ else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
2782
+ files.push({
2783
+ path,
2784
+ mtime: await stat2(path).then(
2785
+ (s) => s.mtimeMs,
2786
+ () => 0
2787
+ )
2788
+ });
2789
+ }
2790
+ }
2791
+ };
2792
+ await walk(join6(codexHome, "sessions"), 0);
2793
+ const found = [];
2794
+ for (const { path } of files.sort((a, b) => b.mtime - a.mtime).slice(0, limit)) {
2795
+ try {
2796
+ const meta = JSON.parse(await firstLine(path));
2797
+ if (meta.type !== "session_meta" || !meta.payload?.cwd || !meta.payload.id) continue;
2798
+ const cwd = await realpath3(meta.payload.cwd).catch(() => meta.payload?.cwd);
2799
+ if (cwd === want) found.push({ path, id: meta.payload.id });
2800
+ } catch {
2801
+ }
2802
+ }
2803
+ return found;
2804
+ }
2805
+ function importCodexRollout(root, rollout, now = /* @__PURE__ */ new Date()) {
2806
+ return ingestTranscript(root, `codex-${rollout.id}`, rollout.path, now, eventsFromCodexLine);
2807
+ }
2808
+ function renderCheckpoint(checkpoint, label, annotate = {}) {
2809
+ const mark = (path) => annotate.missing?.includes(path) ? `${path} (missing now)` : annotate.changed?.includes(path) ? `${path} (changed since)` : path;
2810
+ const lines = [
2811
+ `## CodeBuddy task checkpoint (${label}, ${checkpoint.updatedAt.slice(0, 16)}Z)`,
2812
+ checkpoint.omittedTranscriptLines ? `coverage: ${checkpoint.omittedTranscriptLines} oversized transcript line(s) omitted; consult the original transcript` : "",
2813
+ annotate.identity ?? "",
2814
+ checkpoint.goal ? `goal: ${clip2(checkpoint.goal, 300)}` : "",
2815
+ checkpoint.constraints.length > 0 ? `constraints: ${checkpoint.constraints.join(" ")}` : "",
2816
+ checkpoint.editedPaths.length > 0 ? `edited: ${checkpoint.editedPaths.slice(-8).map(mark).join(", ")}` : "",
2817
+ checkpoint.verification.length > 0 ? `checks: ${checkpoint.verification.map(
2818
+ (run2) => `${run2.command} (${run2.ok === null ? "no result" : run2.ok ? "passed" : "failed"}${run2.stale ? annotate.sourceChanged ? ", stale: source changed" : ", stale: edited after" : ""})`
2819
+ ).join("; ")}` : "",
2820
+ checkpoint.findings.length > 0 ? `findings: ${checkpoint.findings.slice(-3).join(" | ")}` : "",
2821
+ checkpoint.nextAction ? `next: ${checkpoint.nextAction}` : ""
2822
+ ];
2823
+ return clip2(lines.filter(Boolean).join("\n"), 1500);
2824
+ }
2825
+ async function renderCheckpointNow(root, checkpoint, label) {
2826
+ const freshness = await compareFreshness(root, checkpoint.editedPaths, checkpoint.fileHashes);
2827
+ const now = await repoIdentity(root);
2828
+ const then = checkpoint.identity;
2829
+ 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;
2830
+ const sourceChanged = freshness.changed.length > 0 || freshness.missing.length > 0;
2831
+ const rendered = sourceChanged ? {
2832
+ ...checkpoint,
2833
+ verification: checkpoint.verification.map((run2) => ({ ...run2, stale: true }))
2834
+ } : checkpoint;
2835
+ return renderCheckpoint(rendered, label, {
2836
+ changed: freshness.changed,
2837
+ missing: freshness.missing,
2838
+ sourceChanged,
2839
+ ...moved ? { identity: moved } : {}
2840
+ });
2841
+ }
2842
+ function isUnfinished(checkpoint) {
2843
+ if (!checkpoint.goal) return false;
2844
+ return checkpoint.nextAction !== null || [...new Map(checkpoint.verification.map((run2) => [run2.command, run2])).values()].some(
2845
+ (run2) => run2.ok !== true || run2.stale
2846
+ ) || checkpoint.editedPaths.length > 0 && checkpoint.verification.length === 0 || !checkpoint.lastAssistant && checkpoint.verification.length === 0;
2847
+ }
2848
+ async function latestUnfinished(root, excludeSession, now = /* @__PURE__ */ new Date(), maxAgeDays = 3) {
2849
+ const cutoff = now.getTime() - maxAgeDays * 864e5;
2850
+ for (const { session, updatedAt } of await listJournals(root)) {
2851
+ if (session === excludeSession || Date.parse(updatedAt) < cutoff) continue;
2852
+ const checkpoint = await readCheckpoint(root, session);
2853
+ if (checkpoint && isUnfinished(checkpoint)) return checkpoint;
2854
+ }
2855
+ return null;
2856
+ }
2857
+ var CONSTRAINT = /\b(must|never|don't|do not|always|only|without|keep|avoid)\b/i;
2858
+ var NEXT_STEP = /\b(next|remaining|todo|still need|then I(?:'ll| will)|will now)\b/i;
2859
+ function isFollowUp(text) {
2860
+ return text.split(/\s+/).filter(Boolean).length <= 6 && taskTerms(text).length <= 3;
2861
+ }
2862
+ function deriveCheckpoint(session, events, now = /* @__PURE__ */ new Date(), previous) {
2863
+ let goal = previous?.goal ?? null;
2864
+ let constraints = [...previous?.constraints ?? []];
2865
+ let editedPaths = [...previous?.editedPaths ?? []];
2866
+ let runs = (previous?.verification ?? []).map((run2, index) => ({
2867
+ ...run2,
2868
+ ref: previous?.verificationRefs?.[index] ?? null
2869
+ }));
2870
+ let findings = [...previous?.findings ?? []];
2871
+ let lastAssistant = previous?.lastAssistant ?? "";
2872
+ let nextAction = previous?.nextAction ?? null;
2873
+ let fromSeq = previous?.evidence?.fromSeq ?? events[0]?.seq ?? 0;
2874
+ let omittedTranscriptLines = previous?.omittedTranscriptLines ?? 0;
2875
+ 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;
2876
+ const switchTask = /^(?:new task\s*:|switch (?:tasks?|to)\b|now (?:refactor|implement|build|create|fix|migrate|port)\b)/i;
2877
+ const bareFollowUp = /^(?:continue|go on|yes|ok(?:ay)?|thanks|run (?:the )?(?:tests?|checks?)(?: again)?)[.!]?$/i;
2878
+ for (const event of events) {
2879
+ if (event.kind === "user" && event.text) {
2880
+ const completed = runs.length > 0 && [...new Map(runs.map((run2) => [run2.command, run2])).values()].every(
2881
+ (run2) => run2.ok === true && !run2.stale
2882
+ ) && !nextAction;
2883
+ const start = !goal ? !bareFollowUp.test(event.text) : switchTask.test(event.text) || completed && !continuation.test(event.text) && !isFollowUp(event.text);
2884
+ if (start) {
2885
+ goal = event.text;
2886
+ constraints = [];
2887
+ editedPaths = [];
2888
+ runs = [];
2889
+ findings = [];
2890
+ lastAssistant = "";
2891
+ nextAction = null;
2892
+ fromSeq = event.seq;
2893
+ }
2894
+ constraints.push(
2895
+ ...event.text.split(/(?<=[.!?])\s+/).filter((sentence) => CONSTRAINT.test(sentence)).map((sentence) => clip2(sentence, 200))
2896
+ );
2897
+ constraints = [...new Set(constraints)].slice(-JOURNAL_LIMITS.constraints);
2898
+ } else if (event.kind === "edit" && event.path) {
2899
+ editedPaths = [.../* @__PURE__ */ new Set([...editedPaths, event.path])].slice(-JOURNAL_LIMITS.editedPaths);
2900
+ for (const run2 of runs) run2.stale = true;
2901
+ } else if (event.kind === "command" && event.text && isVerificationCommand(event.text)) {
2902
+ runs.push({ command: clip2(event.text, 160), ok: null, stale: false, ref: event.ref ?? null });
2903
+ runs = runs.slice(-JOURNAL_LIMITS.verification);
2904
+ } else if (event.kind === "result" && event.ref) {
2905
+ for (const run2 of runs) if (run2.ref === event.ref) run2.ok = event.ok ?? null;
2906
+ } else if (event.kind === "assistant" && event.text) {
2907
+ if (lastAssistant) findings.push(clip2(lastAssistant, JOURNAL_LIMITS.findingChars));
2908
+ findings = findings.slice(-JOURNAL_LIMITS.findings);
2909
+ lastAssistant = event.text;
2910
+ const next = lastAssistant.split(/(?<=[.!?])\s+/).find((sentence) => NEXT_STEP.test(sentence));
2911
+ nextAction = next ? clip2(next, 200) : null;
2912
+ } else if (event.kind === "gap") {
2913
+ omittedTranscriptLines++;
2914
+ }
2915
+ }
2916
+ return {
2917
+ v: JOURNAL_VERSION,
2918
+ session,
2919
+ updatedAt: now.toISOString(),
2920
+ goal,
2921
+ constraints,
2922
+ editedPaths,
2923
+ verification: runs.map(({ ref: _ref, ...run2 }) => run2),
2924
+ verificationRefs: runs.map((run2) => run2.ref),
2925
+ failedApproaches: [
2926
+ ...new Set(runs.filter((run2) => run2.ok === false).map((run2) => `${run2.command}: failed`))
2927
+ ].slice(-JOURNAL_LIMITS.verification),
2928
+ findings,
2929
+ lastAssistant,
2930
+ ...omittedTranscriptLines ? { omittedTranscriptLines } : {},
2931
+ nextAction,
2932
+ evidence: events.length > 0 ? { fromSeq, toSeq: events.at(-1)?.seq ?? 0 } : previous?.evidence ?? null
2933
+ };
2934
+ }
2935
+
2936
+ // src/memory-extract/engine.ts
2937
+ import { createHash as createHash3 } from "crypto";
2938
+ import { resolve as resolve8 } from "path";
2051
2939
 
2052
2940
  // src/memory-extract/deterministic.ts
2053
2941
  var EXPLICIT_RESOLUTION = /\b(?:fixed|resolved|closed)\b.*\bfact_[a-z0-9]+\b/i;
@@ -2290,8 +3178,8 @@ function matchResolution(fixText, pathCandidates, unresolved) {
2290
3178
  }
2291
3179
 
2292
3180
  // 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";
3181
+ import { mkdir as mkdir5, readdir as readdir4, readFile as readFile8, rename, rm as rm3 } from "fs/promises";
3182
+ import { dirname as dirname5, join as join7, relative as relative5, resolve as resolve7, sep as sep5 } from "path";
2295
3183
  import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
2296
3184
  import { z as z5 } from "zod";
2297
3185
  var reviewSchema = z5.object({
@@ -2326,15 +3214,15 @@ var ReviewStore = class {
2326
3214
  reviewDirectory;
2327
3215
  archiveDirectory;
2328
3216
  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");
3217
+ this.repositoryRoot = resolve7(repositoryRoot);
3218
+ this.reviewDirectory = join7(this.repositoryRoot, ".codebuddy", "memory", "review");
3219
+ this.archiveDirectory = join7(this.repositoryRoot, ".codebuddy", "memory", "review-archive");
2332
3220
  }
2333
3221
  async write(raw) {
2334
3222
  const input = redactPersisted(raw);
2335
3223
  const path = this.itemPath(input.id);
2336
3224
  await this.prepareDirectory();
2337
- await assertNoSymlinkBetween(this.repositoryRoot, dirname4(path));
3225
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname5(path));
2338
3226
  const frontMatter = stringifyYaml3({
2339
3227
  schemaVersion: 1,
2340
3228
  id: input.id,
@@ -2363,7 +3251,7 @@ var ReviewStore = class {
2363
3251
  async read(id) {
2364
3252
  const path = this.itemPath(id);
2365
3253
  await assertNoSymlinkBetween(this.repositoryRoot, path);
2366
- const parsed = parseFrontMatter(await readFile5(path, "utf8"));
3254
+ const parsed = parseFrontMatter(await readFile8(path, "utf8"));
2367
3255
  return {
2368
3256
  ...reviewSchema.parse(parseYaml4(parsed.frontMatter)),
2369
3257
  content: parsed.content.trim(),
@@ -2373,7 +3261,7 @@ var ReviewStore = class {
2373
3261
  async list() {
2374
3262
  try {
2375
3263
  await this.prepareDirectory();
2376
- const entries = await readdir3(this.reviewDirectory, { withFileTypes: true });
3264
+ const entries = await readdir4(this.reviewDirectory, { withFileTypes: true });
2377
3265
  const items = [];
2378
3266
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
2379
3267
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
@@ -2390,7 +3278,7 @@ var ReviewStore = class {
2390
3278
  }
2391
3279
  }
2392
3280
  async delete(id) {
2393
- await rm2(this.itemPath(id), { force: true });
3281
+ await rm3(this.itemPath(id), { force: true });
2394
3282
  }
2395
3283
  async archiveExpired(options = {}) {
2396
3284
  const cutoff = (options.now ?? /* @__PURE__ */ new Date()).getTime() - (options.olderThanDays ?? 30) * 24 * 60 * 60 * 1e3;
@@ -2412,25 +3300,25 @@ var ReviewStore = class {
2412
3300
  return overflow;
2413
3301
  }
2414
3302
  async archive(item) {
2415
- await mkdir3(this.archiveDirectory, { recursive: true, mode: 448 });
3303
+ await mkdir5(this.archiveDirectory, { recursive: true, mode: 448 });
2416
3304
  await assertNoSymlinkBetween(this.repositoryRoot, this.archiveDirectory);
2417
- await rename(item.path, join5(this.archiveDirectory, `${item.id}.md`));
3305
+ await rename(item.path, join7(this.archiveDirectory, `${item.id}.md`));
2418
3306
  }
2419
3307
  itemPath(id) {
2420
3308
  if (!/^rev_[a-z0-9]+$/.test(id)) throw new Error(`Invalid review id: ${id}`);
2421
- return this.safePath(join5(this.reviewDirectory, `${id}.md`));
3309
+ return this.safePath(join7(this.reviewDirectory, `${id}.md`));
2422
3310
  }
2423
3311
  safePath(path) {
2424
- const absolute = resolve6(path);
2425
- const relativePath = relative4(this.repositoryRoot, absolute);
2426
- if (relativePath === ".." || relativePath.startsWith(`..${sep4}`) || relativePath === "") {
3312
+ const absolute = resolve7(path);
3313
+ const relativePath = relative5(this.repositoryRoot, absolute);
3314
+ if (relativePath === ".." || relativePath.startsWith(`..${sep5}`) || relativePath === "") {
2427
3315
  throw new Error(`Review path escapes repository root: ${path}`);
2428
3316
  }
2429
3317
  return absolute;
2430
3318
  }
2431
3319
  async prepareDirectory() {
2432
- await assertNoSymlinkBetween(this.repositoryRoot, dirname4(this.reviewDirectory));
2433
- await mkdir3(this.reviewDirectory, { recursive: true, mode: 448 });
3320
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname5(this.reviewDirectory));
3321
+ await mkdir5(this.reviewDirectory, { recursive: true, mode: 448 });
2434
3322
  }
2435
3323
  };
2436
3324
 
@@ -2517,7 +3405,7 @@ var AUTO_SAVE_THRESHOLD = 0.7;
2517
3405
  var STATED_CONFIDENCE = 0.9;
2518
3406
  var POLICY_CONFIDENCE = 0.6;
2519
3407
  async function captureAfterTurn(input, options = {}) {
2520
- const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
3408
+ const repositoryRoot = resolve8(options.repositoryRoot ?? process.cwd());
2521
3409
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
2522
3410
  const memoryStore = new MemoryFileStore(repositoryRoot);
2523
3411
  const reviewStore = new ReviewStore(repositoryRoot);
@@ -2881,10 +3769,10 @@ function candidateFingerprint(namespace, raw) {
2881
3769
  content: candidate.content.trim().replace(/\s+/g, " ").toLowerCase(),
2882
3770
  paths: [...new Set(candidate.paths.map((path) => path.replace(/\\/g, "/")))].sort()
2883
3771
  });
2884
- return createHash2("sha256").update(canonical).digest("hex");
3772
+ return createHash3("sha256").update(canonical).digest("hex");
2885
3773
  }
2886
3774
  async function approveReviewItem(id, options = {}) {
2887
- const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
3775
+ const repositoryRoot = resolve8(options.repositoryRoot ?? process.cwd());
2888
3776
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
2889
3777
  const reviewStore = new ReviewStore(repositoryRoot);
2890
3778
  const memoryStore = new MemoryFileStore(repositoryRoot);
@@ -2931,17 +3819,17 @@ async function approveReviewItem(id, options = {}) {
2931
3819
  }
2932
3820
 
2933
3821
  // src/hooks/semantic.ts
2934
- import pTimeout from "p-timeout";
3822
+ import pTimeout2 from "p-timeout";
2935
3823
  var CONNECT_BUDGET_MS = 2500;
2936
3824
  var SEARCH_BUDGET_MS = 3e3;
2937
3825
  var RERANK_BUDGET_MS = 2e3;
2938
3826
  async function configuredReranker(repositoryRoot) {
2939
3827
  if (process.env.CODEBUDDY_RERANK === "0") return void 0;
2940
- const file = await readConfigFile(repositoryRoot);
2941
- const provider = file.rerank?.provider ?? "off";
3828
+ const file2 = await readConfigFile(repositoryRoot);
3829
+ const provider = file2.rerank?.provider ?? "off";
2942
3830
  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 });
3831
+ const model = file2.rerank?.model ?? DEFAULT_RERANK_MODELS[provider].id;
3832
+ const bounded = (scores) => pTimeout2(scores, { milliseconds: RERANK_BUDGET_MS });
2945
3833
  if (provider === "local") {
2946
3834
  const revision = model === DEFAULT_RERANK_MODELS.local.id ? DEFAULT_RERANK_MODELS.local.revision : void 0;
2947
3835
  return async (task, documents) => {
@@ -2951,7 +3839,7 @@ async function configuredReranker(repositoryRoot) {
2951
3839
  );
2952
3840
  };
2953
3841
  }
2954
- const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file.provider?.apiKey;
3842
+ const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file2.provider?.apiKey;
2955
3843
  if (!apiKey) return void 0;
2956
3844
  const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-LXZUR3U2.js");
2957
3845
  const call = provider === "openrouter" ? rerankOpenRouter : rerankHuggingFace;
@@ -2962,14 +3850,14 @@ async function openHookSemantics(repositoryRoot) {
2962
3850
  const connect = async () => {
2963
3851
  const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
2964
3852
  if (!config.postgresUrl) return null;
2965
- const { createRuntime } = await import("./operations-OG4ISWIM.js");
3853
+ const { createRuntime } = await import("./operations-STR7S5UO.js");
2966
3854
  const opening = createRuntime(config, {
2967
3855
  repositoryRoot,
2968
3856
  startWorker: false,
2969
3857
  connectTimeoutSeconds: 2
2970
3858
  });
2971
3859
  try {
2972
- return await pTimeout(opening, { milliseconds: CONNECT_BUDGET_MS });
3860
+ return await pTimeout2(opening, { milliseconds: CONNECT_BUDGET_MS });
2973
3861
  } catch {
2974
3862
  void opening.then((handle) => handle.close()).catch(() => void 0);
2975
3863
  return null;
@@ -2997,22 +3885,22 @@ async function openHookSemantics(repositoryRoot) {
2997
3885
  }
2998
3886
 
2999
3887
  // 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";
3888
+ import { mkdir as mkdir6, readdir as readdir5, readFile as readFile9, rename as rename2, rm as rm4, stat as stat3 } from "fs/promises";
3889
+ import { join as join8, resolve as resolve9 } from "path";
3002
3890
  var STAGING_LOCK_STALE_MS = 1e4;
3003
3891
  var HookStagingStore = class {
3004
3892
  directory;
3005
3893
  operations = Promise.resolve();
3006
3894
  constructor(repositoryRoot = process.cwd()) {
3007
- this.directory = join6(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
3895
+ this.directory = join8(resolve9(repositoryRoot), ".codebuddy", "cache", "hooks");
3008
3896
  }
3009
- async add(sessionId, paths) {
3897
+ async add(sessionId, paths2) {
3010
3898
  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 });
3899
+ const clean2 = paths2.map(normalize2).filter(Boolean);
3900
+ if (clean2.length === 0) return;
3901
+ await mkdir6(this.directory, { recursive: true, mode: 448 });
3014
3902
  await this.withFileLock(sessionId, async () => {
3015
- const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean])].sort();
3903
+ const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean2])].sort();
3016
3904
  await writeAtomic(
3017
3905
  this.filePath(sessionId),
3018
3906
  `${JSON.stringify({ paths: merged }, null, 2)}
@@ -3021,9 +3909,18 @@ var HookStagingStore = class {
3021
3909
  });
3022
3910
  });
3023
3911
  }
3912
+ /** Sessions with edits staged but not yet captured by a turn end. */
3913
+ async pending() {
3914
+ const names = await readdir5(this.directory).catch(() => []);
3915
+ const sessions = names.filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -5));
3916
+ const counts = await Promise.all(
3917
+ sessions.map(async (session) => ({ session, paths: (await this.read(session)).length }))
3918
+ );
3919
+ return counts.filter((entry) => entry.paths > 0);
3920
+ }
3024
3921
  async read(sessionId) {
3025
3922
  try {
3026
- const parsed = JSON.parse(await readFile6(this.filePath(sessionId), "utf8"));
3923
+ const parsed = JSON.parse(await readFile9(this.filePath(sessionId), "utf8"));
3027
3924
  return Array.isArray(parsed?.paths) ? parsed.paths.filter((p) => typeof p === "string") : [];
3028
3925
  } catch {
3029
3926
  return [];
@@ -3032,7 +3929,7 @@ var HookStagingStore = class {
3032
3929
  /** Return the staged paths and clear them. */
3033
3930
  async drain(sessionId) {
3034
3931
  return this.serialize(async () => {
3035
- await mkdir4(this.directory, { recursive: true, mode: 448 });
3932
+ await mkdir6(this.directory, { recursive: true, mode: 448 });
3036
3933
  return this.withFileLock(sessionId, async () => {
3037
3934
  const path = this.filePath(sessionId);
3038
3935
  const draining = `${path}.${process.pid}.${Date.now()}.drain`;
@@ -3043,27 +3940,27 @@ var HookStagingStore = class {
3043
3940
  throw error;
3044
3941
  }
3045
3942
  try {
3046
- const parsed = JSON.parse(await readFile6(draining, "utf8"));
3943
+ const parsed = JSON.parse(await readFile9(draining, "utf8"));
3047
3944
  return Array.isArray(parsed?.paths) ? parsed.paths.filter((item) => typeof item === "string") : [];
3048
3945
  } finally {
3049
- await rm3(draining, { force: true });
3946
+ await rm4(draining, { force: true });
3050
3947
  }
3051
3948
  });
3052
3949
  });
3053
3950
  }
3054
3951
  /** Claim a batch without consuming edits staged while capture runs. */
3055
3952
  async claim(sessionId) {
3056
- await mkdir4(this.directory, { recursive: true, mode: 448 });
3953
+ await mkdir6(this.directory, { recursive: true, mode: 448 });
3057
3954
  const captureLock = `${this.filePath(sessionId)}.capture-lock`;
3058
3955
  for (let attempt = 0; ; attempt++) {
3059
3956
  try {
3060
- await mkdir4(captureLock);
3957
+ await mkdir6(captureLock);
3061
3958
  break;
3062
3959
  } catch (error) {
3063
3960
  if (error.code !== "EEXIST") throw error;
3064
3961
  const age = Date.now() - (await stat3(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
3065
3962
  if (age > 6e4) {
3066
- await rm3(captureLock, { recursive: true, force: true });
3963
+ await rm4(captureLock, { recursive: true, force: true });
3067
3964
  continue;
3068
3965
  }
3069
3966
  if (attempt >= 200) return null;
@@ -3074,34 +3971,34 @@ var HookStagingStore = class {
3074
3971
  return await this.withFileLock(sessionId, async () => {
3075
3972
  const claimed = this.claimPath(sessionId);
3076
3973
  try {
3077
- return parsePaths(await readFile6(claimed, "utf8"));
3974
+ return parsePaths(await readFile9(claimed, "utf8"));
3078
3975
  } catch (error) {
3079
3976
  if (error.code !== "ENOENT") throw error;
3080
3977
  }
3081
3978
  try {
3082
3979
  await rename2(this.filePath(sessionId), claimed);
3083
- return parsePaths(await readFile6(claimed, "utf8"));
3980
+ return parsePaths(await readFile9(claimed, "utf8"));
3084
3981
  } catch (error) {
3085
3982
  if (error.code === "ENOENT") return [];
3086
3983
  throw error;
3087
3984
  }
3088
3985
  });
3089
3986
  } catch (error) {
3090
- await rm3(captureLock, { recursive: true, force: true });
3987
+ await rm4(captureLock, { recursive: true, force: true });
3091
3988
  throw error;
3092
3989
  }
3093
3990
  }
3094
3991
  /** Acknowledge only the claimed batch; later edits remain staged. */
3095
3992
  async acknowledge(sessionId) {
3096
3993
  await this.withFileLock(sessionId, async () => {
3097
- await rm3(this.claimPath(sessionId), { force: true });
3994
+ await rm4(this.claimPath(sessionId), { force: true });
3098
3995
  });
3099
- await rm3(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
3996
+ await rm4(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
3100
3997
  }
3101
3998
  /** Restore the claimed batch for retry, merging edits that arrived during capture. */
3102
3999
  async rollback(sessionId) {
3103
4000
  await this.withFileLock(sessionId, async () => {
3104
- const claimed = await readFile6(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
4001
+ const claimed = await readFile9(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
3105
4002
  const merged = [.../* @__PURE__ */ new Set([...claimed, ...await this.read(sessionId)])].sort();
3106
4003
  if (merged.length > 0)
3107
4004
  await writeAtomic(
@@ -3109,9 +4006,9 @@ var HookStagingStore = class {
3109
4006
  `${JSON.stringify({ paths: merged }, null, 2)}
3110
4007
  `
3111
4008
  );
3112
- await rm3(this.claimPath(sessionId), { force: true });
4009
+ await rm4(this.claimPath(sessionId), { force: true });
3113
4010
  });
3114
- await rm3(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
4011
+ await rm4(`${this.filePath(sessionId)}.capture-lock`, { recursive: true, force: true });
3115
4012
  }
3116
4013
  claimPath(sessionId) {
3117
4014
  return `${this.filePath(sessionId)}.claimed`;
@@ -3120,7 +4017,7 @@ var HookStagingStore = class {
3120
4017
  const lock = `${this.filePath(sessionId)}.lock`;
3121
4018
  for (let attempt = 0; ; attempt++) {
3122
4019
  try {
3123
- await mkdir4(lock);
4020
+ await mkdir6(lock);
3124
4021
  break;
3125
4022
  } catch (error) {
3126
4023
  if (error.code !== "EEXIST" || attempt >= 200) throw error;
@@ -3129,7 +4026,7 @@ var HookStagingStore = class {
3129
4026
  () => 0
3130
4027
  );
3131
4028
  if (age > STAGING_LOCK_STALE_MS) {
3132
- await rm3(lock, { recursive: true, force: true });
4029
+ await rm4(lock, { recursive: true, force: true });
3133
4030
  continue;
3134
4031
  }
3135
4032
  await new Promise((resolveWait) => setTimeout(resolveWait, 10));
@@ -3138,7 +4035,7 @@ var HookStagingStore = class {
3138
4035
  try {
3139
4036
  return await operation();
3140
4037
  } finally {
3141
- await rm3(lock, { recursive: true, force: true });
4038
+ await rm4(lock, { recursive: true, force: true });
3142
4039
  }
3143
4040
  }
3144
4041
  async serialize(operation) {
@@ -3156,7 +4053,7 @@ var HookStagingStore = class {
3156
4053
  }
3157
4054
  filePath(sessionId) {
3158
4055
  const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "default";
3159
- return join6(this.directory, `${safe}.json`);
4056
+ return join8(this.directory, `${safe}.json`);
3160
4057
  }
3161
4058
  };
3162
4059
  function normalize2(path) {
@@ -3169,39 +4066,44 @@ function parsePaths(raw) {
3169
4066
 
3170
4067
  // src/hooks/runtime.ts
3171
4068
  var CONTEXT_BLOCK_MAX_CHARS = 1800;
4069
+ var STANDALONE_FREE = /* @__PURE__ */ new Set(["session", "test-command"]);
3172
4070
  var HOOK_TASK_FILES = 2;
3173
4071
  var PROMPT_FACTS = 3;
4072
+ var HOOK_SPANS_PER_FILE = 2;
3174
4073
  var CONTEXT_HOOK_BUDGET_MS = 15e3;
3175
4074
  async function resolveNamespace(repositoryRoot) {
3176
4075
  if (process.env.CODEBUDDY_NAMESPACE) return process.env.CODEBUDDY_NAMESPACE;
3177
- const file = await readConfigFile(repositoryRoot);
3178
- return file.namespace ?? basename(repositoryRoot);
4076
+ const file2 = await readConfigFile(repositoryRoot);
4077
+ return file2.namespace ?? basename(repositoryRoot);
3179
4078
  }
3180
4079
  async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
4080
+ if (skipPromptRetrieval(input.prompt)) return "";
3181
4081
  const started = Date.now();
3182
4082
  const repositoryRoot = hookRoot(input.cwd);
3183
4083
  const namespace = await resolveNamespace(repositoryRoot);
3184
4084
  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
4085
  const ledger = new SessionCapsuleLedger(repositoryRoot);
3203
4086
  const sessionId = input.session_id ?? "default";
3204
4087
  if (input.ephemeral_context) await ledger.reset(sessionId);
4088
+ const followUp = input.prompt !== void 0 && input.session_id !== void 0 && isFollowUpPrompt(input.prompt) && await ledger.delivered(sessionId);
4089
+ let beforeEdit = null;
4090
+ const semantics = input.prompt && !followUp ? await openHookSemantics(repositoryRoot) : null;
4091
+ if (!followUp)
4092
+ try {
4093
+ beforeEdit = await buildBeforeEditContext({
4094
+ repositoryRoot,
4095
+ namespace,
4096
+ // Git changes are the targets only without a prompt; a prompt that
4097
+ // matches no file gets none (an explicit useGit would force them back).
4098
+ ...input.prompt ? { task: input.prompt } : {},
4099
+ ...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
4100
+ ...semantics?.reranker ? { reranker: semantics.reranker } : {}
4101
+ });
4102
+ } catch {
4103
+ beforeEdit = null;
4104
+ } finally {
4105
+ await semantics?.close();
4106
+ }
3205
4107
  const { text, emitted } = await renderContextBlock(
3206
4108
  bootstrap,
3207
4109
  beforeEdit,
@@ -3219,6 +4121,10 @@ async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
3219
4121
  await ledger.markSent(sessionId, emitted);
3220
4122
  return text;
3221
4123
  }
4124
+ function isFollowUpPrompt(prompt) {
4125
+ const words2 = prompt.trim().split(/\s+/).filter(Boolean);
4126
+ return words2.length <= 6 && taskTerms(prompt).length <= 3;
4127
+ }
3222
4128
  function recallTrace(input, beforeEdit, emitted, text, started, late) {
3223
4129
  const shown = new Set(
3224
4130
  emitted.filter((c) => c.key.startsWith("fact:")).map((c) => c.key.slice(5))
@@ -3234,29 +4140,47 @@ function recallTrace(input, beforeEdit, emitted, text, started, late) {
3234
4140
  tokensOut: estimateTokens(text),
3235
4141
  ...input.session_id ? { session: input.session_id } : {},
3236
4142
  detail: {
3237
- prompt: clip2(prompt, 120),
4143
+ prompt: clip3(prompt, 120),
3238
4144
  facts: facts.map((fact) => fact.summary.slice(0, 100)),
3239
4145
  dense: facts.filter((fact) => fact.reason.includes("semantic")).length,
3240
4146
  lexical: facts.filter((fact) => /task term|target path/.test(fact.reason)).length,
3241
4147
  files: (beforeEdit?.targetPaths ?? []).slice(0, 3),
3242
4148
  sections: emitted.length,
3243
- ...text.trim() ? { block: clip2(redactSensitive(text.trim()), 2e3) } : {},
4149
+ ...text.trim() ? { block: clip3(redactSensitive(text.trim()), 2e3) } : {},
3244
4150
  ...late ? { dropped: "built after the hook budget" } : {}
3245
4151
  }
3246
4152
  };
3247
4153
  }
3248
- function clip2(text, max) {
4154
+ function clip3(text, max) {
3249
4155
  return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
3250
4156
  }
3251
4157
  async function runStageHook(input) {
3252
4158
  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;
4159
+ const paths2 = extractEditedPaths(input.tool_input).map((path) => toRepoRelative(repositoryRoot, path)).filter((path) => path !== null);
4160
+ await new HookStagingStore(repositoryRoot).add(input.session_id ?? "default", paths2);
4161
+ if (input.journal_event_id)
4162
+ await appendHostEvents(
4163
+ repositoryRoot,
4164
+ input.session_id ?? "default",
4165
+ paths2.map((path, index) => ({
4166
+ id: `${input.journal_event_id}:${index}`,
4167
+ kind: "edit",
4168
+ path
4169
+ }))
4170
+ );
4171
+ if (input.journal_events?.length) await runCheckpointHook(input).catch(() => null);
4172
+ if (input.transcript_path) {
4173
+ await ingestTranscript(
4174
+ repositoryRoot,
4175
+ input.session_id ?? "default",
4176
+ input.transcript_path
4177
+ ).catch(() => null);
4178
+ }
4179
+ return paths2;
3256
4180
  }
3257
4181
  async function llmMemoryOptions(repositoryRoot) {
3258
4182
  try {
3259
- return await (await import("./memory-TGHVTBME.js")).llmCaptureOptions(repositoryRoot);
4183
+ return await (await import("./memory-PFFVMTCH.js")).llmCaptureOptions(repositoryRoot);
3260
4184
  } catch {
3261
4185
  return {};
3262
4186
  }
@@ -3268,15 +4192,20 @@ async function runCaptureHook(input) {
3268
4192
  const repositoryRoot = hookRoot(input.cwd);
3269
4193
  const staging = new HookStagingStore(repositoryRoot);
3270
4194
  const sessionId = input.session_id ?? "default";
4195
+ if (input.journal_events?.length) await runCheckpointHook(input).catch(() => null);
4196
+ if (input.transcript_path) {
4197
+ await ingestTranscript(repositoryRoot, sessionId, input.transcript_path).catch(() => null);
4198
+ }
3271
4199
  const claimed = await staging.claim(sessionId);
3272
4200
  if (claimed === null) return { changedFiles: [], result: null };
3273
4201
  const changedFiles = claimed;
3274
4202
  try {
3275
- const prompts = input.transcript_path ? await turnUserPrompts(input.transcript_path) : [];
4203
+ const transcript = input.transcript_path ? await readCaptureTurn(input.transcript_path) : null;
4204
+ const prompts = transcript?.prompts ?? input.user_prompts ?? [];
3276
4205
  const { rules, revocations, policies } = extractUserRules(prompts);
3277
4206
  let investigation = false;
3278
4207
  if (changedFiles.length === 0) {
3279
- const assistant = input.transcript_path ? await lastAssistantMessage(input.transcript_path) : input.last_assistant_message?.trim() ?? "";
4208
+ const assistant = transcript?.assistant ?? input.last_assistant_message?.trim() ?? "";
3280
4209
  investigation = assistant.length >= INVESTIGATION_MIN_CHARS && NAMES_CODE.test(assistant);
3281
4210
  if (!investigation && rules.length === 0 && revocations.length === 0 && policies.length === 0) {
3282
4211
  await staging.acknowledge(sessionId);
@@ -3285,11 +4214,11 @@ async function runCaptureHook(input) {
3285
4214
  }
3286
4215
  const namespace = await resolveNamespace(repositoryRoot);
3287
4216
  const summary = await deriveSummary(
3288
- input.transcript_path,
4217
+ void 0,
3289
4218
  changedFiles,
3290
- input.last_assistant_message
4219
+ transcript?.assistant ?? input.last_assistant_message
3291
4220
  );
3292
- const verification = input.transcript_path ? await turnVerification(input.transcript_path) : verificationLines(
4221
+ const verification = transcript ? transcript.verification : verificationLines(
3293
4222
  (input.shell_commands ?? []).map(({ command, exit_code }) => ({
3294
4223
  command,
3295
4224
  ok: typeof exit_code === "number" ? exit_code === 0 : void 0
@@ -3339,26 +4268,26 @@ var SYNC_BUDGET_MS = 5 * 6e4;
3339
4268
  async function runSyncHook(repositoryRoot, options = {}) {
3340
4269
  const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
3341
4270
  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 });
4271
+ const lock = join9(repositoryRoot, ".codebuddy", "cache", "sync.lock");
4272
+ const dirty = join9(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
4273
+ await mkdir7(dirname6(lock), { recursive: true });
3345
4274
  if (!await takeLock(lock)) {
3346
4275
  await writeFile(dirty, "").catch(() => void 0);
3347
4276
  return null;
3348
4277
  }
3349
- const { createRuntime } = await import("./operations-OG4ISWIM.js");
4278
+ const { createRuntime } = await import("./operations-STR7S5UO.js");
3350
4279
  const runtime = await createRuntime(config, { repositoryRoot }).catch(async (error) => {
3351
- await rm4(lock, { recursive: true, force: true });
4280
+ await rm5(lock, { recursive: true, force: true });
3352
4281
  throw error;
3353
4282
  });
3354
4283
  try {
3355
4284
  let result;
3356
4285
  let round = 0;
3357
4286
  do {
3358
- await rm4(dirty, { force: true });
4287
+ await rm5(dirty, { force: true });
3359
4288
  const started = Date.now();
3360
4289
  const reindex = await runtime.memory.reindexMemory(options.respace ? {} : { respace: false });
3361
- const drained = await pTimeout2(runtime.memory.getWorker().drain(), {
4290
+ const drained = await pTimeout3(runtime.memory.getWorker().drain(), {
3362
4291
  milliseconds: SYNC_BUDGET_MS
3363
4292
  }).then(
3364
4293
  () => true,
@@ -3384,7 +4313,9 @@ async function runSyncHook(repositoryRoot, options = {}) {
3384
4313
  ));
3385
4314
  if (config.provider.type === "local") {
3386
4315
  const started = Date.now();
4316
+ const vectorStarted = Date.now();
3387
4317
  const vectors = await syncFileVectors(repositoryRoot, SYNC_BUDGET_MS);
4318
+ await syncReuseVectors(repositoryRoot, SYNC_BUDGET_MS - (Date.now() - vectorStarted));
3388
4319
  if (vectors) {
3389
4320
  await recordTrace(repositoryRoot, {
3390
4321
  kind: "sync",
@@ -3398,13 +4329,13 @@ async function runSyncHook(repositoryRoot, options = {}) {
3398
4329
  return result;
3399
4330
  } finally {
3400
4331
  await runtime.close();
3401
- await rm4(lock, { recursive: true, force: true });
4332
+ await rm5(lock, { recursive: true, force: true });
3402
4333
  }
3403
4334
  }
3404
4335
  var SYNC_LOCK_STALE_MS = SYNC_BUDGET_MS + 6e4;
3405
4336
  async function takeLock(path) {
3406
4337
  try {
3407
- await mkdir5(path);
4338
+ await mkdir7(path);
3408
4339
  return true;
3409
4340
  } catch {
3410
4341
  const age = await stat4(path).then(
@@ -3412,8 +4343,8 @@ async function takeLock(path) {
3412
4343
  () => 0
3413
4344
  );
3414
4345
  if (age < SYNC_LOCK_STALE_MS) return false;
3415
- await rm4(path, { recursive: true, force: true });
3416
- return mkdir5(path).then(
4346
+ await rm5(path, { recursive: true, force: true });
4347
+ return mkdir7(path).then(
3417
4348
  () => true,
3418
4349
  () => false
3419
4350
  );
@@ -3427,22 +4358,56 @@ async function runResetHook(input, emit) {
3427
4358
  if (input.hook_event_name !== "SessionStart") return "";
3428
4359
  const namespace = await resolveNamespace(repositoryRoot);
3429
4360
  const bootstrap = await buildBootstrapContext({ repositoryRoot, namespace });
3430
- const { text, emitted } = await renderContextBlock(
4361
+ const { text: block, emitted } = await renderContextBlock(
3431
4362
  bootstrap,
3432
4363
  null,
3433
4364
  ledger,
3434
4365
  sessionId,
3435
4366
  input.session_id !== void 0
3436
4367
  );
4368
+ let task = "";
4369
+ if (input.source === "compact") {
4370
+ if (input.transcript_path) {
4371
+ await ingestTranscript(repositoryRoot, sessionId, input.transcript_path).catch(() => null);
4372
+ }
4373
+ const checkpoint = await readCheckpoint(repositoryRoot, sessionId).catch(() => null);
4374
+ if (checkpoint?.goal) {
4375
+ task = await renderCheckpointNow(
4376
+ repositoryRoot,
4377
+ checkpoint,
4378
+ "this session, before compaction"
4379
+ );
4380
+ }
4381
+ }
4382
+ const text = [block, task].filter(Boolean).join("\n");
3437
4383
  if (text.trim()) await emit?.(text);
3438
4384
  await ledger.markSent(sessionId, emitted);
3439
4385
  return text;
3440
4386
  }
4387
+ async function runCheckpointHook(input) {
4388
+ const repositoryRoot = hookRoot(input.cwd);
4389
+ if (input.transcript_path)
4390
+ await ingestTranscript(repositoryRoot, input.session_id ?? "default", input.transcript_path);
4391
+ if (input.journal_events?.length)
4392
+ await appendHostEvents(repositoryRoot, input.session_id ?? "default", input.journal_events);
4393
+ }
4394
+ async function runResumeHook(input, emit) {
4395
+ const repositoryRoot = hookRoot(input.cwd);
4396
+ const checkpoint = await latestUnfinished(repositoryRoot, input.session_id);
4397
+ if (!checkpoint) return "";
4398
+ const text = await renderCheckpointNow(
4399
+ repositoryRoot,
4400
+ checkpoint,
4401
+ `unfinished, from session ${checkpoint.session}`
4402
+ );
4403
+ await emit?.(text);
4404
+ return text;
4405
+ }
3441
4406
  function extractEditedPaths(toolInput) {
3442
4407
  if (!toolInput) return [];
3443
- const paths = [];
4408
+ const paths2 = [];
3444
4409
  const push = (value) => {
3445
- if (typeof value === "string" && value.trim()) paths.push(value);
4410
+ if (typeof value === "string" && value.trim()) paths2.push(value);
3446
4411
  };
3447
4412
  push(toolInput.file_path);
3448
4413
  push(toolInput.filePath);
@@ -3460,12 +4425,12 @@ function extractEditedPaths(toolInput) {
3460
4425
  if (edit && typeof edit === "object") push(edit.file_path);
3461
4426
  }
3462
4427
  }
3463
- return [...new Set(paths)];
4428
+ return [...new Set(paths2)];
3464
4429
  }
3465
4430
  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;
4431
+ const absolute = isAbsolute2(path) ? resolve10(path) : resolve10(repositoryRoot, path);
4432
+ const rel = relative6(repositoryRoot, absolute);
4433
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute2(rel)) return null;
3469
4434
  return rel.replace(/\\/g, "/").replace(/^\.\//, "");
3470
4435
  }
3471
4436
  async function deriveSummary(transcriptPath, changedFiles, lastAssistantText) {
@@ -3473,71 +4438,36 @@ async function deriveSummary(transcriptPath, changedFiles, lastAssistantText) {
3473
4438
  const assistantText = transcriptPath ? await lastAssistantMessage(transcriptPath) : lastAssistantText?.trim().slice(0, 2e3) ?? "";
3474
4439
  return [assistantText, fileList].filter(Boolean).join("\n\n");
3475
4440
  }
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
4441
  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
- );
4442
+ return (await readCaptureTurn(transcriptPath)).verification;
3522
4443
  }
3523
4444
  function verificationLines(runs) {
3524
- const lines = runs.filter((run) => isVerificationCommand(run.command)).map(({ command, ok }) => {
4445
+ const lines = runs.filter((run2) => isVerificationCommand(run2.command)).map(({ command, ok }) => {
3525
4446
  const label = ok === void 0 ? "no result" : ok ? "passed" : "failed";
3526
4447
  return `${command.replace(/\s+/g, " ").trim().slice(0, 120)}: ${label}`;
3527
4448
  });
3528
4449
  return lines.slice(-10);
3529
4450
  }
3530
4451
  var MAX_RULE_PROMPT_CHARS = 1500;
3531
- var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
4452
+ var TAGGED_BLOCK2 = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
3532
4453
  async function turnUserPrompts(transcriptPath) {
4454
+ return (await readCaptureTurn(transcriptPath)).prompts;
4455
+ }
4456
+ async function lastAssistantMessage(transcriptPath) {
4457
+ return (await readCaptureTurn(transcriptPath)).assistant;
4458
+ }
4459
+ async function readCaptureTurn(transcriptPath) {
3533
4460
  let raw;
3534
4461
  try {
3535
- raw = await readFile7(transcriptPath, "utf8");
4462
+ raw = await readFile10(transcriptPath, "utf8");
3536
4463
  } catch {
3537
- return [];
4464
+ return { prompts: [], assistant: "", verification: [] };
3538
4465
  }
3539
4466
  let prompts = [];
3540
4467
  let answered = false;
4468
+ let assistant = "";
4469
+ const commands = /* @__PURE__ */ new Map();
4470
+ const outcomes = /* @__PURE__ */ new Map();
3541
4471
  for (const line of raw.split("\n")) {
3542
4472
  let entry;
3543
4473
  try {
@@ -3545,43 +4475,42 @@ async function turnUserPrompts(transcriptPath) {
3545
4475
  } catch {
3546
4476
  continue;
3547
4477
  }
3548
- const message = entry.message ?? {};
3549
- if (message.role === "assistant") {
4478
+ const message = entry.message ?? entry;
4479
+ const role = message.role ?? entry.type;
4480
+ const content = message.content;
4481
+ const blocks = Array.isArray(content) ? content : [];
4482
+ if (role === "assistant") {
3550
4483
  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 {
4484
+ const text = extractText(content);
4485
+ if (text) assistant = text.slice(0, 2e3);
4486
+ for (const block of blocks) {
4487
+ if (block?.type === "tool_use" && block.name === "Bash" && typeof block.id === "string" && typeof block.input?.command === "string" && isVerificationCommand(block.input.command))
4488
+ commands.set(block.id, block.input.command);
4489
+ }
4490
+ } else if (role === "user") {
4491
+ const text = entry.isMeta === true ? "" : extractText(content).replace(TAGGED_BLOCK2, " ").trim();
4492
+ if (text) {
4493
+ commands.clear();
4494
+ outcomes.clear();
4495
+ if (answered) {
4496
+ prompts = [];
4497
+ answered = false;
4498
+ }
4499
+ if (text.length <= MAX_RULE_PROMPT_CHARS) prompts.push(text);
4500
+ }
4501
+ for (const block of blocks) {
4502
+ if (block?.type === "tool_result" && typeof block.tool_use_id === "string")
4503
+ outcomes.set(block.tool_use_id, block.is_error !== true);
3579
4504
  }
3580
4505
  }
3581
- return latest.slice(0, 2e3);
3582
- } catch {
3583
- return "";
3584
4506
  }
4507
+ return {
4508
+ prompts,
4509
+ assistant,
4510
+ verification: verificationLines(
4511
+ [...commands].map(([id, command]) => ({ command, ok: outcomes.get(id) }))
4512
+ )
4513
+ };
3585
4514
  }
3586
4515
  function extractText(content) {
3587
4516
  if (typeof content === "string") return content;
@@ -3607,15 +4536,17 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
3607
4536
  }
3608
4537
  }
3609
4538
  ] : [],
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"}`
4539
+ // Benchmark v3: "project: <name>", "plan: none" and the architecture size
4540
+ // were re-read every turn and never acted on, so only an active plan is shown.
4541
+ ...bootstrap.plan ? [
4542
+ {
4543
+ priority: 4,
4544
+ capsule: {
4545
+ key: "plan",
4546
+ text: `plan: ${bootstrap.plan.title} (${bootstrap.plan.status}); policy: ${bootstrap.policy}`
4547
+ }
3616
4548
  }
3617
- },
3618
- { priority: 4, capsule: { key: "plan-policy", text: `plan policy: ${bootstrap.policy}` } }
4549
+ ] : []
3619
4550
  ];
3620
4551
  const policyRules = beforeEdit?.policyRules ?? [];
3621
4552
  if (policyRules.length > 0) {
@@ -3673,58 +4604,61 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
3673
4604
  capsule: { key: "test-command", text: `test command: ${beforeEdit.testCommand}` }
3674
4605
  });
3675
4606
  }
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
4607
  if (beforeEdit && beforeEdit.targetPaths.length > 0) {
3705
4608
  const targetLabel = beforeEdit.pathSource === "git" ? "current changes" : "task files";
3706
4609
  const tests = (beforeEdit.tests ?? []).map((entry) => entry.test);
3707
- items.push({
4610
+ const ranges = new Map(
4611
+ (beforeEdit.symbols ?? []).map((file2) => [
4612
+ file2.path,
4613
+ (file2.spans ?? []).filter((span) => span.name).slice(0, HOOK_SPANS_PER_FILE).map((span) => `${span.name} L${span.startLine}-${span.endLine}`)
4614
+ ])
4615
+ );
4616
+ const describe = (path) => {
4617
+ const spans = ranges.get(path) ?? [];
4618
+ return spans.length > 0 ? `${path} (${spans.join(", ")})` : path;
4619
+ };
4620
+ capsules.push({
3708
4621
  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
- )
4622
+ capsule: {
4623
+ key: "task-files",
4624
+ text: bound(
4625
+ `${targetLabel}: ${beforeEdit.targetPaths.slice(0, beforeEdit.pathSource === "task" ? HOOK_TASK_FILES : 8).map(describe).join(", ")}${tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""}`,
4626
+ 500
4627
+ )
4628
+ }
3713
4629
  });
3714
4630
  const targets = new Set(beforeEdit.targetPaths);
3715
4631
  const risks = beforeEdit.risks.items.filter(
3716
4632
  (item) => targets.has(item.path) && item.score >= RISK_LINE_MIN_SCORE
3717
4633
  );
3718
4634
  if (risks.length > 0) {
3719
- items.push({
4635
+ capsules.push({
3720
4636
  priority: 1,
3721
- text: bound(
3722
- `risk: ${risks.slice(0, 3).map((item) => `${item.path}=${item.score}`).join(", ")}`,
3723
- 500
3724
- )
4637
+ capsule: {
4638
+ key: "risk",
4639
+ text: bound(
4640
+ `risk: ${risks.slice(0, 3).map((item) => `${item.path}=${item.score}`).join(", ")}`,
4641
+ 500
4642
+ )
4643
+ }
3725
4644
  });
3726
4645
  }
3727
4646
  }
4647
+ const seen = await ledger.seen(
4648
+ sessionId,
4649
+ capsules.map((entry) => entry.capsule)
4650
+ );
4651
+ const items = [
4652
+ { priority: 0, text: "## CodeBuddy project context (auto-recalled)" }
4653
+ ];
4654
+ let newFacts = 0;
4655
+ for (const { priority, capsule } of capsules) {
4656
+ if (seen.has(capsule.key)) continue;
4657
+ if (ranked && capsule.key.startsWith("fact:") && ++newFacts > PROMPT_FACTS) continue;
4658
+ else items.push({ priority, text: capsule.text, capsule });
4659
+ }
4660
+ if (!items.some((item) => item.capsule && !STANDALONE_FREE.has(item.capsule.key)))
4661
+ return { text: "", emitted: [] };
3728
4662
  const chosen = /* @__PURE__ */ new Set();
3729
4663
  let used = -1;
3730
4664
  const byPriority = items.map((item, index) => ({ item, index })).sort((a, b) => a.item.priority - b.item.priority || a.index - b.index);
@@ -3740,15 +4674,6 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
3740
4674
  emitted: packed.flatMap((item) => item.capsule ? [item.capsule] : [])
3741
4675
  };
3742
4676
  }
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
4677
 
3753
4678
  export {
3754
4679
  PLAN_STATUSES,
@@ -3767,28 +4692,45 @@ export {
3767
4692
  PlanNotFoundError,
3768
4693
  ActivePlanExistsError,
3769
4694
  PlanLifecycle,
3770
- enforceContextBudget,
4695
+ appendHostEvents,
4696
+ journalLag,
4697
+ forgetJournal,
4698
+ readCheckpoint,
4699
+ listJournals,
4700
+ findCodexRollouts,
4701
+ importCodexRollout,
4702
+ renderCheckpointNow,
4703
+ isUnfinished,
4704
+ latestUnfinished,
3771
4705
  WorklogStore,
4706
+ enforceContextBudget,
3772
4707
  buildBootstrapContext,
3773
4708
  buildBeforeEditContext,
4709
+ syncReuseVectors,
4710
+ rankSemanticReuse,
3774
4711
  ReviewStore,
3775
4712
  captureAfterTurn,
3776
4713
  approveReviewItem,
3777
4714
  configuredReranker,
3778
4715
  openHookSemantics,
4716
+ HookStagingStore,
3779
4717
  CONTEXT_HOOK_BUDGET_MS,
3780
4718
  runContextHook,
4719
+ isFollowUpPrompt,
3781
4720
  runStageHook,
3782
4721
  llmMemoryOptions,
3783
4722
  runCaptureHook,
3784
4723
  runSyncHook,
3785
4724
  runResetHook,
4725
+ runCheckpointHook,
4726
+ runResumeHook,
3786
4727
  extractEditedPaths,
3787
4728
  deriveSummary,
3788
4729
  turnVerification,
3789
4730
  verificationLines,
3790
4731
  turnUserPrompts,
3791
4732
  lastAssistantMessage,
4733
+ readCaptureTurn,
3792
4734
  renderContextBlock
3793
4735
  };
3794
- //# sourceMappingURL=chunk-F4BJ3DXW.js.map
4736
+ //# sourceMappingURL=chunk-KQCP3ZYH.js.map