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