@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.
- package/ARCHITECTURE.md +98 -0
- package/CLAUDE_CODEX.md +33 -0
- package/README.md +114 -9
- package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
- package/dist/{chunk-GXOML4UZ.js → chunk-4BSO3HST.js} +126 -18
- package/dist/chunk-4BSO3HST.js.map +1 -0
- package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
- package/dist/chunk-57642NGU.js.map +1 -0
- package/dist/{chunk-ESZXCZIN.js → chunk-6YI2D5F5.js} +33 -5
- package/dist/chunk-6YI2D5F5.js.map +1 -0
- package/dist/{chunk-GKNYMNFM.js → chunk-7IPSDDO5.js} +10 -3
- package/dist/chunk-7IPSDDO5.js.map +1 -0
- package/dist/chunk-BTTQWSDS.js +372 -0
- package/dist/chunk-BTTQWSDS.js.map +1 -0
- package/dist/{chunk-2WH2Q4TA.js → chunk-D5E7SGI5.js} +304 -77
- package/dist/chunk-D5E7SGI5.js.map +1 -0
- package/dist/chunk-EMWGLKPR.js +45 -0
- package/dist/chunk-EMWGLKPR.js.map +1 -0
- package/dist/chunk-GQKWUHMA.js +20 -0
- package/dist/chunk-GQKWUHMA.js.map +1 -0
- package/dist/chunk-ITXE27SZ.js +358 -0
- package/dist/chunk-ITXE27SZ.js.map +1 -0
- package/dist/{chunk-UK6XTS2Y.js → chunk-JJQDVGF7.js} +23 -23
- package/dist/chunk-JJQDVGF7.js.map +1 -0
- package/dist/{chunk-F4BJ3DXW.js → chunk-KQCP3ZYH.js} +1273 -331
- package/dist/chunk-KQCP3ZYH.js.map +1 -0
- package/dist/{chunk-QN76UJP7.js → chunk-KYRM3HPP.js} +89 -21
- package/dist/chunk-KYRM3HPP.js.map +1 -0
- package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
- package/dist/chunk-M75SN4D4.js.map +1 -0
- package/dist/{chunk-J4T6HHI4.js → chunk-RZ266NHE.js} +538 -317
- package/dist/chunk-RZ266NHE.js.map +1 -0
- package/dist/chunk-U7KDM62P.js +43 -0
- package/dist/chunk-U7KDM62P.js.map +1 -0
- package/dist/chunk-UATJCD2T.js +368 -0
- package/dist/chunk-UATJCD2T.js.map +1 -0
- package/dist/{chunk-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
- package/dist/chunk-UOTKHSQ5.js.map +1 -0
- package/dist/{chunk-TLWGZVPC.js → chunk-UR3DF74I.js} +1007 -132
- package/dist/chunk-UR3DF74I.js.map +1 -0
- package/dist/chunk-UTPC66MV.js +128 -0
- package/dist/chunk-UTPC66MV.js.map +1 -0
- package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
- package/dist/chunk-V7PT6ER5.js.map +1 -0
- package/dist/chunk-WHKXUXHE.js +156 -0
- package/dist/chunk-WHKXUXHE.js.map +1 -0
- package/dist/{chunk-SRA4MDVG.js → chunk-YMXFSHXT.js} +978 -950
- package/dist/chunk-YMXFSHXT.js.map +1 -0
- package/dist/cli/index.js +23 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
- package/dist/config-TXQJCCVI.js +15 -0
- package/dist/expand-KWYGNCKZ.js +98 -0
- package/dist/expand-KWYGNCKZ.js.map +1 -0
- package/dist/file-vectors-MKXTSGGH.js +19 -0
- package/dist/index-facts-4MTEHKMN.js +38 -0
- package/dist/index-pass-TBDUEZCB.js +14 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +25 -23
- package/dist/install-E3JWINTN.js +31 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-MLTAFS5L.js +29 -0
- package/dist/local-75DJECVR.js +48 -0
- package/dist/local-llm-MECVJTVU.js +19 -0
- package/dist/{main-YJKSYFPT.js → main-RLMKQZW4.js} +999 -328
- package/dist/main-RLMKQZW4.js.map +1 -0
- package/dist/{memory-TGHVTBME.js → memory-PFFVMTCH.js} +9 -6
- package/dist/{memory-TGHVTBME.js.map → memory-PFFVMTCH.js.map} +1 -1
- package/dist/{operations-OG4ISWIM.js → operations-STR7S5UO.js} +8 -7
- package/dist/operations-STR7S5UO.js.map +1 -0
- package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
- package/dist/pretool-command-B5GY5T6E.js.map +1 -0
- package/dist/prompt-gate-TDTFFQW2.js +8 -0
- package/dist/prompt-gate-TDTFFQW2.js.map +1 -0
- package/dist/{rerank-IQAEWLQR.js → rerank-XXKWDI5Q.js} +7 -4
- package/dist/{rerank-IQAEWLQR.js.map → rerank-XXKWDI5Q.js.map} +1 -1
- package/dist/{runtime-GCSEIAVD.js → runtime-SWFWGTQ4.js} +18 -9
- package/dist/runtime-SWFWGTQ4.js.map +1 -0
- package/dist/summaries-QXWFFDUZ.js +31 -0
- package/dist/summaries-QXWFFDUZ.js.map +1 -0
- package/dist/{zvec-XDZNZ2OL.js → zvec-F73IBWO5.js} +13 -6
- package/dist/zvec-F73IBWO5.js.map +1 -0
- package/package.json +13 -3
- package/dist/chunk-2WH2Q4TA.js.map +0 -1
- package/dist/chunk-6NZEZJYD.js +0 -206
- package/dist/chunk-6NZEZJYD.js.map +0 -1
- package/dist/chunk-CAEPHLHC.js.map +0 -1
- package/dist/chunk-ENKDFLOO.js.map +0 -1
- package/dist/chunk-ESZXCZIN.js.map +0 -1
- package/dist/chunk-F4BJ3DXW.js.map +0 -1
- package/dist/chunk-GKNYMNFM.js.map +0 -1
- package/dist/chunk-GXOML4UZ.js.map +0 -1
- package/dist/chunk-ITQO632W.js +0 -673
- package/dist/chunk-ITQO632W.js.map +0 -1
- package/dist/chunk-J4T6HHI4.js.map +0 -1
- package/dist/chunk-KNR2P2UY.js.map +0 -1
- package/dist/chunk-QN76UJP7.js.map +0 -1
- package/dist/chunk-SFMIMLXR.js +0 -63
- package/dist/chunk-SFMIMLXR.js.map +0 -1
- package/dist/chunk-SRA4MDVG.js.map +0 -1
- package/dist/chunk-TLWGZVPC.js.map +0 -1
- package/dist/chunk-UK6XTS2Y.js.map +0 -1
- package/dist/chunk-W7GAXL6C.js.map +0 -1
- package/dist/config-GEZFLXK6.js +0 -12
- package/dist/install-BXM56QCD.js +0 -19
- package/dist/lean-command-7DFW32MA.js +0 -19
- package/dist/main-YJKSYFPT.js.map +0 -1
- package/dist/summaries-ML53R7EL.js +0 -21
- /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
- /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
- /package/dist/{install-BXM56QCD.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
- /package/dist/{lean-command-7DFW32MA.js.map → index-facts-4MTEHKMN.js.map} +0 -0
- /package/dist/{operations-OG4ISWIM.js.map → index-pass-TBDUEZCB.js.map} +0 -0
- /package/dist/{pretool-command-PRFOIHMW.js.map → install-E3JWINTN.js.map} +0 -0
- /package/dist/{runtime-GCSEIAVD.js.map → lean-MLTAFS5L.js.map} +0 -0
- /package/dist/{summaries-ML53R7EL.js.map → local-75DJECVR.js.map} +0 -0
- /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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
74
|
-
import { basename, dirname as
|
|
75
|
-
import
|
|
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
|
|
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((
|
|
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((
|
|
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) =>
|
|
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
|
|
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 (
|
|
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
|
|
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)
|
|
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
|
|
740
|
-
const contained = containedRelativePath(input.repositoryRoot,
|
|
741
|
-
if (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 ||
|
|
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)
|
|
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
|
-
[...
|
|
757
|
+
[...paths2].map((path) => realPathInside(realRoot, resolve3(input.repositoryRoot, path)))
|
|
753
758
|
);
|
|
754
|
-
const resolved = [...
|
|
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
|
|
1003
|
-
import { dirname as dirname3, join as join3,
|
|
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 =
|
|
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
|
|
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
|
|
1187
|
+
const paths2 = new Set((input.paths ?? []).map(normalizePath3));
|
|
1120
1188
|
const taskWords = words(input.task ?? "");
|
|
1121
|
-
const targeted =
|
|
1189
|
+
const targeted = paths2.size > 0 || taskWords.size > 0;
|
|
1122
1190
|
const ranked = entries.map((entry, recency) => {
|
|
1123
|
-
const fileHits = entry.files.filter((
|
|
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
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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 =
|
|
1404
|
+
var execFileAsync = promisify2(execFile2);
|
|
1343
1405
|
async function buildBootstrapContext(input) {
|
|
1344
|
-
const repositoryRoot =
|
|
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 =
|
|
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,
|
|
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((
|
|
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,
|
|
1665
|
+
function summarizeMatchingPolicyRules(rules, paths2) {
|
|
1600
1666
|
const matching = rules.filter(
|
|
1601
|
-
(rule) =>
|
|
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,
|
|
1689
|
+
function computeNeighbours(map, paths2) {
|
|
1624
1690
|
const result = [];
|
|
1625
|
-
for (const path of
|
|
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,
|
|
1748
|
-
if (
|
|
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
|
-
...
|
|
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
|
-
...
|
|
1842
|
+
...paths2
|
|
1777
1843
|
],
|
|
1778
|
-
["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...
|
|
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,
|
|
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,
|
|
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", "--", ...
|
|
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/
|
|
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 {
|
|
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
|
|
2294
|
-
import { dirname as
|
|
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 =
|
|
2330
|
-
this.reviewDirectory =
|
|
2331
|
-
this.archiveDirectory =
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3303
|
+
await mkdir5(this.archiveDirectory, { recursive: true, mode: 448 });
|
|
2416
3304
|
await assertNoSymlinkBetween(this.repositoryRoot, this.archiveDirectory);
|
|
2417
|
-
await rename(item.path,
|
|
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(
|
|
3309
|
+
return this.safePath(join7(this.reviewDirectory, `${id}.md`));
|
|
2422
3310
|
}
|
|
2423
3311
|
safePath(path) {
|
|
2424
|
-
const absolute =
|
|
2425
|
-
const relativePath =
|
|
2426
|
-
if (relativePath === ".." || relativePath.startsWith(`..${
|
|
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,
|
|
2433
|
-
await
|
|
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 =
|
|
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
|
|
3772
|
+
return createHash3("sha256").update(canonical).digest("hex");
|
|
2885
3773
|
}
|
|
2886
3774
|
async function approveReviewItem(id, options = {}) {
|
|
2887
|
-
const repositoryRoot =
|
|
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
|
|
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
|
|
2941
|
-
const provider =
|
|
3828
|
+
const file2 = await readConfigFile(repositoryRoot);
|
|
3829
|
+
const provider = file2.rerank?.provider ?? "off";
|
|
2942
3830
|
if (provider === "off") return void 0;
|
|
2943
|
-
const model =
|
|
2944
|
-
const bounded = (scores) =>
|
|
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 ??
|
|
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-
|
|
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
|
|
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
|
|
3001
|
-
import { join as
|
|
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 =
|
|
3895
|
+
this.directory = join8(resolve9(repositoryRoot), ".codebuddy", "cache", "hooks");
|
|
3008
3896
|
}
|
|
3009
|
-
async add(sessionId,
|
|
3897
|
+
async add(sessionId, paths2) {
|
|
3010
3898
|
await this.serialize(async () => {
|
|
3011
|
-
const
|
|
3012
|
-
if (
|
|
3013
|
-
await
|
|
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), ...
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3994
|
+
await rm4(this.claimPath(sessionId), { force: true });
|
|
3098
3995
|
});
|
|
3099
|
-
await
|
|
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
|
|
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
|
|
4009
|
+
await rm4(this.claimPath(sessionId), { force: true });
|
|
3113
4010
|
});
|
|
3114
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3178
|
-
return
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
3254
|
-
await new HookStagingStore(repositoryRoot).add(input.session_id ?? "default",
|
|
3255
|
-
|
|
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-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
4217
|
+
void 0,
|
|
3289
4218
|
changedFiles,
|
|
3290
|
-
input.last_assistant_message
|
|
4219
|
+
transcript?.assistant ?? input.last_assistant_message
|
|
3291
4220
|
);
|
|
3292
|
-
const verification =
|
|
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 =
|
|
3343
|
-
const dirty =
|
|
3344
|
-
await
|
|
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-
|
|
4278
|
+
const { createRuntime } = await import("./operations-STR7S5UO.js");
|
|
3350
4279
|
const runtime = await createRuntime(config, { repositoryRoot }).catch(async (error) => {
|
|
3351
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3416
|
-
return
|
|
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
|
|
4408
|
+
const paths2 = [];
|
|
3444
4409
|
const push = (value) => {
|
|
3445
|
-
if (typeof value === "string" && value.trim())
|
|
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(
|
|
4428
|
+
return [...new Set(paths2)];
|
|
3464
4429
|
}
|
|
3465
4430
|
function toRepoRelative(repositoryRoot, path) {
|
|
3466
|
-
const absolute =
|
|
3467
|
-
const rel =
|
|
3468
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${
|
|
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
|
-
|
|
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((
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
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
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
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
|
-
|
|
4635
|
+
capsules.push({
|
|
3720
4636
|
priority: 1,
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
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
|
-
|
|
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-
|
|
4736
|
+
//# sourceMappingURL=chunk-KQCP3ZYH.js.map
|