@orquentra/codebuddy 2.6.0 → 2.7.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 +6 -4
- package/WORKFLOW.md +5 -5
- package/dist/{chunk-U4HAJLIB.js → chunk-3QGW53GD.js} +16 -3
- package/dist/chunk-3QGW53GD.js.map +1 -0
- package/dist/{chunk-L5AYZC3F.js → chunk-5EDGAZQN.js} +438 -68
- package/dist/chunk-5EDGAZQN.js.map +1 -0
- package/dist/chunk-5UH3BEQR.js +32 -0
- package/dist/chunk-5UH3BEQR.js.map +1 -0
- package/dist/{chunk-JFDRBPYF.js → chunk-6JCKMIQY.js} +2 -2
- package/dist/{chunk-UN57FKQX.js → chunk-AK2UAE32.js} +152 -862
- package/dist/chunk-AK2UAE32.js.map +1 -0
- package/dist/{chunk-GDJH7XJC.js → chunk-AL6VBTNA.js} +77 -3
- package/dist/chunk-AL6VBTNA.js.map +1 -0
- package/dist/{chunk-WHZSVY2L.js → chunk-AQHPEJR6.js} +94 -23
- package/dist/chunk-AQHPEJR6.js.map +1 -0
- package/dist/{chunk-PCE4STAY.js → chunk-BQM7FJ7D.js} +6 -6
- package/dist/chunk-BQM7FJ7D.js.map +1 -0
- package/dist/{chunk-XBJQKCLU.js → chunk-CAEPHLHC.js} +13 -9
- package/dist/chunk-CAEPHLHC.js.map +1 -0
- package/dist/chunk-CG5B3LYG.js +66 -0
- package/dist/chunk-CG5B3LYG.js.map +1 -0
- package/dist/chunk-CSRD5OQA.js +79 -0
- package/dist/chunk-CSRD5OQA.js.map +1 -0
- package/dist/{chunk-IHQM2ZDV.js → chunk-DFV2SK6C.js} +1 -1
- package/dist/{chunk-IHQM2ZDV.js.map → chunk-DFV2SK6C.js.map} +1 -1
- package/dist/chunk-EEUFU7KV.js +1634 -0
- package/dist/chunk-EEUFU7KV.js.map +1 -0
- package/dist/{chunk-UFZD3JAA.js → chunk-ESZXCZIN.js} +3 -3
- package/dist/{chunk-PYLH4MH5.js → chunk-FJOTP632.js} +1 -1
- package/dist/{chunk-SYWJG5NA.js → chunk-GKNYMNFM.js} +17 -7
- package/dist/{chunk-SYWJG5NA.js.map → chunk-GKNYMNFM.js.map} +1 -1
- package/dist/{chunk-JPHJBCZB.js → chunk-HPOR6YXA.js} +3 -2
- package/dist/chunk-HPOR6YXA.js.map +1 -0
- package/dist/chunk-JUAFCLRB.js +1014 -0
- package/dist/chunk-JUAFCLRB.js.map +1 -0
- package/dist/{chunk-776EQXDV.js → chunk-KNR2P2UY.js} +110 -83
- package/dist/chunk-KNR2P2UY.js.map +1 -0
- package/dist/{chunk-N34XHWBA.js → chunk-LCSBL5TT.js} +24 -11
- package/dist/chunk-LCSBL5TT.js.map +1 -0
- package/dist/{chunk-QDBDYCM2.js → chunk-MSUAVR3R.js} +76 -11
- package/dist/chunk-MSUAVR3R.js.map +1 -0
- package/dist/{chunk-6MKEIDX5.js → chunk-SFMIMLXR.js} +2 -2
- package/dist/chunk-TGFTKWOR.js +16 -0
- package/dist/chunk-TGFTKWOR.js.map +1 -0
- package/dist/chunk-TUR6YKEE.js +55 -0
- package/dist/chunk-TUR6YKEE.js.map +1 -0
- package/dist/chunk-Y7D7PUBE.js +321 -0
- package/dist/chunk-Y7D7PUBE.js.map +1 -0
- package/dist/{chunk-VAKBJV26.js → chunk-YJIA2MFL.js} +918 -2238
- package/dist/chunk-YJIA2MFL.js.map +1 -0
- package/dist/cli/index.js +14 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/{cloud-7WM3YLI4.js → cloud-LXZUR3U2.js} +3 -3
- package/dist/{codebuddy-DM-i0Fu5.d.ts → codebuddy-DO7ZhoWn.d.ts} +37 -3
- package/dist/config-GEZFLXK6.js +12 -0
- package/dist/docker-autostart-KDME7TVC.js +12 -0
- package/dist/index.d.ts +48 -3
- package/dist/index.js +32 -25
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-command-CWJAVQ5G.js +18 -0
- package/dist/local-ST4WMRCG.js +17 -0
- package/dist/{main-HFHLGID4.js → main-AHNGHC4L.js} +327 -329
- package/dist/main-AHNGHC4L.js.map +1 -0
- package/dist/{memory-4RGVLPKG.js → memory-TGHVTBME.js} +9 -8
- package/dist/memory-TGHVTBME.js.map +1 -0
- package/dist/operations-7BTAAUHF.js +26 -0
- package/dist/postgres-docker-QQZQ2ABK.js +16 -0
- package/dist/pretool-command-3SXQSMAO.js +13 -0
- package/dist/{rerank-3P3C2UHN.js → rerank-IQAEWLQR.js} +6 -5
- package/dist/rerank-IQAEWLQR.js.map +1 -0
- package/dist/runtime-E7JQZ2BZ.js +46 -0
- package/dist/runtime-E7JQZ2BZ.js.map +1 -0
- package/dist/stdio-HOV4P3DQ.js +12 -0
- package/dist/stdio-HOV4P3DQ.js.map +1 -0
- package/dist/summaries-XV3JGGCC.js +20 -0
- package/dist/summaries-XV3JGGCC.js.map +1 -0
- package/dist/{zvec-MAPSJQSR.js → zvec-KP4KNB7Y.js} +10 -3
- package/dist/zvec-KP4KNB7Y.js.map +1 -0
- package/package.json +7 -10
- package/src/db/migrations/0006_fact_idempotency_key.sql +2 -0
- package/src/db/migrations/meta/0006_snapshot.json +1258 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/dist/chunk-776EQXDV.js.map +0 -1
- package/dist/chunk-GDJH7XJC.js.map +0 -1
- package/dist/chunk-JPHJBCZB.js.map +0 -1
- package/dist/chunk-L5AYZC3F.js.map +0 -1
- package/dist/chunk-N34XHWBA.js.map +0 -1
- package/dist/chunk-PCE4STAY.js.map +0 -1
- package/dist/chunk-PQK3GKZH.js +0 -98
- package/dist/chunk-PQK3GKZH.js.map +0 -1
- package/dist/chunk-QDBDYCM2.js.map +0 -1
- package/dist/chunk-U4HAJLIB.js.map +0 -1
- package/dist/chunk-UN57FKQX.js.map +0 -1
- package/dist/chunk-VAKBJV26.js.map +0 -1
- package/dist/chunk-WHZSVY2L.js.map +0 -1
- package/dist/chunk-XBJQKCLU.js.map +0 -1
- package/dist/config-KFEHLV6W.js +0 -11
- package/dist/lean-command-C5YPFKHP.js +0 -15
- package/dist/local-OTOY56B4.js +0 -13
- package/dist/main-HFHLGID4.js.map +0 -1
- package/dist/memory-4RGVLPKG.js.map +0 -1
- package/dist/operations-LOBAQRLO.js +0 -24
- package/dist/pretool-command-MQQ2UNQM.js +0 -10
- package/dist/rerank-3P3C2UHN.js.map +0 -1
- package/dist/summaries-ZBV5IN7Y.js +0 -18
- /package/dist/{chunk-JFDRBPYF.js.map → chunk-6JCKMIQY.js.map} +0 -0
- /package/dist/{chunk-UFZD3JAA.js.map → chunk-ESZXCZIN.js.map} +0 -0
- /package/dist/{chunk-PYLH4MH5.js.map → chunk-FJOTP632.js.map} +0 -0
- /package/dist/{chunk-6MKEIDX5.js.map → chunk-SFMIMLXR.js.map} +0 -0
- /package/dist/{cloud-7WM3YLI4.js.map → cloud-LXZUR3U2.js.map} +0 -0
- /package/dist/{config-KFEHLV6W.js.map → config-GEZFLXK6.js.map} +0 -0
- /package/dist/{lean-command-C5YPFKHP.js.map → docker-autostart-KDME7TVC.js.map} +0 -0
- /package/dist/{local-OTOY56B4.js.map → lean-command-CWJAVQ5G.js.map} +0 -0
- /package/dist/{operations-LOBAQRLO.js.map → local-ST4WMRCG.js.map} +0 -0
- /package/dist/{pretool-command-MQQ2UNQM.js.map → operations-7BTAAUHF.js.map} +0 -0
- /package/dist/{summaries-ZBV5IN7Y.js.map → postgres-docker-QQZQ2ABK.js.map} +0 -0
- /package/dist/{zvec-MAPSJQSR.js.map → pretool-command-3SXQSMAO.js.map} +0 -0
|
@@ -1,188 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordTrace
|
|
3
|
+
} from "./chunk-TUR6YKEE.js";
|
|
1
4
|
import {
|
|
2
5
|
MEMORY_CLASSES,
|
|
3
6
|
memoryCandidateSchema
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DFV2SK6C.js";
|
|
5
8
|
import {
|
|
6
9
|
SessionCapsuleLedger,
|
|
7
10
|
detectTestCommand,
|
|
8
11
|
estimateTokens,
|
|
9
12
|
findCoveringTest
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GKNYMNFM.js";
|
|
14
|
+
import {
|
|
15
|
+
hookRoot
|
|
16
|
+
} from "./chunk-TGFTKWOR.js";
|
|
17
|
+
import {
|
|
18
|
+
INCIDENT_SEVERITIES,
|
|
19
|
+
MemoryFileStore,
|
|
20
|
+
admitsLexically,
|
|
21
|
+
isEligibleMemory,
|
|
22
|
+
lexicalTerms,
|
|
23
|
+
termOverlap
|
|
24
|
+
} from "./chunk-JUAFCLRB.js";
|
|
25
|
+
import {
|
|
26
|
+
generateEntityId
|
|
27
|
+
} from "./chunk-CAEPHLHC.js";
|
|
11
28
|
import {
|
|
12
29
|
TASK_PATH_LIMIT,
|
|
13
|
-
buildArchitectureMap,
|
|
14
30
|
countTerms,
|
|
15
31
|
discoverTaskPaths,
|
|
16
|
-
generateGraphifyGraph,
|
|
17
32
|
identifierTokens,
|
|
18
|
-
indexRepository,
|
|
19
|
-
loadArchitectureMap,
|
|
20
33
|
loadFreshIndex,
|
|
21
34
|
loadProjectArchitectureMap,
|
|
22
35
|
namedSegments,
|
|
23
|
-
neighbours,
|
|
24
|
-
queryMap,
|
|
25
36
|
readBoundedSource,
|
|
37
|
+
syncFileVectors,
|
|
26
38
|
taskTerms
|
|
27
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-5EDGAZQN.js";
|
|
28
40
|
import {
|
|
29
41
|
extractSymbolTable,
|
|
30
42
|
supportsSymbols
|
|
31
|
-
} from "./chunk-
|
|
32
|
-
import {
|
|
33
|
-
INCIDENT_SEVERITIES,
|
|
34
|
-
MemoryFileStore,
|
|
35
|
-
codeBuddyConfigSchema,
|
|
36
|
-
createRuntime,
|
|
37
|
-
isEligibleMemory,
|
|
38
|
-
lexicalTerms,
|
|
39
|
-
listFactsPage,
|
|
40
|
-
termOverlap
|
|
41
|
-
} from "./chunk-UN57FKQX.js";
|
|
43
|
+
} from "./chunk-Y7D7PUBE.js";
|
|
42
44
|
import {
|
|
45
|
+
REDACTED_LINE,
|
|
43
46
|
claimText,
|
|
47
|
+
isCredentialPath,
|
|
48
|
+
loadKnownSecretValues,
|
|
44
49
|
redactPersisted,
|
|
45
50
|
redactSensitive,
|
|
51
|
+
redactSourceText,
|
|
46
52
|
scanFields,
|
|
47
53
|
scanSensitive
|
|
48
|
-
} from "./chunk-
|
|
49
|
-
import {
|
|
50
|
-
generateEntityId
|
|
51
|
-
} from "./chunk-XBJQKCLU.js";
|
|
54
|
+
} from "./chunk-AL6VBTNA.js";
|
|
52
55
|
import {
|
|
53
56
|
DEFAULT_RERANK_MODELS
|
|
54
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-HPOR6YXA.js";
|
|
55
58
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
loadPlanPolicy,
|
|
60
|
+
loadRuntimeConfig,
|
|
61
|
+
readConfigFile
|
|
62
|
+
} from "./chunk-KNR2P2UY.js";
|
|
58
63
|
import {
|
|
59
64
|
assertNoSymlinkBetween,
|
|
60
65
|
composeMarkdown,
|
|
61
|
-
configPath,
|
|
62
|
-
loadPlanPolicy,
|
|
63
|
-
loadRuntimeConfig,
|
|
64
66
|
parseFrontMatter,
|
|
65
|
-
readConfigFile,
|
|
66
|
-
redactSecrets,
|
|
67
67
|
writeAtomic
|
|
68
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-CG5B3LYG.js";
|
|
69
69
|
|
|
70
|
-
// src/
|
|
71
|
-
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
summaries: { found: 0, existing: 0, toWrite: 0, conflicts: 0, written: 0 },
|
|
80
|
-
conflicts: [],
|
|
81
|
-
failures: []
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
const [facts, summaries, existingFacts, existingSummaries] = await Promise.all([
|
|
85
|
-
input.repository.listFacts({ namespaceId: namespace.id, limit: 1e5 }),
|
|
86
|
-
input.repository.listSummaries({ namespaceId: namespace.id, limit: 1e5 }),
|
|
87
|
-
input.store.listFacts(),
|
|
88
|
-
input.store.listSummaries()
|
|
89
|
-
]);
|
|
90
|
-
const warnings = facts.length >= 1e5 || summaries.length >= 1e5 ? [
|
|
91
|
-
"namespace is at or above the 100,000-row read cap; the migration may be partial \u2014 prune or export in batches"
|
|
92
|
-
] : [];
|
|
93
|
-
const existingFactsById = new Map(existingFacts.map((fact) => [fact.id, fact]));
|
|
94
|
-
const existingSummariesById = new Map(existingSummaries.map((summary) => [summary.id, summary]));
|
|
95
|
-
const conflicts = [];
|
|
96
|
-
const failures = [];
|
|
97
|
-
const factsToWrite = facts.filter((fact) => {
|
|
98
|
-
const existing = existingFactsById.get(fact.id);
|
|
99
|
-
if (!existing) return true;
|
|
100
|
-
const databaseChecksum = checksum(redactPersisted(fact).content);
|
|
101
|
-
const fileChecksum = checksum(existing.content);
|
|
102
|
-
if (databaseChecksum !== fileChecksum) {
|
|
103
|
-
conflicts.push({ kind: "fact", id: fact.id, databaseChecksum, fileChecksum });
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
106
|
-
});
|
|
107
|
-
const summariesToWrite = summaries.filter((summary) => {
|
|
108
|
-
const existing = existingSummariesById.get(summary.id);
|
|
109
|
-
if (!existing) return true;
|
|
110
|
-
const databaseChecksum = checksum(summary.content);
|
|
111
|
-
const fileChecksum = checksum(existing.content);
|
|
112
|
-
if (databaseChecksum !== fileChecksum) {
|
|
113
|
-
conflicts.push({ kind: "summary", id: summary.id, databaseChecksum, fileChecksum });
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
});
|
|
117
|
-
let factsWritten = 0;
|
|
118
|
-
let summariesWritten = 0;
|
|
119
|
-
if (input.write) {
|
|
120
|
-
for (const fact of factsToWrite) {
|
|
121
|
-
try {
|
|
122
|
-
await input.store.writeFact({
|
|
123
|
-
id: fact.id,
|
|
124
|
-
namespace: input.namespace,
|
|
125
|
-
subject: fact.subject,
|
|
126
|
-
predicate: fact.predicate,
|
|
127
|
-
object: fact.object,
|
|
128
|
-
confidence: fact.confidence,
|
|
129
|
-
createdAt: fact.createdAt.toISOString(),
|
|
130
|
-
createdByAgent: fact.createdByAgent,
|
|
131
|
-
sourceInteractionId: fact.sourceInteractionId,
|
|
132
|
-
sourceDeleted: fact.sourceDeleted,
|
|
133
|
-
supersededBy: fact.supersededBy,
|
|
134
|
-
content: fact.content
|
|
135
|
-
});
|
|
136
|
-
factsWritten += 1;
|
|
137
|
-
} catch (error) {
|
|
138
|
-
failures.push({ kind: "fact", id: fact.id, error: error.message });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
for (const summary of summariesToWrite) {
|
|
142
|
-
try {
|
|
143
|
-
await input.store.writeSummary({
|
|
144
|
-
id: summary.id,
|
|
145
|
-
namespace: input.namespace,
|
|
146
|
-
sessionId: summary.sessionId,
|
|
147
|
-
version: summary.version,
|
|
148
|
-
tokenCount: summary.tokenCount,
|
|
149
|
-
createdAt: summary.createdAt.toISOString(),
|
|
150
|
-
createdByAgent: summary.createdByAgent,
|
|
151
|
-
content: summary.content
|
|
152
|
-
});
|
|
153
|
-
summariesWritten += 1;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
failures.push({ kind: "summary", id: summary.id, error: error.message });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const factConflicts = conflicts.filter((conflict) => conflict.kind === "fact").length;
|
|
160
|
-
const summaryConflicts = conflicts.length - factConflicts;
|
|
161
|
-
return {
|
|
162
|
-
namespace: input.namespace,
|
|
163
|
-
dryRun: !input.write,
|
|
164
|
-
...warnings.length > 0 ? { warnings } : {},
|
|
165
|
-
facts: {
|
|
166
|
-
found: facts.length,
|
|
167
|
-
existing: facts.length - factsToWrite.length - factConflicts,
|
|
168
|
-
toWrite: factsToWrite.length,
|
|
169
|
-
conflicts: factConflicts,
|
|
170
|
-
written: factsWritten
|
|
171
|
-
},
|
|
172
|
-
summaries: {
|
|
173
|
-
found: summaries.length,
|
|
174
|
-
existing: summaries.length - summariesToWrite.length - summaryConflicts,
|
|
175
|
-
toWrite: summariesToWrite.length,
|
|
176
|
-
conflicts: summaryConflicts,
|
|
177
|
-
written: summariesWritten
|
|
178
|
-
},
|
|
179
|
-
conflicts,
|
|
180
|
-
failures
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
function checksum(content) {
|
|
184
|
-
return createHash("sha256").update(content).digest("hex");
|
|
185
|
-
}
|
|
70
|
+
// src/hooks/runtime.ts
|
|
71
|
+
import { mkdir as mkdir5, readFile as readFile7, rm as rm4, stat as stat4, writeFile } from "fs/promises";
|
|
72
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative as relative5, resolve as resolve9, sep as sep5 } from "path";
|
|
73
|
+
import pTimeout2 from "p-timeout";
|
|
74
|
+
|
|
75
|
+
// src/context/engine.ts
|
|
76
|
+
import { execFile } from "child_process";
|
|
77
|
+
import { join as join4, resolve as resolve5 } from "path";
|
|
78
|
+
import { promisify } from "util";
|
|
186
79
|
|
|
187
80
|
// src/core/plan-file-store.ts
|
|
188
81
|
import { mkdir, open, readdir, readFile, rm, stat } from "fs/promises";
|
|
@@ -413,7 +306,7 @@ function formatIssues(error) {
|
|
|
413
306
|
});
|
|
414
307
|
}
|
|
415
308
|
function delay(ms) {
|
|
416
|
-
return new Promise((
|
|
309
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
417
310
|
}
|
|
418
311
|
async function releaseOwnedLock(lockPath) {
|
|
419
312
|
try {
|
|
@@ -426,357 +319,60 @@ async function releaseOwnedLock(lockPath) {
|
|
|
426
319
|
}
|
|
427
320
|
}
|
|
428
321
|
|
|
429
|
-
// src/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.name = "PlanTransitionError";
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
var PlanNotFoundError = class extends Error {
|
|
445
|
-
constructor(id) {
|
|
446
|
-
super(`Plan ${id} not found.`);
|
|
447
|
-
this.name = "PlanNotFoundError";
|
|
322
|
+
// src/risk/service.ts
|
|
323
|
+
import { spawn as spawn2 } from "child_process";
|
|
324
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
325
|
+
import { dirname as dirname2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
326
|
+
|
|
327
|
+
// src/risk/assessor.ts
|
|
328
|
+
var RiskAssessor = class {
|
|
329
|
+
signals;
|
|
330
|
+
minScore;
|
|
331
|
+
constructor(signals, options = {}) {
|
|
332
|
+
this.signals = signals;
|
|
333
|
+
this.minScore = options.minScore ?? 0;
|
|
448
334
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
335
|
+
async assess(input) {
|
|
336
|
+
const enabled = this.signals.filter((signal) => signal.enabled);
|
|
337
|
+
const settled = await Promise.all(
|
|
338
|
+
enabled.map(async (signal) => ({
|
|
339
|
+
signal,
|
|
340
|
+
contributions: await signal.assess(input)
|
|
341
|
+
}))
|
|
455
342
|
);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
].join("\n");
|
|
473
|
-
var PlanLifecycle = class {
|
|
474
|
-
store;
|
|
475
|
-
constructor(store) {
|
|
476
|
-
this.store = store;
|
|
477
|
-
}
|
|
478
|
-
async create(input) {
|
|
479
|
-
const id = generateEntityId("pln");
|
|
480
|
-
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
481
|
-
await this.store.writePlan({
|
|
482
|
-
id,
|
|
483
|
-
namespace: input.namespace,
|
|
484
|
-
title: input.title,
|
|
485
|
-
status: "draft",
|
|
486
|
-
brief: input.brief,
|
|
487
|
-
filesToTouch: input.filesToTouch ?? [],
|
|
488
|
-
testsToAdd: input.testsToAdd ?? [],
|
|
489
|
-
outOfScope: input.outOfScope ?? [],
|
|
490
|
-
risks: input.risks ?? [],
|
|
491
|
-
createdAt: now,
|
|
492
|
-
createdByAgent: input.createdByAgent ?? null,
|
|
493
|
-
approvedAt: null,
|
|
494
|
-
executingAt: null,
|
|
495
|
-
completedAt: null,
|
|
496
|
-
abandonedAt: null,
|
|
497
|
-
commitSha: null,
|
|
498
|
-
abandonReason: null,
|
|
499
|
-
body: input.body ?? DEFAULT_BODY
|
|
500
|
-
});
|
|
501
|
-
return this.store.readPlan(id);
|
|
502
|
-
}
|
|
503
|
-
async approve(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
504
|
-
const current = await this.require(id);
|
|
505
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
506
|
-
const fresh = await this.require(id);
|
|
507
|
-
this.assertTransition(fresh.status, "approved");
|
|
508
|
-
const active = await this.store.findActivePlan(fresh.namespace);
|
|
509
|
-
if (active && active.id !== id) {
|
|
510
|
-
throw new ActivePlanExistsError(fresh.namespace, active.id);
|
|
511
|
-
}
|
|
512
|
-
return this.rewrite(fresh, { status: "approved", approvedAt: now });
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
async start(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
516
|
-
const current = await this.require(id);
|
|
517
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
518
|
-
const fresh = await this.require(id);
|
|
519
|
-
this.assertTransition(fresh.status, "executing");
|
|
520
|
-
return this.rewrite(fresh, { status: "executing", executingAt: now });
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
async complete(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
524
|
-
const current = await this.require(id);
|
|
525
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
526
|
-
const fresh = await this.require(id);
|
|
527
|
-
this.assertTransition(fresh.status, "complete");
|
|
528
|
-
return this.rewrite(fresh, {
|
|
529
|
-
status: "complete",
|
|
530
|
-
completedAt: now,
|
|
531
|
-
...options.commitSha !== void 0 ? { commitSha: options.commitSha } : {}
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
async abandon(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
536
|
-
const current = await this.require(id);
|
|
537
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
538
|
-
const fresh = await this.require(id);
|
|
539
|
-
this.assertTransition(fresh.status, "abandoned");
|
|
540
|
-
return this.rewrite(fresh, {
|
|
541
|
-
status: "abandoned",
|
|
542
|
-
abandonedAt: now,
|
|
543
|
-
...options.reason !== void 0 ? { abandonReason: options.reason } : {}
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
async amend(id, patch) {
|
|
548
|
-
const current = await this.require(id);
|
|
549
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
550
|
-
const fresh = await this.require(id);
|
|
551
|
-
if (!AMENDABLE.includes(fresh.status)) {
|
|
552
|
-
throw new Error(
|
|
553
|
-
`Plan ${id} is ${fresh.status}; only draft or executing plans can be amended.`
|
|
554
|
-
);
|
|
343
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
344
|
+
const perSignal = /* @__PURE__ */ new Map();
|
|
345
|
+
for (const { signal, contributions } of settled) {
|
|
346
|
+
const stat5 = perSignal.get(signal.id) ?? {
|
|
347
|
+
id: signal.id,
|
|
348
|
+
category: signal.category,
|
|
349
|
+
contributions: 0,
|
|
350
|
+
totalWeight: 0
|
|
351
|
+
};
|
|
352
|
+
for (const contribution of contributions) {
|
|
353
|
+
if (contribution.evidence.length === 0) continue;
|
|
354
|
+
const list = byPath.get(contribution.path) ?? [];
|
|
355
|
+
list.push({ signal, contribution });
|
|
356
|
+
byPath.set(contribution.path, list);
|
|
357
|
+
stat5.contributions += 1;
|
|
358
|
+
stat5.totalWeight += contribution.weight;
|
|
555
359
|
}
|
|
556
|
-
|
|
557
|
-
...patch.title !== void 0 ? { title: patch.title } : {},
|
|
558
|
-
...patch.brief !== void 0 ? { brief: patch.brief } : {},
|
|
559
|
-
...patch.filesToTouch !== void 0 ? { filesToTouch: patch.filesToTouch } : {},
|
|
560
|
-
...patch.testsToAdd !== void 0 ? { testsToAdd: patch.testsToAdd } : {},
|
|
561
|
-
...patch.outOfScope !== void 0 ? { outOfScope: patch.outOfScope } : {},
|
|
562
|
-
...patch.risks !== void 0 ? { risks: patch.risks } : {},
|
|
563
|
-
...patch.body !== void 0 ? { body: patch.body } : {}
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
async current(namespace) {
|
|
568
|
-
return this.store.findActivePlan(namespace);
|
|
569
|
-
}
|
|
570
|
-
assertTransition(from, to) {
|
|
571
|
-
if (!TRANSITIONS[from].includes(to)) {
|
|
572
|
-
throw new PlanTransitionError(from, to);
|
|
360
|
+
perSignal.set(signal.id, stat5);
|
|
573
361
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return await this.store.readPlan(id);
|
|
578
|
-
} catch (error) {
|
|
579
|
-
if (error.code === "ENOENT") {
|
|
580
|
-
throw new PlanNotFoundError(id);
|
|
581
|
-
}
|
|
582
|
-
throw error;
|
|
362
|
+
const items = [];
|
|
363
|
+
for (const [path, contributions] of byPath) {
|
|
364
|
+
items.push(this.fold(path, contributions));
|
|
583
365
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
filesToTouch: next.filesToTouch,
|
|
595
|
-
testsToAdd: next.testsToAdd,
|
|
596
|
-
outOfScope: next.outOfScope,
|
|
597
|
-
risks: next.risks,
|
|
598
|
-
createdAt: next.createdAt,
|
|
599
|
-
createdByAgent: next.createdByAgent,
|
|
600
|
-
approvedAt: next.approvedAt,
|
|
601
|
-
executingAt: next.executingAt,
|
|
602
|
-
completedAt: next.completedAt,
|
|
603
|
-
abandonedAt: next.abandonedAt,
|
|
604
|
-
commitSha: next.commitSha,
|
|
605
|
-
abandonReason: next.abandonReason,
|
|
606
|
-
body: next.body
|
|
607
|
-
});
|
|
608
|
-
return this.store.readPlan(next.id);
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
// package.json
|
|
613
|
-
var package_default = {
|
|
614
|
-
name: "@orquentra/codebuddy",
|
|
615
|
-
version: "2.6.0",
|
|
616
|
-
description: "Local-first project context and memory for AI coding agents.",
|
|
617
|
-
type: "module",
|
|
618
|
-
license: "MIT",
|
|
619
|
-
publishConfig: {
|
|
620
|
-
access: "public"
|
|
621
|
-
},
|
|
622
|
-
author: "Orquentra Inc",
|
|
623
|
-
repository: {
|
|
624
|
-
type: "git",
|
|
625
|
-
url: "https://github.com/Orquentra-Inc/CodeBuddy"
|
|
626
|
-
},
|
|
627
|
-
engines: {
|
|
628
|
-
node: ">=20"
|
|
629
|
-
},
|
|
630
|
-
bin: {
|
|
631
|
-
codebuddy: "./dist/cli/index.js"
|
|
632
|
-
},
|
|
633
|
-
exports: {
|
|
634
|
-
".": {
|
|
635
|
-
types: "./dist/index.d.ts",
|
|
636
|
-
import: "./dist/index.js"
|
|
637
|
-
},
|
|
638
|
-
"./langgraph": {
|
|
639
|
-
types: "./dist/langgraph/index.d.ts",
|
|
640
|
-
import: "./dist/langgraph/index.js"
|
|
641
|
-
}
|
|
642
|
-
},
|
|
643
|
-
files: [
|
|
644
|
-
"dist",
|
|
645
|
-
"src/db/migrations",
|
|
646
|
-
"README.md",
|
|
647
|
-
"CLAUDE_CODEX.md",
|
|
648
|
-
"ARCHITECTURE.md",
|
|
649
|
-
"WORKFLOW.md",
|
|
650
|
-
"LICENSE",
|
|
651
|
-
"docker-compose.yml",
|
|
652
|
-
"examples"
|
|
653
|
-
],
|
|
654
|
-
scripts: {
|
|
655
|
-
build: "tsup",
|
|
656
|
-
dev: "tsx src/cli/index.ts",
|
|
657
|
-
"db:generate": "drizzle-kit generate",
|
|
658
|
-
"db:migrate": "drizzle-kit migrate",
|
|
659
|
-
typecheck: "tsc --noEmit",
|
|
660
|
-
test: "vitest run --passWithNoTests",
|
|
661
|
-
"test:pg": "bash scripts/test-pg-recovery.sh",
|
|
662
|
-
"eval:agent-ab": "tsx scripts/eval-agent-ab.mts",
|
|
663
|
-
"eval:memory-ab": "tsx scripts/eval-memory-ab.mts",
|
|
664
|
-
"eval:retrieval": "tsx scripts/eval-retrieval.mts",
|
|
665
|
-
"eval:savings": "tsx scripts/eval-task-savings.mts",
|
|
666
|
-
"branches:prune": "bash scripts/prune-merged-branches.sh",
|
|
667
|
-
lint: "biome check .",
|
|
668
|
-
format: "biome format --write .",
|
|
669
|
-
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
670
|
-
"eval:semantic": "tsx scripts/eval-semantic.mts"
|
|
671
|
-
},
|
|
672
|
-
keywords: [
|
|
673
|
-
"mcp",
|
|
674
|
-
"memory",
|
|
675
|
-
"agent",
|
|
676
|
-
"multi-agent",
|
|
677
|
-
"huggingface",
|
|
678
|
-
"pgvector",
|
|
679
|
-
"postgres",
|
|
680
|
-
"langgraph",
|
|
681
|
-
"claude"
|
|
682
|
-
],
|
|
683
|
-
homepage: "https://github.com/Orquentra-Inc/CodeBuddy",
|
|
684
|
-
bugs: {
|
|
685
|
-
url: "https://github.com/Orquentra-Inc/CodeBuddy/issues"
|
|
686
|
-
},
|
|
687
|
-
dependencies: {
|
|
688
|
-
"@clack/prompts": "^0.11.0",
|
|
689
|
-
"@huggingface/inference": "^4.13.19",
|
|
690
|
-
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
691
|
-
commander: "^14.0.0",
|
|
692
|
-
"drizzle-orm": "^0.45.2",
|
|
693
|
-
"js-tiktoken": "^1.0.21",
|
|
694
|
-
langsmith: "^0.7.10",
|
|
695
|
-
ora: "^8.2.0",
|
|
696
|
-
"p-queue": "^8.1.0",
|
|
697
|
-
"p-retry": "^6.2.1",
|
|
698
|
-
"p-timeout": "^6.1.4",
|
|
699
|
-
picocolors: "^1.1.1",
|
|
700
|
-
pino: "^9.9.0",
|
|
701
|
-
"pino-pretty": "^13.1.1",
|
|
702
|
-
postgres: "^3.4.7",
|
|
703
|
-
yaml: "^2.9.0",
|
|
704
|
-
zod: "^4.6.7"
|
|
705
|
-
},
|
|
706
|
-
peerDependencies: {
|
|
707
|
-
"@langchain/langgraph": "^0.2.0 || ^0.3.0"
|
|
708
|
-
},
|
|
709
|
-
peerDependenciesMeta: {
|
|
710
|
-
"@langchain/langgraph": {
|
|
711
|
-
optional: true
|
|
712
|
-
}
|
|
713
|
-
},
|
|
714
|
-
devDependencies: {
|
|
715
|
-
"@biomejs/biome": "^2.5.0",
|
|
716
|
-
"@types/node": "^24.3.0",
|
|
717
|
-
"drizzle-kit": "^0.31.10",
|
|
718
|
-
tsup: "^8.5.0",
|
|
719
|
-
tsx: "^4.20.5",
|
|
720
|
-
typescript: "^5.9.2",
|
|
721
|
-
vitest: "^4.1.12"
|
|
722
|
-
},
|
|
723
|
-
optionalDependencies: {
|
|
724
|
-
"@huggingface/transformers": "^4.3.2"
|
|
725
|
-
}
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
// src/version.ts
|
|
729
|
-
var VERSION = package_default.version;
|
|
730
|
-
|
|
731
|
-
// src/risk/assessor.ts
|
|
732
|
-
var RiskAssessor = class {
|
|
733
|
-
signals;
|
|
734
|
-
minScore;
|
|
735
|
-
constructor(signals, options = {}) {
|
|
736
|
-
this.signals = signals;
|
|
737
|
-
this.minScore = options.minScore ?? 0;
|
|
738
|
-
}
|
|
739
|
-
async assess(input) {
|
|
740
|
-
const enabled = this.signals.filter((signal) => signal.enabled);
|
|
741
|
-
const settled = await Promise.all(
|
|
742
|
-
enabled.map(async (signal) => ({
|
|
743
|
-
signal,
|
|
744
|
-
contributions: await signal.assess(input)
|
|
745
|
-
}))
|
|
746
|
-
);
|
|
747
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
748
|
-
const perSignal = /* @__PURE__ */ new Map();
|
|
749
|
-
for (const { signal, contributions } of settled) {
|
|
750
|
-
const stat6 = perSignal.get(signal.id) ?? {
|
|
751
|
-
id: signal.id,
|
|
752
|
-
category: signal.category,
|
|
753
|
-
contributions: 0,
|
|
754
|
-
totalWeight: 0
|
|
755
|
-
};
|
|
756
|
-
for (const contribution of contributions) {
|
|
757
|
-
if (contribution.evidence.length === 0) continue;
|
|
758
|
-
const list = byPath.get(contribution.path) ?? [];
|
|
759
|
-
list.push({ signal, contribution });
|
|
760
|
-
byPath.set(contribution.path, list);
|
|
761
|
-
stat6.contributions += 1;
|
|
762
|
-
stat6.totalWeight += contribution.weight;
|
|
763
|
-
}
|
|
764
|
-
perSignal.set(signal.id, stat6);
|
|
765
|
-
}
|
|
766
|
-
const items = [];
|
|
767
|
-
for (const [path, contributions] of byPath) {
|
|
768
|
-
items.push(this.fold(path, contributions));
|
|
769
|
-
}
|
|
770
|
-
const ranked = items.filter((item) => item.score >= this.minScore).sort(compareRiskItems);
|
|
771
|
-
return {
|
|
772
|
-
items: ranked,
|
|
773
|
-
stats: {
|
|
774
|
-
pathsConsidered: input.paths.length,
|
|
775
|
-
itemsProduced: ranked.length,
|
|
776
|
-
signalsRun: enabled.length,
|
|
777
|
-
perSignal: [...perSignal.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
778
|
-
}
|
|
779
|
-
};
|
|
366
|
+
const ranked = items.filter((item) => item.score >= this.minScore).sort(compareRiskItems);
|
|
367
|
+
return {
|
|
368
|
+
items: ranked,
|
|
369
|
+
stats: {
|
|
370
|
+
pathsConsidered: input.paths.length,
|
|
371
|
+
itemsProduced: ranked.length,
|
|
372
|
+
signalsRun: enabled.length,
|
|
373
|
+
perSignal: [...perSignal.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
374
|
+
}
|
|
375
|
+
};
|
|
780
376
|
}
|
|
781
377
|
/** Fold all contributions for one path into a single RiskItem. */
|
|
782
378
|
fold(path, contributions) {
|
|
@@ -827,9 +423,10 @@ function round2(value) {
|
|
|
827
423
|
|
|
828
424
|
// src/risk/signals/churn.ts
|
|
829
425
|
import { spawn } from "child_process";
|
|
830
|
-
import { realpath } from "fs/promises";
|
|
426
|
+
import { readFile as readFile2, realpath } from "fs/promises";
|
|
831
427
|
import { resolve as resolve2 } from "path";
|
|
832
428
|
var GIT_LOG_CONCURRENCY = 4;
|
|
429
|
+
var DAY_MS = 864e5;
|
|
833
430
|
function createChurnSignal(options) {
|
|
834
431
|
return {
|
|
835
432
|
id: "git-churn",
|
|
@@ -844,20 +441,56 @@ function createChurnSignal(options) {
|
|
|
844
441
|
if (root.code !== 0 || resolve2(root.stdout.trim()) !== expected) {
|
|
845
442
|
return [];
|
|
846
443
|
}
|
|
847
|
-
const
|
|
444
|
+
const lifetime = options.mode === "lifetime-size";
|
|
445
|
+
const asOf = options.asOf === void 0 ? void 0 : new Date(options.asOf);
|
|
446
|
+
const end = asOf ?? /* @__PURE__ */ new Date();
|
|
447
|
+
const range = [
|
|
448
|
+
...lifetime ? [] : [
|
|
449
|
+
`--since=${options.since ?? new Date(end.getTime() - (options.windowDays ?? 30) * DAY_MS).toISOString()}`
|
|
450
|
+
],
|
|
451
|
+
...asOf ? [`--until=${asOf.toISOString()}`] : []
|
|
452
|
+
];
|
|
453
|
+
const revision = lifetime && asOf ? (await runGit(options.repositoryRoot, [
|
|
454
|
+
"rev-list",
|
|
455
|
+
"-1",
|
|
456
|
+
`--before=${asOf.toISOString()}`,
|
|
457
|
+
"HEAD"
|
|
458
|
+
])).stdout.trim() : null;
|
|
848
459
|
const logs = [];
|
|
460
|
+
const sizes = [];
|
|
849
461
|
for (let start = 0; start < input.paths.length; start += GIT_LOG_CONCURRENCY) {
|
|
850
462
|
const batch = input.paths.slice(start, start + GIT_LOG_CONCURRENCY);
|
|
851
463
|
logs.push(
|
|
852
464
|
...await Promise.all(
|
|
853
|
-
batch.map((path) => gitLogForPath(options.repositoryRoot, path,
|
|
465
|
+
batch.map((path) => gitLogForPath(options.repositoryRoot, path, range))
|
|
854
466
|
)
|
|
855
467
|
);
|
|
468
|
+
if (lifetime) {
|
|
469
|
+
sizes.push(
|
|
470
|
+
...await Promise.all(
|
|
471
|
+
batch.map((path) => lineCount(options.repositoryRoot, path, revision))
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
856
475
|
}
|
|
857
476
|
const contributions = [];
|
|
858
477
|
for (const [index, path] of input.paths.entries()) {
|
|
859
478
|
const commits = logs[index] ?? [];
|
|
860
479
|
if (commits.length === 0) continue;
|
|
480
|
+
if (lifetime) {
|
|
481
|
+
const lines = sizes[index] ?? 0;
|
|
482
|
+
contributions.push({
|
|
483
|
+
path,
|
|
484
|
+
// ponytail: hand-picked coefficients; fit them on the time-split eval before defaulting on.
|
|
485
|
+
weight: Math.min(
|
|
486
|
+
100,
|
|
487
|
+
Math.round(8 * Math.log2(1 + commits.length) + 4 * Math.log2(1 + lines))
|
|
488
|
+
),
|
|
489
|
+
reason: `${path} has ${commits.length} commit${commits.length === 1 ? "" : "s"}, ${lines} line${lines === 1 ? "" : "s"} (lifetime churn and size).`,
|
|
490
|
+
evidence: evidenceOf(commits)
|
|
491
|
+
});
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
861
494
|
const authors = new Set(commits.map((commit) => commit.author));
|
|
862
495
|
const reverts = commits.filter((commit) => /revert|rollback|backout/i.test(commit.message));
|
|
863
496
|
const weight = Math.min(100, commits.length * 8 + authors.size * 12 + reverts.length * 20);
|
|
@@ -865,23 +498,38 @@ function createChurnSignal(options) {
|
|
|
865
498
|
path,
|
|
866
499
|
weight,
|
|
867
500
|
reason: `${path} changed ${commits.length} time${commits.length === 1 ? "" : "s"} in the recent git history by ${authors.size} author${authors.size === 1 ? "" : "s"}.`,
|
|
868
|
-
evidence: commits
|
|
869
|
-
kind: "git_commit",
|
|
870
|
-
sha: commit.sha,
|
|
871
|
-
message: commit.message,
|
|
872
|
-
ts: commit.ts
|
|
873
|
-
}))
|
|
501
|
+
evidence: evidenceOf(commits)
|
|
874
502
|
});
|
|
875
503
|
}
|
|
876
504
|
return contributions;
|
|
877
505
|
}
|
|
878
506
|
};
|
|
879
507
|
}
|
|
880
|
-
|
|
508
|
+
function evidenceOf(commits) {
|
|
509
|
+
return commits.slice(0, 5).map((commit) => ({
|
|
510
|
+
kind: "git_commit",
|
|
511
|
+
sha: commit.sha,
|
|
512
|
+
message: commit.message,
|
|
513
|
+
ts: commit.ts
|
|
514
|
+
}));
|
|
515
|
+
}
|
|
516
|
+
async function lineCount(repositoryRoot, path, revision) {
|
|
517
|
+
let text = "";
|
|
518
|
+
if (revision === null) {
|
|
519
|
+
text = await readFile2(resolve2(repositoryRoot, path), "utf8").catch(() => "");
|
|
520
|
+
} else if (revision) {
|
|
521
|
+
const shown = await runGit(repositoryRoot, ["show", `${revision}:${path}`]);
|
|
522
|
+
if (shown.code === 0) text = shown.stdout;
|
|
523
|
+
}
|
|
524
|
+
if (text.length === 0) return 0;
|
|
525
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
526
|
+
}
|
|
527
|
+
async function gitLogForPath(repositoryRoot, path, range) {
|
|
881
528
|
const result = await runGit(repositoryRoot, [
|
|
882
529
|
"log",
|
|
883
530
|
"--follow",
|
|
884
|
-
|
|
531
|
+
"--max-count=1000",
|
|
532
|
+
...range,
|
|
885
533
|
"--format=%H%x1f%an%x1f%cI%x1f%s%x1e",
|
|
886
534
|
"--",
|
|
887
535
|
path
|
|
@@ -893,7 +541,7 @@ async function gitLogForPath(repositoryRoot, path, since) {
|
|
|
893
541
|
}).filter((record) => record.sha.length > 0);
|
|
894
542
|
}
|
|
895
543
|
function runGit(cwd, args) {
|
|
896
|
-
return new Promise((
|
|
544
|
+
return new Promise((resolve10, reject) => {
|
|
897
545
|
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
898
546
|
let stdout = "";
|
|
899
547
|
let stderr = "";
|
|
@@ -904,7 +552,7 @@ function runGit(cwd, args) {
|
|
|
904
552
|
stderr += String(chunk);
|
|
905
553
|
});
|
|
906
554
|
child.on("error", reject);
|
|
907
|
-
child.on("close", (code) =>
|
|
555
|
+
child.on("close", (code) => resolve10({ code: code ?? 0, stdout, stderr }));
|
|
908
556
|
});
|
|
909
557
|
}
|
|
910
558
|
|
|
@@ -967,7 +615,7 @@ function severityWeight(severity) {
|
|
|
967
615
|
}
|
|
968
616
|
|
|
969
617
|
// src/risk/signals/policy.ts
|
|
970
|
-
import { readFile as
|
|
618
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
971
619
|
import { join as join2 } from "path";
|
|
972
620
|
import { parse as parseYaml2 } from "yaml";
|
|
973
621
|
import { z as z2 } from "zod";
|
|
@@ -1019,7 +667,7 @@ function createPolicySignal(options) {
|
|
|
1019
667
|
}
|
|
1020
668
|
async function readPolicyFile(path) {
|
|
1021
669
|
try {
|
|
1022
|
-
const raw = await
|
|
670
|
+
const raw = await readFile3(path, "utf8");
|
|
1023
671
|
return policyFileSchema.parse(parseYaml2(raw) ?? {});
|
|
1024
672
|
} catch (error) {
|
|
1025
673
|
if (error.code === "ENOENT") return { rules: [] };
|
|
@@ -1040,8 +688,6 @@ function escapeRegExp(value) {
|
|
|
1040
688
|
}
|
|
1041
689
|
|
|
1042
690
|
// src/risk/service.ts
|
|
1043
|
-
import { spawn as spawn2 } from "child_process";
|
|
1044
|
-
import { relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
1045
691
|
async function assessRisk(input) {
|
|
1046
692
|
const repositoryRoot = resolve3(input.repositoryRoot ?? process.cwd());
|
|
1047
693
|
const paths = await resolveRiskPaths({
|
|
@@ -1057,10 +703,17 @@ async function assessRisk(input) {
|
|
|
1057
703
|
};
|
|
1058
704
|
}
|
|
1059
705
|
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
706
|
+
const churn = (await readConfigFile(repositoryRoot).catch(() => ({ risk: void 0 }))).risk;
|
|
1060
707
|
const signals = input.signals ?? [
|
|
1061
708
|
createIncidentSignal({ store: memoryStore }),
|
|
1062
709
|
createPolicySignal({ repositoryRoot }),
|
|
1063
|
-
...input.useGit === false ? [] : [
|
|
710
|
+
...input.useGit === false ? [] : [
|
|
711
|
+
createChurnSignal({
|
|
712
|
+
repositoryRoot,
|
|
713
|
+
...churn?.churnWindowDays !== void 0 ? { windowDays: churn.churnWindowDays } : {},
|
|
714
|
+
...churn?.churnMode !== void 0 ? { mode: churn.churnMode } : {}
|
|
715
|
+
})
|
|
716
|
+
]
|
|
1064
717
|
];
|
|
1065
718
|
const assessment = await new RiskAssessor(signals).assess({
|
|
1066
719
|
paths,
|
|
@@ -1092,10 +745,27 @@ async function resolveRiskPaths(input) {
|
|
|
1092
745
|
if (contained) paths.add(contained);
|
|
1093
746
|
}
|
|
1094
747
|
}
|
|
1095
|
-
const
|
|
748
|
+
const realRoot = await realpath2(input.repositoryRoot).catch(() => resolve3(input.repositoryRoot));
|
|
749
|
+
const inside = await Promise.all(
|
|
750
|
+
[...paths].map((path) => realPathInside(realRoot, resolve3(input.repositoryRoot, path)))
|
|
751
|
+
);
|
|
752
|
+
const resolved = [...paths].filter((_, index) => inside[index]).sort();
|
|
1096
753
|
return resolved.length > MAX_RESOLVED_PATHS ? resolved.slice(0, MAX_RESOLVED_PATHS) : resolved;
|
|
1097
754
|
}
|
|
1098
755
|
var MAX_RESOLVED_PATHS = 200;
|
|
756
|
+
async function containedRepoPath(root, path) {
|
|
757
|
+
const contained = containedRelativePath(root, path);
|
|
758
|
+
if (contained === null) return null;
|
|
759
|
+
const realRoot = await realpath2(root).catch(() => resolve3(root));
|
|
760
|
+
return await realPathInside(realRoot, resolve3(root, contained)) ? contained : null;
|
|
761
|
+
}
|
|
762
|
+
async function realPathInside(realRoot, absolute) {
|
|
763
|
+
for (let current = absolute; ; current = dirname2(current)) {
|
|
764
|
+
const real = await realpath2(current).catch(() => null);
|
|
765
|
+
if (real !== null) return real === realRoot || real.startsWith(`${realRoot}${sep2}`);
|
|
766
|
+
if (dirname2(current) === current) return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
1099
769
|
function containedRelativePath(root, path) {
|
|
1100
770
|
const trimmed = normalizePath2(path);
|
|
1101
771
|
if (!trimmed || trimmed.includes("\0")) return null;
|
|
@@ -1106,18 +776,18 @@ function containedRelativePath(root, path) {
|
|
|
1106
776
|
}
|
|
1107
777
|
async function changedGitPaths(repositoryRoot) {
|
|
1108
778
|
const [tracked, untracked] = await Promise.all([
|
|
1109
|
-
|
|
1110
|
-
runGit2(repositoryRoot, ["
|
|
779
|
+
// `-z`: NUL-separated, unquoted names (non-ASCII, quotes, tabs, newlines).
|
|
780
|
+
runGit2(repositoryRoot, ["diff", "--name-only", "-z", "HEAD"]),
|
|
781
|
+
runGit2(repositoryRoot, ["ls-files", "-z", "--others", "--exclude-standard"])
|
|
1111
782
|
]);
|
|
1112
|
-
return [...tracked.stdout.split("\
|
|
1113
|
-
(path) => path && !path.startsWith(".codebuddy/cache/") && !path.startsWith(".codebuddy/worklog/")
|
|
1114
|
-
);
|
|
783
|
+
return [...tracked.stdout.split("\0"), ...untracked.stdout.split("\0")].filter((path) => path && !path.split("/").some((segment) => segment.startsWith(".")));
|
|
1115
784
|
}
|
|
1116
785
|
function runGit2(cwd, args) {
|
|
1117
786
|
return new Promise((resolveResult, reject) => {
|
|
1118
787
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1119
788
|
let stdout = "";
|
|
1120
789
|
let stderr = "";
|
|
790
|
+
child.stdout.setEncoding("utf8");
|
|
1121
791
|
child.stdout.on("data", (chunk) => {
|
|
1122
792
|
stdout += String(chunk);
|
|
1123
793
|
});
|
|
@@ -1129,212 +799,206 @@ function runGit2(cwd, args) {
|
|
|
1129
799
|
});
|
|
1130
800
|
}
|
|
1131
801
|
function normalizePath2(path) {
|
|
1132
|
-
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
802
|
+
return (sep2 === "\\" ? path.replace(/\\/g, "/") : path).replace(/^\.\//, "");
|
|
1133
803
|
}
|
|
1134
804
|
|
|
1135
|
-
// src/
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const candidates = [];
|
|
1149
|
-
const add = (section, value, priority, essential = false) => {
|
|
1150
|
-
candidates.push({ section, value, priority, essential });
|
|
1151
|
-
};
|
|
1152
|
-
for (const [section, priority] of [
|
|
1153
|
-
["diffs", 10],
|
|
1154
|
-
["neighbours", 20],
|
|
1155
|
-
["handoffs", 30],
|
|
1156
|
-
["facts", 40],
|
|
1157
|
-
["rules", 45],
|
|
1158
|
-
["symbols", 50],
|
|
1159
|
-
["policyRules", 90],
|
|
1160
|
-
["incidents", 80]
|
|
1161
|
-
]) {
|
|
1162
|
-
const values = output[section];
|
|
1163
|
-
if (!Array.isArray(values)) continue;
|
|
1164
|
-
for (const value of values) {
|
|
1165
|
-
const essential = section === "policyRules" || section === "incidents" && !value.resolved;
|
|
1166
|
-
const severity = section === "incidents" ? { critical: 4, high: 3, medium: 2, low: 1 }[value.severity] ?? 0 : 0;
|
|
1167
|
-
add(section, value, priority + severity, essential);
|
|
1168
|
-
}
|
|
1169
|
-
output[section] = [];
|
|
805
|
+
// src/core/plan-lifecycle.ts
|
|
806
|
+
var TRANSITIONS = {
|
|
807
|
+
draft: ["approved", "abandoned"],
|
|
808
|
+
approved: ["executing", "abandoned"],
|
|
809
|
+
executing: ["complete", "abandoned"],
|
|
810
|
+
complete: [],
|
|
811
|
+
abandoned: []
|
|
812
|
+
};
|
|
813
|
+
var AMENDABLE = ["draft", "executing"];
|
|
814
|
+
var PlanTransitionError = class extends Error {
|
|
815
|
+
constructor(from, to) {
|
|
816
|
+
super(`Illegal plan transition: ${from} \u2192 ${to}.`);
|
|
817
|
+
this.name = "PlanTransitionError";
|
|
1170
818
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
value,
|
|
1177
|
-
value.score >= 50 ? 100 + value.score : 60 + value.score / 100,
|
|
1178
|
-
value.score > 0
|
|
1179
|
-
);
|
|
1180
|
-
risks.items = [];
|
|
819
|
+
};
|
|
820
|
+
var PlanNotFoundError = class extends Error {
|
|
821
|
+
constructor(id) {
|
|
822
|
+
super(`Plan ${id} not found.`);
|
|
823
|
+
this.name = "PlanNotFoundError";
|
|
1181
824
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
825
|
+
};
|
|
826
|
+
var ActivePlanExistsError = class extends Error {
|
|
827
|
+
activePlanId;
|
|
828
|
+
constructor(namespace, activePlanId) {
|
|
829
|
+
super(
|
|
830
|
+
`Namespace "${namespace}" already has an active plan (${activePlanId}). Complete or abandon it before approving another.`
|
|
831
|
+
);
|
|
832
|
+
this.name = "ActivePlanExistsError";
|
|
833
|
+
this.activePlanId = activePlanId;
|
|
1185
834
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
835
|
+
};
|
|
836
|
+
var DEFAULT_BODY = [
|
|
837
|
+
"# Goal",
|
|
838
|
+
"",
|
|
839
|
+
"(what this plan achieves)",
|
|
840
|
+
"",
|
|
841
|
+
"# Approach",
|
|
842
|
+
"",
|
|
843
|
+
"(ordered steps)",
|
|
844
|
+
"",
|
|
845
|
+
"# Notes",
|
|
846
|
+
"",
|
|
847
|
+
"(freeform; ignored by tooling)"
|
|
848
|
+
].join("\n");
|
|
849
|
+
var PlanLifecycle = class {
|
|
850
|
+
store;
|
|
851
|
+
constructor(store) {
|
|
852
|
+
this.store = store;
|
|
1190
853
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
854
|
+
async create(input) {
|
|
855
|
+
await this.assertPathsInside(input);
|
|
856
|
+
const id = generateEntityId("pln");
|
|
857
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
858
|
+
await this.store.writePlan({
|
|
859
|
+
id,
|
|
860
|
+
namespace: input.namespace,
|
|
861
|
+
title: input.title,
|
|
862
|
+
status: "draft",
|
|
863
|
+
brief: input.brief,
|
|
864
|
+
filesToTouch: input.filesToTouch ?? [],
|
|
865
|
+
testsToAdd: input.testsToAdd ?? [],
|
|
866
|
+
outOfScope: input.outOfScope ?? [],
|
|
867
|
+
risks: input.risks ?? [],
|
|
868
|
+
createdAt: now,
|
|
869
|
+
createdByAgent: input.createdByAgent ?? null,
|
|
870
|
+
approvedAt: null,
|
|
871
|
+
executingAt: null,
|
|
872
|
+
completedAt: null,
|
|
873
|
+
abandonedAt: null,
|
|
874
|
+
commitSha: null,
|
|
875
|
+
abandonReason: null,
|
|
876
|
+
body: input.body ?? DEFAULT_BODY
|
|
877
|
+
});
|
|
878
|
+
return this.store.readPlan(id);
|
|
1202
879
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
else if (candidate.section === "hotspots") architecture?.hotspots.pop();
|
|
1215
|
-
else output[candidate.section].pop();
|
|
1216
|
-
};
|
|
1217
|
-
insert(candidate.value);
|
|
1218
|
-
if (candidate.essential) output.tokens.essentialOmitted--;
|
|
1219
|
-
if (account(output) <= output.tokens.budget) continue;
|
|
1220
|
-
remove();
|
|
1221
|
-
if (candidate.essential) output.tokens.essentialOmitted++;
|
|
1222
|
-
const smaller = candidate.essential ? compactSafety(candidate.value) : compact(candidate.value, 100);
|
|
1223
|
-
insert(smaller);
|
|
1224
|
-
if (account(output) <= output.tokens.budget) continue;
|
|
1225
|
-
remove();
|
|
880
|
+
async approve(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
881
|
+
const current = await this.require(id);
|
|
882
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
883
|
+
const fresh = await this.require(id);
|
|
884
|
+
this.assertTransition(fresh.status, "approved");
|
|
885
|
+
const active = await this.store.findActivePlan(fresh.namespace);
|
|
886
|
+
if (active && active.id !== id) {
|
|
887
|
+
throw new ActivePlanExistsError(fresh.namespace, active.id);
|
|
888
|
+
}
|
|
889
|
+
return this.rewrite(fresh, { status: "approved", approvedAt: now });
|
|
890
|
+
});
|
|
1226
891
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
892
|
+
async start(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
893
|
+
const current = await this.require(id);
|
|
894
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
895
|
+
const fresh = await this.require(id);
|
|
896
|
+
this.assertTransition(fresh.status, "executing");
|
|
897
|
+
return this.rewrite(fresh, { status: "executing", executingAt: now });
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
async complete(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
901
|
+
const current = await this.require(id);
|
|
902
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
903
|
+
const fresh = await this.require(id);
|
|
904
|
+
this.assertTransition(fresh.status, "complete");
|
|
905
|
+
return this.rewrite(fresh, {
|
|
906
|
+
status: "complete",
|
|
907
|
+
completedAt: now,
|
|
908
|
+
...options.commitSha !== void 0 ? { commitSha: options.commitSha } : {}
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
async abandon(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
913
|
+
const current = await this.require(id);
|
|
914
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
915
|
+
const fresh = await this.require(id);
|
|
916
|
+
this.assertTransition(fresh.status, "abandoned");
|
|
917
|
+
return this.rewrite(fresh, {
|
|
918
|
+
status: "abandoned",
|
|
919
|
+
abandonedAt: now,
|
|
920
|
+
...options.reason !== void 0 ? { abandonReason: options.reason } : {}
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
async amend(id, patch) {
|
|
925
|
+
await this.assertPathsInside(patch);
|
|
926
|
+
const current = await this.require(id);
|
|
927
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
928
|
+
const fresh = await this.require(id);
|
|
929
|
+
if (!AMENDABLE.includes(fresh.status)) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
`Plan ${id} is ${fresh.status}; only draft or executing plans can be amended.`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
return this.rewrite(fresh, {
|
|
935
|
+
...patch.title !== void 0 ? { title: patch.title } : {},
|
|
936
|
+
...patch.brief !== void 0 ? { brief: patch.brief } : {},
|
|
937
|
+
...patch.filesToTouch !== void 0 ? { filesToTouch: patch.filesToTouch } : {},
|
|
938
|
+
...patch.testsToAdd !== void 0 ? { testsToAdd: patch.testsToAdd } : {},
|
|
939
|
+
...patch.outOfScope !== void 0 ? { outOfScope: patch.outOfScope } : {},
|
|
940
|
+
...patch.risks !== void 0 ? { risks: patch.risks } : {},
|
|
941
|
+
...patch.body !== void 0 ? { body: patch.body } : {}
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
/** Plan paths flow into risk, context and file reads; none may leave the repository. */
|
|
946
|
+
async assertPathsInside(input) {
|
|
947
|
+
for (const { path } of [...input.filesToTouch ?? [], ...input.testsToAdd ?? []]) {
|
|
948
|
+
if (await containedRepoPath(this.store.repositoryRoot, path) === null) {
|
|
949
|
+
throw new Error(`Plan path is outside the repository: ${path}`);
|
|
950
|
+
}
|
|
1267
951
|
}
|
|
1268
952
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
"neighbours"
|
|
1284
|
-
];
|
|
1285
|
-
var SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
1286
|
-
async function applySessionDelivery(repositoryRoot, payload, options) {
|
|
1287
|
-
let output = { ...payload };
|
|
1288
|
-
if (!options.sessionId) return output;
|
|
1289
|
-
const sections = OMITTABLE_SECTIONS.filter((key) => output[key] !== void 0).map((key) => ({
|
|
1290
|
-
key: `mcp:${key}`,
|
|
1291
|
-
hash: createHash2("sha256").update(JSON.stringify(output[key])).digest("hex")
|
|
1292
|
-
}));
|
|
1293
|
-
try {
|
|
1294
|
-
await new SessionCapsuleLedger(repositoryRoot).exchange(options.sessionId, sections, {
|
|
1295
|
-
...options.ack !== void 0 ? { ack: options.ack } : {},
|
|
1296
|
-
ttlMs: SESSION_TTL_MS,
|
|
1297
|
-
...options.full ? { full: true } : {},
|
|
1298
|
-
prepare: (exchange) => {
|
|
1299
|
-
const unchanged = exchange.unchanged.map((key) => key.slice("mcp:".length));
|
|
1300
|
-
let avoidedTokens = 0;
|
|
1301
|
-
for (const key of unchanged) {
|
|
1302
|
-
avoidedTokens += estimateTokens(output[key]);
|
|
1303
|
-
delete output[key];
|
|
1304
|
-
}
|
|
1305
|
-
const delivery = {
|
|
1306
|
-
version: DELIVERY_VERSION,
|
|
1307
|
-
sessionId: options.sessionId,
|
|
1308
|
-
deliveryId: exchange.deliveryId,
|
|
1309
|
-
ack: exchange.ack,
|
|
1310
|
-
unchanged,
|
|
1311
|
-
avoidedTokens,
|
|
1312
|
-
note: "Pass deliveryId as ack. Use full=true after losing context."
|
|
1313
|
-
};
|
|
1314
|
-
output.delivery = delivery;
|
|
1315
|
-
output = enforceContextBudget(output);
|
|
1316
|
-
return OMITTABLE_SECTIONS.filter((key) => output[key] !== void 0).map((key) => ({
|
|
1317
|
-
key: `mcp:${key}`,
|
|
1318
|
-
hash: createHash2("sha256").update(JSON.stringify(output[key])).digest("hex")
|
|
1319
|
-
}));
|
|
953
|
+
async current(namespace) {
|
|
954
|
+
return this.store.findActivePlan(namespace);
|
|
955
|
+
}
|
|
956
|
+
assertTransition(from, to) {
|
|
957
|
+
if (!TRANSITIONS[from].includes(to)) {
|
|
958
|
+
throw new PlanTransitionError(from, to);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async require(id) {
|
|
962
|
+
try {
|
|
963
|
+
return await this.store.readPlan(id);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
if (error.code === "ENOENT") {
|
|
966
|
+
throw new PlanNotFoundError(id);
|
|
1320
967
|
}
|
|
968
|
+
throw error;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/** Rewrite the plan file with a partial set of changed fields. */
|
|
972
|
+
async rewrite(plan, changes) {
|
|
973
|
+
const next = { ...plan, ...changes };
|
|
974
|
+
await this.store.writePlan({
|
|
975
|
+
id: next.id,
|
|
976
|
+
namespace: next.namespace,
|
|
977
|
+
title: next.title,
|
|
978
|
+
status: next.status,
|
|
979
|
+
brief: next.brief,
|
|
980
|
+
filesToTouch: next.filesToTouch,
|
|
981
|
+
testsToAdd: next.testsToAdd,
|
|
982
|
+
outOfScope: next.outOfScope,
|
|
983
|
+
risks: next.risks,
|
|
984
|
+
createdAt: next.createdAt,
|
|
985
|
+
createdByAgent: next.createdByAgent,
|
|
986
|
+
approvedAt: next.approvedAt,
|
|
987
|
+
executingAt: next.executingAt,
|
|
988
|
+
completedAt: next.completedAt,
|
|
989
|
+
abandonedAt: next.abandonedAt,
|
|
990
|
+
commitSha: next.commitSha,
|
|
991
|
+
abandonReason: next.abandonReason,
|
|
992
|
+
body: next.body
|
|
1321
993
|
});
|
|
1322
|
-
return
|
|
1323
|
-
} catch {
|
|
1324
|
-
return enforceContextBudget({ ...payload });
|
|
994
|
+
return this.store.readPlan(next.id);
|
|
1325
995
|
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// src/context/engine.ts
|
|
1329
|
-
import { execFile } from "child_process";
|
|
1330
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1331
|
-
import { join as join4, resolve as resolve5 } from "path";
|
|
1332
|
-
import { promisify } from "util";
|
|
996
|
+
};
|
|
1333
997
|
|
|
1334
998
|
// src/worklog/store.ts
|
|
1335
|
-
import { createHash
|
|
1336
|
-
import { mkdir as mkdir2, readdir as readdir2, readFile as
|
|
1337
|
-
import { dirname as
|
|
999
|
+
import { createHash } from "crypto";
|
|
1000
|
+
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1001
|
+
import { dirname as dirname3, join as join3, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
|
|
1338
1002
|
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
1339
1003
|
import { z as z3 } from "zod";
|
|
1340
1004
|
var LIMITS = {
|
|
@@ -1375,7 +1039,7 @@ var WorklogStore = class {
|
|
|
1375
1039
|
const files = [...new Set((input.files ?? []).map(normalizePath3))].filter(Boolean).sort().slice(0, LIMITS.files);
|
|
1376
1040
|
const summary = boundSummary(input.summary, files);
|
|
1377
1041
|
const verification = (input.verification ?? []).map((line) => line.trim().slice(0, LIMITS.verificationChars)).filter((line) => line && !scanSensitive(line).sensitive).slice(0, LIMITS.verification);
|
|
1378
|
-
const fingerprint =
|
|
1042
|
+
const fingerprint = createHash("sha256").update(
|
|
1379
1043
|
JSON.stringify({
|
|
1380
1044
|
namespace: input.namespace.trim().toLowerCase(),
|
|
1381
1045
|
session: input.sessionId ?? null,
|
|
@@ -1387,7 +1051,7 @@ var WorklogStore = class {
|
|
|
1387
1051
|
if (existing) return existing.id;
|
|
1388
1052
|
const id = generateEntityId("wlg", now.getTime());
|
|
1389
1053
|
const path = this.entryPath(id);
|
|
1390
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
1054
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname3(path));
|
|
1391
1055
|
await mkdir2(this.directory, { recursive: true, mode: 448 });
|
|
1392
1056
|
const frontMatter = stringifyYaml2({
|
|
1393
1057
|
schemaVersion: 1,
|
|
@@ -1410,7 +1074,7 @@ var WorklogStore = class {
|
|
|
1410
1074
|
async read(id) {
|
|
1411
1075
|
const path = this.entryPath(id);
|
|
1412
1076
|
await assertNoSymlinkBetween(this.repositoryRoot, path);
|
|
1413
|
-
const parsed = parseFrontMatter(await
|
|
1077
|
+
const parsed = parseFrontMatter(await readFile4(path, "utf8"));
|
|
1414
1078
|
return {
|
|
1415
1079
|
...entrySchema.parse(parseYaml3(parsed.frontMatter)),
|
|
1416
1080
|
summary: parsed.content.trim(),
|
|
@@ -1522,90 +1186,221 @@ function words(text) {
|
|
|
1522
1186
|
return new Set(text.toLowerCase().match(/[a-z0-9_]{4,}/g) ?? []);
|
|
1523
1187
|
}
|
|
1524
1188
|
|
|
1525
|
-
// src/context/
|
|
1526
|
-
var
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
symbolsPerFile: 12,
|
|
1536
|
-
spanFiles: 3,
|
|
1537
|
-
excerptLines: 6,
|
|
1538
|
-
excerptCharsPerSpan: 400,
|
|
1539
|
-
excerptChars: 1200,
|
|
1540
|
-
relevantFacts: 5
|
|
1541
|
-
};
|
|
1542
|
-
var DEFAULT_TOKEN_BUDGET = 4e3;
|
|
1543
|
-
var MAX_DIFF_FILES = 8;
|
|
1544
|
-
var MAX_DIFF_CHARS_PER_FILE = 2400;
|
|
1545
|
-
var execFileAsync = promisify(execFile);
|
|
1546
|
-
async function buildBootstrapContext(input) {
|
|
1547
|
-
const repositoryRoot = resolve5(input.repositoryRoot ?? process.cwd());
|
|
1548
|
-
const namespace = input.namespace;
|
|
1549
|
-
const planStore = new PlanFileStore(repositoryRoot);
|
|
1550
|
-
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
1551
|
-
const freshPromise = loadFreshIndex(repositoryRoot);
|
|
1552
|
-
const manifestPromise = freshPromise.then((fresh2) => fresh2.manifest);
|
|
1553
|
-
const mapPromise = loadProjectArchitectureMap(repositoryRoot, manifestPromise);
|
|
1554
|
-
const [activePlan, policy, policyFile, incidents, facts, fresh, map, handoffs] = await Promise.all([
|
|
1555
|
-
new PlanLifecycle(planStore).current(namespace),
|
|
1556
|
-
loadPlanPolicy(repositoryRoot),
|
|
1557
|
-
readPolicyFile(join4(repositoryRoot, ".codebuddy", "policies.yaml")),
|
|
1558
|
-
memoryStore.listIncidentFacts({ namespace }),
|
|
1559
|
-
memoryStore.listFacts(),
|
|
1560
|
-
freshPromise,
|
|
1561
|
-
mapPromise,
|
|
1562
|
-
recallHandoffs(repositoryRoot, { namespace })
|
|
1563
|
-
]);
|
|
1564
|
-
const explain = [fresh.explain];
|
|
1565
|
-
const plan = summarizePlan(activePlan);
|
|
1566
|
-
explain.push(plan ? `plan: active plan ${plan.id} (${plan.status})` : "plan: no active plan");
|
|
1567
|
-
explain.push(`policy: plan policy is "${policy}"`);
|
|
1568
|
-
const policyRules = summarizePolicyRules(policyFile.rules);
|
|
1569
|
-
if (policyRules.length > 0) explain.push(`policyRules: ${policyRules.length} rule(s) by weight`);
|
|
1570
|
-
const incidentSummaries = sortIncidents(incidents).slice(0, LIMITS2.bootstrapIncidents).map(summarizeIncident);
|
|
1571
|
-
if (incidentSummaries.length > 0) {
|
|
1572
|
-
explain.push(`incidents: ${incidentSummaries.length} unresolved incident hotspot(s)`);
|
|
1573
|
-
}
|
|
1574
|
-
const architecture = summarizeArchitecture(map);
|
|
1575
|
-
explain.push(
|
|
1576
|
-
`architecture: ${architecture.moduleCount} module(s), ${architecture.edgeCount} edge(s)` + (map.source ? ` (${map.source})` : "")
|
|
1577
|
-
);
|
|
1578
|
-
const rules = selectRules(facts, namespace);
|
|
1579
|
-
if (rules.length > 0) {
|
|
1580
|
-
const procedures = rules.filter((rule) => rule.subject === "procedure").length;
|
|
1581
|
-
explain.push(`rules: ${rules.length - procedures} rule(s), ${procedures} procedure(s)`);
|
|
1582
|
-
}
|
|
1583
|
-
const bootstrapFacts = await rankRelevantFacts(
|
|
1584
|
-
facts.filter((fact) => !STANDING_PREDICATES.has(fact.predicate)),
|
|
1585
|
-
namespace,
|
|
1586
|
-
"",
|
|
1587
|
-
[]
|
|
1588
|
-
);
|
|
1589
|
-
explainHandoffs(explain, handoffs);
|
|
1590
|
-
const assembled = {
|
|
1591
|
-
project: { namespace, root: repositoryRoot },
|
|
1592
|
-
plan,
|
|
1593
|
-
policy,
|
|
1594
|
-
policyRules,
|
|
1595
|
-
incidents: incidentSummaries,
|
|
1596
|
-
facts: bootstrapFacts,
|
|
1597
|
-
rules,
|
|
1598
|
-
handoffs,
|
|
1599
|
-
architecture,
|
|
1600
|
-
explain
|
|
1189
|
+
// src/context/budget.ts
|
|
1190
|
+
var WARNING = "Context incomplete: inspect omitted policies, risks and incidents before editing.";
|
|
1191
|
+
function enforceContextBudget(input) {
|
|
1192
|
+
const output = structuredClone(input);
|
|
1193
|
+
if (!output.tokens) return input;
|
|
1194
|
+
output.tokens.essentialOmitted ??= 0;
|
|
1195
|
+
if (account(output) <= output.tokens.budget) return output;
|
|
1196
|
+
const candidates = [];
|
|
1197
|
+
const add = (section, value, priority, essential = false) => {
|
|
1198
|
+
candidates.push({ section, value, priority, essential });
|
|
1601
1199
|
};
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1200
|
+
for (const [section, priority] of [
|
|
1201
|
+
["diffs", 10],
|
|
1202
|
+
["neighbours", 20],
|
|
1203
|
+
["handoffs", 30],
|
|
1204
|
+
["facts", 40],
|
|
1205
|
+
["rules", 45],
|
|
1206
|
+
["symbols", 50],
|
|
1207
|
+
["policyRules", 90],
|
|
1208
|
+
["incidents", 80]
|
|
1209
|
+
]) {
|
|
1210
|
+
const values = output[section];
|
|
1211
|
+
if (!Array.isArray(values)) continue;
|
|
1212
|
+
for (const value of values) {
|
|
1213
|
+
const essential = section === "policyRules" || section === "incidents" && !value.resolved;
|
|
1214
|
+
const severity = section === "incidents" ? { critical: 4, high: 3, medium: 2, low: 1 }[value.severity] ?? 0 : 0;
|
|
1215
|
+
add(section, value, priority + severity, essential);
|
|
1216
|
+
}
|
|
1217
|
+
output[section] = [];
|
|
1218
|
+
}
|
|
1219
|
+
const risks = output.risks;
|
|
1220
|
+
if (risks) {
|
|
1221
|
+
for (const value of risks.items)
|
|
1222
|
+
add(
|
|
1223
|
+
"risks",
|
|
1224
|
+
value,
|
|
1225
|
+
value.score >= 50 ? 100 + value.score : 60 + value.score / 100,
|
|
1226
|
+
value.score > 0
|
|
1227
|
+
);
|
|
1228
|
+
risks.items = [];
|
|
1229
|
+
}
|
|
1230
|
+
if (output.plan) {
|
|
1231
|
+
add("plan", output.plan, 55);
|
|
1232
|
+
output.plan = null;
|
|
1233
|
+
}
|
|
1234
|
+
const architecture = output.architecture;
|
|
1235
|
+
if (architecture) {
|
|
1236
|
+
for (const value of architecture.hotspots) add("hotspots", value, 25);
|
|
1237
|
+
architecture.hotspots = [];
|
|
1238
|
+
}
|
|
1239
|
+
output.explain = [];
|
|
1240
|
+
if (typeof output.task === "string") output.task = compact(output.task, 160);
|
|
1241
|
+
const riskStats = output.risks?.stats;
|
|
1242
|
+
if (riskStats) riskStats.perSignal = [];
|
|
1243
|
+
const previousOmitted = output.tokens.essentialOmitted;
|
|
1244
|
+
output.tokens.essentialOmitted = previousOmitted + candidates.filter((c) => c.essential).length;
|
|
1245
|
+
output.tokens.warning = WARNING;
|
|
1246
|
+
if (account(output) > output.tokens.budget && output.task) output.task = null;
|
|
1247
|
+
if (estimateTokens({ project: output.project, targetPaths: output.targetPaths }) > output.tokens.budget) {
|
|
1248
|
+
account(output);
|
|
1249
|
+
return output;
|
|
1250
|
+
}
|
|
1251
|
+
candidates.sort((a, b) => b.priority - a.priority);
|
|
1252
|
+
for (const candidate of candidates) {
|
|
1253
|
+
const insert = (value) => {
|
|
1254
|
+
if (candidate.section === "plan") output.plan = value;
|
|
1255
|
+
else if (candidate.section === "risks") risks?.items.push(value);
|
|
1256
|
+
else if (candidate.section === "hotspots") architecture?.hotspots.push(value);
|
|
1257
|
+
else output[candidate.section].push(value);
|
|
1258
|
+
};
|
|
1259
|
+
const remove = () => {
|
|
1260
|
+
if (candidate.section === "plan") output.plan = null;
|
|
1261
|
+
else if (candidate.section === "risks") risks?.items.pop();
|
|
1262
|
+
else if (candidate.section === "hotspots") architecture?.hotspots.pop();
|
|
1263
|
+
else output[candidate.section].pop();
|
|
1264
|
+
};
|
|
1265
|
+
insert(candidate.value);
|
|
1266
|
+
if (candidate.essential) output.tokens.essentialOmitted--;
|
|
1267
|
+
if (account(output) <= output.tokens.budget) continue;
|
|
1268
|
+
remove();
|
|
1269
|
+
if (candidate.essential) output.tokens.essentialOmitted++;
|
|
1270
|
+
const smaller = candidate.essential ? compactSafety(candidate.value) : compact(candidate.value, 100);
|
|
1271
|
+
insert(smaller);
|
|
1272
|
+
if (account(output) <= output.tokens.budget) continue;
|
|
1273
|
+
remove();
|
|
1274
|
+
}
|
|
1275
|
+
if (output.tokens.essentialOmitted === 0) delete output.tokens.warning;
|
|
1276
|
+
account(output);
|
|
1277
|
+
return output;
|
|
1278
|
+
}
|
|
1279
|
+
function compactSafety(value) {
|
|
1280
|
+
if (!value || typeof value !== "object") return value;
|
|
1281
|
+
const item = value;
|
|
1282
|
+
return {
|
|
1283
|
+
...item,
|
|
1284
|
+
...typeof item.reason === "string" ? { reason: compact(item.reason, 100) } : {},
|
|
1285
|
+
...typeof item.summary === "string" ? { summary: compact(item.summary, 100) } : {},
|
|
1286
|
+
...Array.isArray(item.evidence) ? { evidence: item.evidence.slice(0, 1).map((entry) => compact(entry, 100)) } : {}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function compact(value, limit) {
|
|
1290
|
+
if (typeof value === "string")
|
|
1291
|
+
return value.length > limit ? `${value.slice(0, limit - 1)}\u2026` : value;
|
|
1292
|
+
if (Array.isArray(value)) return value.slice(0, 3).map((item) => compact(item, limit));
|
|
1293
|
+
if (value && typeof value === "object")
|
|
1294
|
+
return Object.fromEntries(
|
|
1295
|
+
Object.entries(value).map(([key, item]) => [key, compact(item, limit)])
|
|
1296
|
+
);
|
|
1297
|
+
return value;
|
|
1298
|
+
}
|
|
1299
|
+
function account(output) {
|
|
1300
|
+
const tokens = output.tokens;
|
|
1301
|
+
const set = (count) => {
|
|
1302
|
+
tokens.returnedEstimate = count;
|
|
1303
|
+
tokens.countedSurface = "compact_json";
|
|
1304
|
+
tokens.status = count > tokens.budget ? "overflow" : tokens.essentialOmitted > 0 ? "minimum_envelope" : "within_budget";
|
|
1305
|
+
if (tokens.status === "overflow") tokens.warning = WARNING;
|
|
1306
|
+
};
|
|
1307
|
+
for (let padding = 0; padding < 32; padding++) {
|
|
1308
|
+
if (padding > 0) output.accountingPadding = " ".repeat(padding);
|
|
1309
|
+
let count = estimateTokens(output);
|
|
1310
|
+
for (let attempt = 0; attempt < 12; attempt++) {
|
|
1311
|
+
set(count);
|
|
1312
|
+
const measured = estimateTokens(output);
|
|
1313
|
+
if (measured === count) return count;
|
|
1314
|
+
count = measured;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
throw new Error("Could not stabilize compact context token accounting");
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/context/engine.ts
|
|
1321
|
+
var LIMITS2 = {
|
|
1322
|
+
bootstrapIncidents: 5,
|
|
1323
|
+
beforeEditIncidents: 8,
|
|
1324
|
+
policyRules: 5,
|
|
1325
|
+
hotspots: 5,
|
|
1326
|
+
neighbourPaths: 10,
|
|
1327
|
+
neighbourEdges: 10,
|
|
1328
|
+
incidentSummaryChars: 140,
|
|
1329
|
+
symbolFiles: TASK_PATH_LIMIT,
|
|
1330
|
+
symbolsPerFile: 12,
|
|
1331
|
+
spanFiles: 3,
|
|
1332
|
+
excerptLines: 6,
|
|
1333
|
+
excerptCharsPerSpan: 400,
|
|
1334
|
+
excerptChars: 1200,
|
|
1335
|
+
relevantFacts: 5
|
|
1336
|
+
};
|
|
1337
|
+
var DEFAULT_TOKEN_BUDGET = 4e3;
|
|
1338
|
+
var MAX_DIFF_FILES = 8;
|
|
1339
|
+
var MAX_DIFF_CHARS_PER_FILE = 2400;
|
|
1340
|
+
var execFileAsync = promisify(execFile);
|
|
1341
|
+
async function buildBootstrapContext(input) {
|
|
1342
|
+
const repositoryRoot = resolve5(input.repositoryRoot ?? process.cwd());
|
|
1343
|
+
const namespace = input.namespace;
|
|
1344
|
+
const planStore = new PlanFileStore(repositoryRoot);
|
|
1345
|
+
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
1346
|
+
const freshPromise = loadFreshIndex(repositoryRoot);
|
|
1347
|
+
const manifestPromise = freshPromise.then((fresh2) => fresh2.manifest);
|
|
1348
|
+
const mapPromise = loadProjectArchitectureMap(repositoryRoot, manifestPromise);
|
|
1349
|
+
const [activePlan, policy, policyFile, incidents, facts, fresh, map, handoffs] = await Promise.all([
|
|
1350
|
+
new PlanLifecycle(planStore).current(namespace),
|
|
1351
|
+
loadPlanPolicy(repositoryRoot),
|
|
1352
|
+
readPolicyFile(join4(repositoryRoot, ".codebuddy", "policies.yaml")),
|
|
1353
|
+
memoryStore.listIncidentFacts({ namespace }),
|
|
1354
|
+
memoryStore.listFacts(),
|
|
1355
|
+
freshPromise,
|
|
1356
|
+
mapPromise,
|
|
1357
|
+
recallHandoffs(repositoryRoot, { namespace })
|
|
1358
|
+
]);
|
|
1359
|
+
const explain = [fresh.explain];
|
|
1360
|
+
const plan = summarizePlan(activePlan);
|
|
1361
|
+
explain.push(plan ? `plan: active plan ${plan.id} (${plan.status})` : "plan: no active plan");
|
|
1362
|
+
explain.push(`policy: plan policy is "${policy}"`);
|
|
1363
|
+
const policyRules = summarizePolicyRules(policyFile.rules);
|
|
1364
|
+
if (policyRules.length > 0) explain.push(`policyRules: ${policyRules.length} rule(s) by weight`);
|
|
1365
|
+
const incidentSummaries = sortIncidents(incidents).slice(0, LIMITS2.bootstrapIncidents).map(summarizeIncident);
|
|
1366
|
+
if (incidentSummaries.length > 0) {
|
|
1367
|
+
explain.push(`incidents: ${incidentSummaries.length} unresolved incident hotspot(s)`);
|
|
1368
|
+
}
|
|
1369
|
+
const architecture = summarizeArchitecture(map);
|
|
1370
|
+
explain.push(
|
|
1371
|
+
`architecture: ${architecture.moduleCount} module(s), ${architecture.edgeCount} edge(s)` + (map.source ? ` (${map.source})` : "")
|
|
1372
|
+
);
|
|
1373
|
+
const rules = selectRules(facts, namespace);
|
|
1374
|
+
if (rules.length > 0) {
|
|
1375
|
+
const procedures = rules.filter((rule) => rule.subject === "procedure").length;
|
|
1376
|
+
explain.push(`rules: ${rules.length - procedures} rule(s), ${procedures} procedure(s)`);
|
|
1377
|
+
}
|
|
1378
|
+
const bootstrapFacts = await rankRelevantFacts(
|
|
1379
|
+
facts.filter((fact) => !STANDING_PREDICATES.has(fact.predicate)),
|
|
1380
|
+
namespace,
|
|
1381
|
+
"",
|
|
1382
|
+
[]
|
|
1383
|
+
);
|
|
1384
|
+
explainHandoffs(explain, handoffs);
|
|
1385
|
+
const assembled = {
|
|
1386
|
+
project: { namespace, root: repositoryRoot },
|
|
1387
|
+
plan,
|
|
1388
|
+
policy,
|
|
1389
|
+
policyRules,
|
|
1390
|
+
incidents: incidentSummaries,
|
|
1391
|
+
facts: bootstrapFacts,
|
|
1392
|
+
rules,
|
|
1393
|
+
handoffs,
|
|
1394
|
+
architecture,
|
|
1395
|
+
explain
|
|
1396
|
+
};
|
|
1397
|
+
const tokens = buildTokenStats(assembled, input.tokenBudget);
|
|
1398
|
+
return enforceContextBudget({ ...assembled, tokens });
|
|
1399
|
+
}
|
|
1400
|
+
async function buildBeforeEditContext(input) {
|
|
1401
|
+
const repositoryRoot = resolve5(input.repositoryRoot ?? process.cwd());
|
|
1402
|
+
const namespace = input.namespace;
|
|
1403
|
+
const hasExplicitTargets = (input.paths?.length ?? 0) > 0 || input.planId !== void 0;
|
|
1609
1404
|
const fresh = await loadFreshIndex(repositoryRoot);
|
|
1610
1405
|
const manifest = fresh.manifest;
|
|
1611
1406
|
const resolvedPaths = await resolveRiskPaths({
|
|
@@ -1617,11 +1412,13 @@ async function buildBeforeEditContext(input) {
|
|
|
1617
1412
|
const mapPromise = loadProjectArchitectureMap(repositoryRoot, manifest);
|
|
1618
1413
|
const discovery = !hasExplicitTargets && input.task ? await discoverTaskPaths(repositoryRoot, input.task, manifest, mapPromise) : { paths: [], hits: [], explain: [] };
|
|
1619
1414
|
const taskPaths = discovery.paths;
|
|
1620
|
-
const
|
|
1415
|
+
const askedTask = Boolean(input.task?.trim());
|
|
1416
|
+
const targetPaths = hasExplicitTargets ? resolvedPaths : taskPaths.length > 0 ? taskPaths : askedTask && input.useGit !== true ? [] : resolvedPaths;
|
|
1621
1417
|
const discoveredFromTask = taskPaths.length > 0;
|
|
1622
1418
|
const pathSource = classifyPathSource(input, targetPaths.length, discoveredFromTask);
|
|
1623
1419
|
const planStore = new PlanFileStore(repositoryRoot);
|
|
1624
1420
|
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
1421
|
+
const knownSecrets = await loadKnownSecretValues(repositoryRoot);
|
|
1625
1422
|
const [referencedPlan, policy, policyFile, incidents, facts, risks, map, diffs, handoffs] = await Promise.all([
|
|
1626
1423
|
resolveRelevantPlan(planStore, namespace, input.planId),
|
|
1627
1424
|
loadPlanPolicy(repositoryRoot),
|
|
@@ -1636,7 +1433,7 @@ async function buildBeforeEditContext(input) {
|
|
|
1636
1433
|
useGit: !hasExplicitTargets && input.useGit !== false
|
|
1637
1434
|
}),
|
|
1638
1435
|
mapPromise,
|
|
1639
|
-
input.useGit === false ? Promise.resolve([]) : readGitDiffs(repositoryRoot, targetPaths),
|
|
1436
|
+
input.useGit === false ? Promise.resolve([]) : readGitDiffs(repositoryRoot, targetPaths, knownSecrets),
|
|
1640
1437
|
recallHandoffs(repositoryRoot, {
|
|
1641
1438
|
namespace,
|
|
1642
1439
|
paths: targetPaths,
|
|
@@ -1652,6 +1449,11 @@ async function buildBeforeEditContext(input) {
|
|
|
1652
1449
|
);
|
|
1653
1450
|
}
|
|
1654
1451
|
explain.push(...discovery.explain);
|
|
1452
|
+
if (askedTask && !hasExplicitTargets && taskPaths.length === 0) {
|
|
1453
|
+
explain.push(
|
|
1454
|
+
targetPaths.length > 0 ? "paths: no file matched the task; useGit asked for the uncommitted changes instead" : "paths: no file matched the task; none returned rather than unrelated changes"
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1655
1457
|
const plan = summarizePlan(referencedPlan);
|
|
1656
1458
|
if (plan) explain.push(`plan: ${plan.id} (${plan.status})`);
|
|
1657
1459
|
const policyRules = summarizeMatchingPolicyRules(policyFile.rules, targetPaths);
|
|
@@ -1663,10 +1465,10 @@ async function buildBeforeEditContext(input) {
|
|
|
1663
1465
|
explain.push(`incidents: ${incidentSummaries.length} incident(s) touch target paths`);
|
|
1664
1466
|
}
|
|
1665
1467
|
if (risks.items.length > 0) explain.push(`risks: ${risks.items.length} scored path(s)`);
|
|
1666
|
-
const
|
|
1667
|
-
if (
|
|
1468
|
+
const neighbours = computeNeighbours(map, targetPaths);
|
|
1469
|
+
if (neighbours.length > 0)
|
|
1668
1470
|
explain.push(
|
|
1669
|
-
`neighbours: import graph for ${
|
|
1471
|
+
`neighbours: import graph for ${neighbours.length} file(s)` + (map.source ? ` (${map.source})` : "")
|
|
1670
1472
|
);
|
|
1671
1473
|
const symbols = await computeTargetSymbols(
|
|
1672
1474
|
repositoryRoot,
|
|
@@ -1674,7 +1476,8 @@ async function buildBeforeEditContext(input) {
|
|
|
1674
1476
|
input.task ?? "",
|
|
1675
1477
|
discovery.hits,
|
|
1676
1478
|
"symbols" in discovery ? discovery.symbols ?? {} : {},
|
|
1677
|
-
input.evidence ?? "overview"
|
|
1479
|
+
input.evidence ?? "overview",
|
|
1480
|
+
knownSecrets
|
|
1678
1481
|
);
|
|
1679
1482
|
if (symbols.length > 0) {
|
|
1680
1483
|
const spans = symbols.reduce((total, file) => total + (file.spans?.length ?? 0), 0);
|
|
@@ -1720,7 +1523,7 @@ async function buildBeforeEditContext(input) {
|
|
|
1720
1523
|
facts: relevantFacts,
|
|
1721
1524
|
handoffs,
|
|
1722
1525
|
risks,
|
|
1723
|
-
neighbours
|
|
1526
|
+
neighbours,
|
|
1724
1527
|
symbols,
|
|
1725
1528
|
tests,
|
|
1726
1529
|
testCommand,
|
|
@@ -1841,7 +1644,7 @@ function computeNeighbours(map, paths) {
|
|
|
1841
1644
|
}
|
|
1842
1645
|
return result;
|
|
1843
1646
|
}
|
|
1844
|
-
async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, picked = {}, evidence = "overview") {
|
|
1647
|
+
async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, picked = {}, evidence = "overview", knownSecrets = []) {
|
|
1845
1648
|
const terms = taskTerms(task);
|
|
1846
1649
|
const tokens = identifierTokens(task);
|
|
1847
1650
|
const result = [];
|
|
@@ -1850,9 +1653,9 @@ async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, pic
|
|
|
1850
1653
|
for (const path of targetPaths.slice(0, LIMITS2.symbolFiles)) {
|
|
1851
1654
|
const source = await readBoundedSource(repositoryRoot, path);
|
|
1852
1655
|
if (source === null) continue;
|
|
1853
|
-
const lines = source.split(/\r?\n/);
|
|
1656
|
+
const lines = redactSourceText(source, knownSecrets).split(/\r?\n/);
|
|
1854
1657
|
const fileHits = hits.filter((hit2) => hit2.path === path && hit2.endLine <= lines.length);
|
|
1855
|
-
const table = supportsSymbols(path) ? extractSymbolTable(source) : [];
|
|
1658
|
+
const table = supportsSymbols(path) ? extractSymbolTable(source, path) : [];
|
|
1856
1659
|
const credited = /* @__PURE__ */ new Map();
|
|
1857
1660
|
for (const hit2 of fileHits) {
|
|
1858
1661
|
const owner = table.filter((symbol) => hit2.startLine <= symbol.endLine && hit2.endLine >= symbol.startLine).sort(
|
|
@@ -1868,7 +1671,9 @@ async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, pic
|
|
|
1868
1671
|
}).sort(
|
|
1869
1672
|
(left, right) => right.score - left.score || left.symbol.startLine - right.symbol.startLine
|
|
1870
1673
|
);
|
|
1871
|
-
const signatures = ranked.filter(({ symbol, score }) => symbol.kind !== "method" || score > 0).slice(0, LIMITS2.symbolsPerFile).map(
|
|
1674
|
+
const signatures = ranked.filter(({ symbol, score }) => symbol.kind !== "method" || score > 0).slice(0, LIMITS2.symbolsPerFile).map(
|
|
1675
|
+
({ symbol }) => lines[symbol.startLine - 1] === REDACTED_LINE ? REDACTED_LINE : redactSourceText(symbol.signature, knownSecrets)
|
|
1676
|
+
);
|
|
1872
1677
|
const best = ranked[0]?.score ? ranked[0] : void 0;
|
|
1873
1678
|
const hit = best ? best.hit : fileHits[0];
|
|
1874
1679
|
const range = best?.symbol ?? hit;
|
|
@@ -1937,20 +1742,38 @@ function headInSignature(lines, symbol) {
|
|
|
1937
1742
|
const line = (lines[symbol.startLine - 1] ?? "").trim();
|
|
1938
1743
|
return !head.endsWith("\u2026") && line.startsWith(head) && /^[\s{=>(]*$/.test(line.slice(head.length));
|
|
1939
1744
|
}
|
|
1940
|
-
async function readGitDiffs(repositoryRoot, paths) {
|
|
1745
|
+
async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
|
|
1941
1746
|
if (paths.length === 0) return [];
|
|
1942
1747
|
const sections = [];
|
|
1943
1748
|
try {
|
|
1944
1749
|
const { stdout } = await execFileAsync(
|
|
1945
1750
|
"git",
|
|
1946
|
-
[
|
|
1751
|
+
[
|
|
1752
|
+
"-c",
|
|
1753
|
+
"core.quotePath=false",
|
|
1754
|
+
"diff",
|
|
1755
|
+
"HEAD",
|
|
1756
|
+
"--no-ext-diff",
|
|
1757
|
+
"--unified=3",
|
|
1758
|
+
"--",
|
|
1759
|
+
...paths
|
|
1760
|
+
],
|
|
1947
1761
|
{ cwd: repositoryRoot, maxBuffer: 2e6 }
|
|
1948
1762
|
);
|
|
1949
1763
|
sections.push(stdout);
|
|
1950
1764
|
} catch {
|
|
1951
1765
|
for (const args of [
|
|
1952
|
-
[
|
|
1953
|
-
|
|
1766
|
+
[
|
|
1767
|
+
"-c",
|
|
1768
|
+
"core.quotePath=false",
|
|
1769
|
+
"diff",
|
|
1770
|
+
"--cached",
|
|
1771
|
+
"--no-ext-diff",
|
|
1772
|
+
"--unified=3",
|
|
1773
|
+
"--",
|
|
1774
|
+
...paths
|
|
1775
|
+
],
|
|
1776
|
+
["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...paths]
|
|
1954
1777
|
]) {
|
|
1955
1778
|
try {
|
|
1956
1779
|
const { stdout } = await execFileAsync("git", args, {
|
|
@@ -1962,21 +1785,59 @@ async function readGitDiffs(repositoryRoot, paths) {
|
|
|
1962
1785
|
}
|
|
1963
1786
|
}
|
|
1964
1787
|
}
|
|
1965
|
-
const parsed = parseGitDiff(sections.join("\n"));
|
|
1788
|
+
const parsed = parseGitDiff(sections.join("\n"), knownSecrets);
|
|
1966
1789
|
const known = new Set(parsed.map((diff) => diff.path));
|
|
1967
1790
|
const untracked = await listUntrackedPaths(repositoryRoot, paths);
|
|
1968
1791
|
for (const path of untracked) {
|
|
1969
1792
|
if (known.has(path)) continue;
|
|
1970
|
-
const patch = await buildUntrackedPatch(repositoryRoot, path);
|
|
1793
|
+
const patch = isCredentialPath(path) ? withheldPatch(path) : await buildUntrackedPatch(repositoryRoot, path, knownSecrets);
|
|
1971
1794
|
if (patch) parsed.push(patch);
|
|
1972
1795
|
}
|
|
1973
1796
|
return parsed.slice(0, MAX_DIFF_FILES);
|
|
1974
1797
|
}
|
|
1975
|
-
function
|
|
1798
|
+
function diffHeaderPaths(section) {
|
|
1799
|
+
const quoted = '"(?:[^"\\\\]|\\\\.)*"';
|
|
1800
|
+
const header = new RegExp(`^diff --git (${quoted}|a/.+?) (${quoted}|b/.+?)\\n`, "m").exec(
|
|
1801
|
+
section
|
|
1802
|
+
);
|
|
1803
|
+
if (!header?.[1] || !header[2]) return null;
|
|
1804
|
+
const path = (raw) => (raw.startsWith('"') ? unquoteGitPath(raw) : raw).slice(2);
|
|
1805
|
+
return [header[0], path(header[1]), path(header[2])];
|
|
1806
|
+
}
|
|
1807
|
+
var C_ESCAPES = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 };
|
|
1808
|
+
function unquoteGitPath(raw) {
|
|
1809
|
+
const bytes = [];
|
|
1810
|
+
const body = raw.slice(1, -1);
|
|
1811
|
+
for (let at = 0; at < body.length; at++) {
|
|
1812
|
+
const char = body[at];
|
|
1813
|
+
if (char !== "\\") {
|
|
1814
|
+
bytes.push(...Buffer.from(char, "utf8"));
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const next = body[++at] ?? "";
|
|
1818
|
+
const octal = /^[0-7]{3}/.exec(body.slice(at))?.[0];
|
|
1819
|
+
if (octal) {
|
|
1820
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
1821
|
+
at += 2;
|
|
1822
|
+
} else bytes.push(C_ESCAPES[next] ?? next.charCodeAt(0));
|
|
1823
|
+
}
|
|
1824
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1825
|
+
}
|
|
1826
|
+
function withheldPatch(path) {
|
|
1827
|
+
return {
|
|
1828
|
+
path,
|
|
1829
|
+
patch: `diff --git a/${path} b/${path}
|
|
1830
|
+
(contents withheld: credential file)`
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function parseGitDiff(diff, knownSecrets = []) {
|
|
1976
1834
|
return diff.split(/(?=^diff --git )/m).filter((section) => section.startsWith("diff --git ")).flatMap((section) => {
|
|
1977
|
-
const header = section
|
|
1835
|
+
const header = diffHeaderPaths(section);
|
|
1978
1836
|
if (!header?.[2]) return [];
|
|
1979
|
-
|
|
1837
|
+
if (isCredentialPath(header[1] ?? "") || isCredentialPath(header[2])) {
|
|
1838
|
+
return [withheldPatch(header[2])];
|
|
1839
|
+
}
|
|
1840
|
+
const rawPatch = redactSourceText(section.trim(), knownSecrets, true);
|
|
1980
1841
|
const patch = rawPatch.slice(0, MAX_DIFF_CHARS_PER_FILE).trim();
|
|
1981
1842
|
return patch ? [
|
|
1982
1843
|
{
|
|
@@ -1991,20 +1852,20 @@ async function listUntrackedPaths(repositoryRoot, paths) {
|
|
|
1991
1852
|
try {
|
|
1992
1853
|
const { stdout } = await execFileAsync(
|
|
1993
1854
|
"git",
|
|
1994
|
-
["ls-files", "--others", "--exclude-standard", "--", ...paths],
|
|
1855
|
+
["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths],
|
|
1995
1856
|
{ cwd: repositoryRoot, maxBuffer: 1e6 }
|
|
1996
1857
|
);
|
|
1997
|
-
return stdout.split(
|
|
1858
|
+
return stdout.split("\0").filter(Boolean);
|
|
1998
1859
|
} catch {
|
|
1999
1860
|
return [];
|
|
2000
1861
|
}
|
|
2001
1862
|
}
|
|
2002
|
-
async function buildUntrackedPatch(repositoryRoot, path) {
|
|
1863
|
+
async function buildUntrackedPatch(repositoryRoot, path, knownSecrets) {
|
|
2003
1864
|
try {
|
|
2004
|
-
const content = await
|
|
2005
|
-
if (content
|
|
1865
|
+
const content = await readBoundedSource(repositoryRoot, path);
|
|
1866
|
+
if (content === null) return null;
|
|
2006
1867
|
const lines = content.split(/\r?\n/);
|
|
2007
|
-
const body = lines.map((line) => `+${line}`).join("\n");
|
|
1868
|
+
const body = redactSourceText(lines.map((line) => `+${line}`).join("\n"), knownSecrets, true);
|
|
2008
1869
|
const rawPatch = [
|
|
2009
1870
|
`diff --git a/${path} b/${path}`,
|
|
2010
1871
|
"new file mode 100644",
|
|
@@ -2025,8 +1886,10 @@ async function buildUntrackedPatch(repositoryRoot, path) {
|
|
|
2025
1886
|
}
|
|
2026
1887
|
var STANDING_PREDICATES = /* @__PURE__ */ new Set(["rule", "procedure"]);
|
|
2027
1888
|
var STANDING_LIMITS = { rule: 5, procedure: 3, chars: 200 };
|
|
1889
|
+
var SHELL_CONTROL = /[;&|`$<>\n\r]/;
|
|
2028
1890
|
function selectRules(facts, namespace) {
|
|
2029
|
-
const
|
|
1891
|
+
const latest = new Date(Date.now() + 864e5).toISOString();
|
|
1892
|
+
const newest = (predicate) => facts.filter((fact) => fact.predicate === predicate && isEligibleMemory(fact, { namespace })).filter((fact) => fact.createdAt <= latest).filter((fact) => predicate !== "procedure" || !SHELL_CONTROL.test(fact.object)).sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, STANDING_LIMITS[predicate]);
|
|
2030
1893
|
return [...newest("rule"), ...newest("procedure")].map((fact) => ({
|
|
2031
1894
|
id: fact.id,
|
|
2032
1895
|
subject: fact.predicate,
|
|
@@ -2046,11 +1909,27 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
2046
1909
|
const incidentText = new Set(
|
|
2047
1910
|
eligible.filter((fact) => fact.category === "incident").map((fact) => normalizeText(fact.content || fact.object))
|
|
2048
1911
|
);
|
|
2049
|
-
const
|
|
1912
|
+
const terms = lexicalTerms(task).filter((term) => term.length > 2);
|
|
1913
|
+
const withTerms = eligible.filter((fact) => fact.category !== "incident").filter((fact) => !incidentText.has(normalizeText(fact.content || fact.object))).map((fact) => {
|
|
2050
1914
|
const text = `${fact.subject} ${fact.predicate} ${fact.object} ${fact.content}`;
|
|
2051
|
-
const
|
|
1915
|
+
const words2 = new Set(text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []);
|
|
1916
|
+
return { fact, text, shared: terms.filter((term) => words2.has(term)) };
|
|
1917
|
+
});
|
|
1918
|
+
const documentFrequency = new Map(
|
|
1919
|
+
terms.map((term) => [term, withTerms.filter(({ shared }) => shared.includes(term)).length])
|
|
1920
|
+
);
|
|
1921
|
+
const candidates = withTerms.map(({ fact, text, shared }) => {
|
|
1922
|
+
const taskScore = shared.length * 3;
|
|
2052
1923
|
const pathScore = fact.paths.some((path) => targetPaths.includes(path)) ? 5 : 0;
|
|
2053
|
-
|
|
1924
|
+
const admitted = admitsLexically(shared, documentFrequency, withTerms.length);
|
|
1925
|
+
return {
|
|
1926
|
+
fact,
|
|
1927
|
+
text,
|
|
1928
|
+
taskScore,
|
|
1929
|
+
pathScore,
|
|
1930
|
+
admitted,
|
|
1931
|
+
lexical: (admitted ? taskScore : 0) + pathScore
|
|
1932
|
+
};
|
|
2054
1933
|
});
|
|
2055
1934
|
const summarize = (fact, relevance, reason) => ({
|
|
2056
1935
|
id: fact.id,
|
|
@@ -2126,10 +2005,10 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
2126
2005
|
}
|
|
2127
2006
|
selected.push(candidate);
|
|
2128
2007
|
}
|
|
2129
|
-
return selected.map(({ fact, taskScore, pathScore, score }) => {
|
|
2008
|
+
return selected.map(({ fact, taskScore, pathScore, score, admitted }) => {
|
|
2130
2009
|
const evidence = [
|
|
2131
2010
|
pathScore > 0 ? "matches a target path" : "",
|
|
2132
|
-
taskScore > 0 ? `shares ${taskScore / 3} task term(s)` : "",
|
|
2011
|
+
admitted && taskScore > 0 ? `shares ${taskScore / 3} task term(s)` : "",
|
|
2133
2012
|
cosine(fact.id) !== void 0 ? `semantic ${cosine(fact.id)?.toFixed(2)}` : ""
|
|
2134
2013
|
].filter(Boolean);
|
|
2135
2014
|
return summarize(
|
|
@@ -2140,12 +2019,6 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
2140
2019
|
);
|
|
2141
2020
|
});
|
|
2142
2021
|
}
|
|
2143
|
-
function relevanceScore(value, task) {
|
|
2144
|
-
const terms = lexicalTerms(task).filter((term) => term.length > 2);
|
|
2145
|
-
if (terms.length === 0) return 0;
|
|
2146
|
-
const haystack = new Set(value.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []);
|
|
2147
|
-
return terms.filter((term) => haystack.has(term)).length * 3;
|
|
2148
|
-
}
|
|
2149
2022
|
function normalizeText(value) {
|
|
2150
2023
|
return value.toLowerCase().replace(/[^a-z0-9_./-]+/g, " ").trim();
|
|
2151
2024
|
}
|
|
@@ -2170,13 +2043,8 @@ function truncate(value, max) {
|
|
|
2170
2043
|
return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max - 1)}\u2026`;
|
|
2171
2044
|
}
|
|
2172
2045
|
|
|
2173
|
-
// src/hooks/runtime.ts
|
|
2174
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
2175
|
-
import { basename, isAbsolute, relative as relative5, resolve as resolve9, sep as sep5 } from "path";
|
|
2176
|
-
import pTimeout2 from "p-timeout";
|
|
2177
|
-
|
|
2178
2046
|
// src/memory-extract/engine.ts
|
|
2179
|
-
import { createHash as
|
|
2047
|
+
import { createHash as createHash2 } from "crypto";
|
|
2180
2048
|
import { resolve as resolve7 } from "path";
|
|
2181
2049
|
|
|
2182
2050
|
// src/memory-extract/deterministic.ts
|
|
@@ -2197,6 +2065,21 @@ var RULES = [
|
|
|
2197
2065
|
confidence: 0.8,
|
|
2198
2066
|
regex: EXPLICIT_RESOLUTION
|
|
2199
2067
|
},
|
|
2068
|
+
{
|
|
2069
|
+
// An explicit label ("Decision: …") is the agent saying this is durable.
|
|
2070
|
+
class: "decision",
|
|
2071
|
+
subject: "decision",
|
|
2072
|
+
predicate: "made",
|
|
2073
|
+
confidence: 0.8,
|
|
2074
|
+
regex: /^(?:decision|decided)\s*:/i
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
class: "fact",
|
|
2078
|
+
subject: "project",
|
|
2079
|
+
predicate: "fact",
|
|
2080
|
+
confidence: 0.75,
|
|
2081
|
+
regex: /^(?:convention|fact|rule|policy)\s*:/i
|
|
2082
|
+
},
|
|
2200
2083
|
{
|
|
2201
2084
|
class: "decision",
|
|
2202
2085
|
subject: "decision",
|
|
@@ -2216,11 +2099,20 @@ var RULES = [
|
|
|
2216
2099
|
subject: "project",
|
|
2217
2100
|
predicate: "fact",
|
|
2218
2101
|
confidence: 0.55,
|
|
2219
|
-
regex: /\b(uses?|is|are|requires?|depends on|stores?|returns?|implemented|added|supports?|configured|lives in|located|handles
|
|
2102
|
+
regex: /\b(uses?|is|are|requires?|depends on|stores?|returns?|implemented|added|supports?|configured|lives in|located|handles?|must)\b|\b(?:always|never)\b(?!\s+(?:the|a|an|mind)\b)/i
|
|
2220
2103
|
}
|
|
2221
2104
|
];
|
|
2222
2105
|
var INCIDENT_REGEX = RULES.find((rule) => rule.predicate === "occurred").regex;
|
|
2223
2106
|
var SPECULATIVE = /\b(might|maybe|possibly|could|consider(?:ing)?|planned|planning|should|if|would|unless|whether|suppose|hypothetical(?:ly)?|in theory|in case)\b/i;
|
|
2107
|
+
var HEDGED = /\b(might|maybe|perhaps|possibly|probably|could|consider(?:ing)?|planned|planning|would|whether|suppose|hypothetical(?:ly)?|in theory)\b/i;
|
|
2108
|
+
var NOT_DURABLE = /\b(?:for now|for the moment|right now|today|tonight|tomorrow|yesterday|this (?:session|turn|time|week|sprint)|for this (?:pr|change|task|ticket|branch|commit|session)|temporarily|as a temporary|i['’]ll|i will|i['’]m going to|let me|never ?mind|i think|i guess|i suspect)\b|^(?:it|this|that|there)\s+must\s+be\b/i;
|
|
2109
|
+
var DURABLE_LABEL = /^(?:decision|decided|rule|policy|convention|fact)\s*:/i;
|
|
2110
|
+
function stripDecoration(sentence) {
|
|
2111
|
+
return sentence.replace(/^(?:[-*•]\s+|\d+[.)]\s+)/, "").replace(
|
|
2112
|
+
/^(?:\*{1,2}|_{1,2})?(decision|decided|rule|policy|convention|fact)(?:\*{1,2}|_{1,2})?\s*:\s*(?:\*{1,2}|_{1,2})?\s*/i,
|
|
2113
|
+
"$1: "
|
|
2114
|
+
).trim();
|
|
2115
|
+
}
|
|
2224
2116
|
var NEGATION = /\b(no|not|never|none|nothing|nobody|without|zero|neither|nor|cannot)\b(?!\s+longer)|\w+n['’]t\b/i;
|
|
2225
2117
|
var EXAMPLE = /\b(?:e\.g|i\.e|for example|for instance|such as|example|sample|hypothetical)\b/i;
|
|
2226
2118
|
var QUOTED = /"[^"]*"|\u201c[^\u201d]*\u201d|`[^`]*`/g;
|
|
@@ -2247,17 +2139,19 @@ var SEVERITY_MARKERS = [
|
|
|
2247
2139
|
function deterministicExtract(input) {
|
|
2248
2140
|
const changedFiles = (input.changedFiles ?? []).filter(Boolean);
|
|
2249
2141
|
const candidates = [];
|
|
2250
|
-
for (const sentence of splitSentences(input.summary)) {
|
|
2142
|
+
for (const sentence of splitSentences(input.summary).map(stripDecoration)) {
|
|
2251
2143
|
const classified = classify(sentence);
|
|
2252
2144
|
const doubt = classified?.class === "incident" ? incidentDoubt(sentence) : null;
|
|
2253
|
-
const
|
|
2145
|
+
const speculative = DURABLE_LABEL.test(sentence) ? HEDGED.test(sentence) : SPECULATIVE.test(sentence) && !EXPLICIT_DECISION.test(sentence) && !EXPLICIT_RESOLUTION.test(sentence);
|
|
2146
|
+
const rule = doubt || speculative ? {
|
|
2254
2147
|
class: "note",
|
|
2255
2148
|
subject: "speculation",
|
|
2256
2149
|
predicate: "consider",
|
|
2257
2150
|
confidence: 0.3,
|
|
2258
2151
|
regex: SPECULATIVE
|
|
2259
2152
|
} : classified;
|
|
2260
|
-
|
|
2153
|
+
const passing = (rule?.class === "decision" || rule?.class === "fact") && NOT_DURABLE.test(sentence);
|
|
2154
|
+
if (!rule || passing) {
|
|
2261
2155
|
candidates.push(ignoredCandidate(sentence));
|
|
2262
2156
|
continue;
|
|
2263
2157
|
}
|
|
@@ -2395,7 +2289,7 @@ function matchResolution(fixText, pathCandidates, unresolved) {
|
|
|
2395
2289
|
|
|
2396
2290
|
// src/memory-extract/review-store.ts
|
|
2397
2291
|
import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, rename, rm as rm2 } from "fs/promises";
|
|
2398
|
-
import { dirname as
|
|
2292
|
+
import { dirname as dirname4, join as join5, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
|
|
2399
2293
|
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
2400
2294
|
import { z as z5 } from "zod";
|
|
2401
2295
|
var reviewSchema = z5.object({
|
|
@@ -2438,7 +2332,7 @@ var ReviewStore = class {
|
|
|
2438
2332
|
const input = redactPersisted(raw);
|
|
2439
2333
|
const path = this.itemPath(input.id);
|
|
2440
2334
|
await this.prepareDirectory();
|
|
2441
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
2335
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname4(path));
|
|
2442
2336
|
const frontMatter = stringifyYaml3({
|
|
2443
2337
|
schemaVersion: 1,
|
|
2444
2338
|
id: input.id,
|
|
@@ -2533,7 +2427,7 @@ var ReviewStore = class {
|
|
|
2533
2427
|
return absolute;
|
|
2534
2428
|
}
|
|
2535
2429
|
async prepareDirectory() {
|
|
2536
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
2430
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname4(this.reviewDirectory));
|
|
2537
2431
|
await mkdir3(this.reviewDirectory, { recursive: true, mode: 448 });
|
|
2538
2432
|
}
|
|
2539
2433
|
};
|
|
@@ -2544,7 +2438,8 @@ var INJECTED_BLOCK = /^##\s*CodeBuddy\b/m;
|
|
|
2544
2438
|
var FENCED_CODE = /```[\s\S]*?(?:```|$)/g;
|
|
2545
2439
|
var QUOTE_LINE = /^\s*>.*$/gm;
|
|
2546
2440
|
var RULE_PREFIX = /^(?:also,?\s+|please,?\s+)?(?:from now on|going forward|(?:a\s+)?rule\b[^:.]{0,60}:)[,:]?\s*/i;
|
|
2547
|
-
var IMPERATIVE = /^(?:please,?\s+)?(?:always|never)\s
|
|
2441
|
+
var IMPERATIVE = /^(?:please,?\s+)?(?:always|never)\s+(?!(?:the|a|an|mind|so|again|ever)\b)\S+\s+\S+|^(?:we|you)\s+(?:always|never|must)\b|^every\s+(?!time\b)\S+.*\bmust\b/i;
|
|
2442
|
+
var POLICY = /^(?:decision|decided|convention|policy)\s*:\s*\S+\s+\S+|^we\s+(?:decided|agreed|settled on|chose)\b|^(?!(?:i|it|this|that|there|he|she|they|which|what)\b)[a-z][\w-]*(?:\s+[\w./-]+){0,6}\s+must(?:\s+not|n['’]t)?\s+(?!be\s+\w+ing\b|have\s+(?:been\b|\w+ed\b))\w+/i;
|
|
2548
2443
|
var QUESTION_START = /^(?:why|how|what|when|where|which|who|can|could|would|will|do|does|did|is|are|should)\b/i;
|
|
2549
2444
|
var REVOCATION_MARKER = /^(?:please,?\s+)?stop\s+\w+ing\b|\bno longer\b|\b(?:don't|do not)\b.*\banymore\b|\b(?:forget|drop|remove)\s+the\s+rule\b/i;
|
|
2550
2445
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
@@ -2572,21 +2467,31 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
2572
2467
|
function extractUserRules(prompts) {
|
|
2573
2468
|
const rules = /* @__PURE__ */ new Map();
|
|
2574
2469
|
const revocations = /* @__PURE__ */ new Map();
|
|
2470
|
+
const policies = /* @__PURE__ */ new Map();
|
|
2575
2471
|
for (const prompt of prompts) {
|
|
2576
2472
|
const own = (prompt.split(INJECTED_BLOCK)[0] ?? "").replace(FENCED_CODE, "\n").replace(QUOTE_LINE, "");
|
|
2577
2473
|
for (const raw of own.split(/(?<=[.!?])\s+|\n+/)) {
|
|
2578
|
-
const sentence = raw.trim();
|
|
2474
|
+
const sentence = stripDecoration(raw.trim());
|
|
2579
2475
|
if (!sentence || sentence.length > MAX_RULE_CHARS || sentence.endsWith("?")) continue;
|
|
2580
2476
|
if (QUESTION_START.test(sentence)) continue;
|
|
2581
2477
|
const key = sentence.toLowerCase().replace(/\s+/g, " ").replace(/[.!]+$/, "");
|
|
2582
2478
|
if (REVOCATION_MARKER.test(sentence)) {
|
|
2583
2479
|
if (!revocations.has(key)) revocations.set(key, sentence);
|
|
2584
|
-
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
if (HEDGED.test(sentence) || NOT_DURABLE.test(sentence)) continue;
|
|
2483
|
+
if (isRule(sentence)) {
|
|
2585
2484
|
if (!rules.has(key)) rules.set(key, sentence);
|
|
2485
|
+
} else if (POLICY.test(sentence) && !policies.has(key)) {
|
|
2486
|
+
policies.set(key, sentence);
|
|
2586
2487
|
}
|
|
2587
2488
|
}
|
|
2588
2489
|
}
|
|
2589
|
-
return {
|
|
2490
|
+
return {
|
|
2491
|
+
rules: [...rules.values()],
|
|
2492
|
+
revocations: [...revocations.values()],
|
|
2493
|
+
policies: [...policies.values()]
|
|
2494
|
+
};
|
|
2590
2495
|
}
|
|
2591
2496
|
function isRule(sentence) {
|
|
2592
2497
|
const rest = sentence.replace(RULE_PREFIX, "");
|
|
@@ -2608,6 +2513,7 @@ function revokes(revocation, rule) {
|
|
|
2608
2513
|
// src/memory-extract/engine.ts
|
|
2609
2514
|
var AUTO_SAVE_THRESHOLD = 0.7;
|
|
2610
2515
|
var STATED_CONFIDENCE = 0.9;
|
|
2516
|
+
var POLICY_CONFIDENCE = 0.6;
|
|
2611
2517
|
async function captureAfterTurn(input, options = {}) {
|
|
2612
2518
|
const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
|
|
2613
2519
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -2617,7 +2523,8 @@ async function captureAfterTurn(input, options = {}) {
|
|
|
2617
2523
|
const source = extracted.source;
|
|
2618
2524
|
const candidates = [
|
|
2619
2525
|
...extracted.candidates,
|
|
2620
|
-
...(input.userRules ?? []).map(ruleCandidate),
|
|
2526
|
+
...(input.userRules ?? []).map((rule) => ruleCandidate(rule)),
|
|
2527
|
+
...(input.userPolicies ?? []).map((policy) => ruleCandidate(policy, POLICY_CONFIDENCE)),
|
|
2621
2528
|
...await procedureCandidates(input, repositoryRoot)
|
|
2622
2529
|
];
|
|
2623
2530
|
const saved = [];
|
|
@@ -2692,14 +2599,14 @@ async function recordWorklog(input, saved, queued, repositoryRoot, now, reasons)
|
|
|
2692
2599
|
return null;
|
|
2693
2600
|
}
|
|
2694
2601
|
}
|
|
2695
|
-
function ruleCandidate(rule) {
|
|
2602
|
+
function ruleCandidate(rule, confidence = STATED_CONFIDENCE) {
|
|
2696
2603
|
return {
|
|
2697
2604
|
class: "decision",
|
|
2698
2605
|
content: rule,
|
|
2699
2606
|
subject: "rule",
|
|
2700
2607
|
predicate: "rule",
|
|
2701
2608
|
object: rule,
|
|
2702
|
-
confidence
|
|
2609
|
+
confidence,
|
|
2703
2610
|
paths: [],
|
|
2704
2611
|
severity: null
|
|
2705
2612
|
};
|
|
@@ -2972,7 +2879,7 @@ function candidateFingerprint(namespace, raw) {
|
|
|
2972
2879
|
content: candidate.content.trim().replace(/\s+/g, " ").toLowerCase(),
|
|
2973
2880
|
paths: [...new Set(candidate.paths.map((path) => path.replace(/\\/g, "/")))].sort()
|
|
2974
2881
|
});
|
|
2975
|
-
return
|
|
2882
|
+
return createHash2("sha256").update(canonical).digest("hex");
|
|
2976
2883
|
}
|
|
2977
2884
|
async function approveReviewItem(id, options = {}) {
|
|
2978
2885
|
const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
|
|
@@ -3021,50 +2928,6 @@ async function approveReviewItem(id, options = {}) {
|
|
|
3021
2928
|
return factId;
|
|
3022
2929
|
}
|
|
3023
2930
|
|
|
3024
|
-
// src/tracing/log.ts
|
|
3025
|
-
import { appendFile, mkdir as mkdir4, readFile as readFile6, stat as stat3, writeFile } from "fs/promises";
|
|
3026
|
-
import { dirname as dirname4, join as join6 } from "path";
|
|
3027
|
-
var MAX_BYTES = 4 * 1024 * 1024;
|
|
3028
|
-
function tracePath(repositoryRoot) {
|
|
3029
|
-
return join6(repositoryRoot, ".codebuddy", "cache", "traces.jsonl");
|
|
3030
|
-
}
|
|
3031
|
-
async function recordTrace(repositoryRoot, event) {
|
|
3032
|
-
if (process.env.CODEBUDDY_TRACE === "0") return;
|
|
3033
|
-
const path = tracePath(repositoryRoot);
|
|
3034
|
-
try {
|
|
3035
|
-
await mkdir4(dirname4(path), { recursive: true });
|
|
3036
|
-
await appendFile(path, `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event })}
|
|
3037
|
-
`);
|
|
3038
|
-
if ((await stat3(path)).size > MAX_BYTES) {
|
|
3039
|
-
const lines = (await readFile6(path, "utf8")).split("\n").filter(Boolean);
|
|
3040
|
-
await writeFile(path, `${lines.slice(Math.floor(lines.length / 2)).join("\n")}
|
|
3041
|
-
`);
|
|
3042
|
-
}
|
|
3043
|
-
} catch {
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
async function readTraces(repositoryRoot, limit = 2e3) {
|
|
3047
|
-
let raw;
|
|
3048
|
-
try {
|
|
3049
|
-
raw = await readFile6(tracePath(repositoryRoot), "utf8");
|
|
3050
|
-
} catch {
|
|
3051
|
-
return [];
|
|
3052
|
-
}
|
|
3053
|
-
return parseTraceLines(raw).slice(-limit);
|
|
3054
|
-
}
|
|
3055
|
-
function parseTraceLines(raw) {
|
|
3056
|
-
const events = [];
|
|
3057
|
-
for (const line of raw.split("\n")) {
|
|
3058
|
-
if (!line.trim()) continue;
|
|
3059
|
-
try {
|
|
3060
|
-
const event = JSON.parse(line);
|
|
3061
|
-
if (typeof event.ts === "string" && typeof event.name === "string") events.push(event);
|
|
3062
|
-
} catch {
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
return events;
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
2931
|
// src/hooks/semantic.ts
|
|
3069
2932
|
import pTimeout from "p-timeout";
|
|
3070
2933
|
var CONNECT_BUDGET_MS = 2500;
|
|
@@ -3080,7 +2943,7 @@ async function configuredReranker(repositoryRoot) {
|
|
|
3080
2943
|
if (provider === "local") {
|
|
3081
2944
|
const revision = model === DEFAULT_RERANK_MODELS.local.id ? DEFAULT_RERANK_MODELS.local.revision : void 0;
|
|
3082
2945
|
return async (task, documents) => {
|
|
3083
|
-
const { rerankLocally } = await import("./local-
|
|
2946
|
+
const { rerankLocally } = await import("./local-ST4WMRCG.js");
|
|
3084
2947
|
return bounded(
|
|
3085
2948
|
rerankLocally({ id: model, ...revision ? { revision } : {} }, task, documents)
|
|
3086
2949
|
);
|
|
@@ -3088,7 +2951,7 @@ async function configuredReranker(repositoryRoot) {
|
|
|
3088
2951
|
}
|
|
3089
2952
|
const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file.provider?.apiKey;
|
|
3090
2953
|
if (!apiKey) return void 0;
|
|
3091
|
-
const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-
|
|
2954
|
+
const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-LXZUR3U2.js");
|
|
3092
2955
|
const call = provider === "openrouter" ? rerankOpenRouter : rerankHuggingFace;
|
|
3093
2956
|
return (task, documents) => call({ apiKey, model, timeoutMs: RERANK_BUDGET_MS }, task, documents);
|
|
3094
2957
|
}
|
|
@@ -3097,8 +2960,8 @@ async function openHookSemantics(repositoryRoot) {
|
|
|
3097
2960
|
const connect = async () => {
|
|
3098
2961
|
const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
|
|
3099
2962
|
if (!config.postgresUrl) return null;
|
|
3100
|
-
const { createRuntime
|
|
3101
|
-
const opening =
|
|
2963
|
+
const { createRuntime } = await import("./operations-7BTAAUHF.js");
|
|
2964
|
+
const opening = createRuntime(config, {
|
|
3102
2965
|
repositoryRoot,
|
|
3103
2966
|
startWorker: false,
|
|
3104
2967
|
connectTimeoutSeconds: 2
|
|
@@ -3132,19 +2995,20 @@ async function openHookSemantics(repositoryRoot) {
|
|
|
3132
2995
|
}
|
|
3133
2996
|
|
|
3134
2997
|
// src/hooks/staging.ts
|
|
3135
|
-
import { mkdir as
|
|
3136
|
-
import { join as
|
|
2998
|
+
import { mkdir as mkdir4, readFile as readFile6, rename as rename2, rm as rm3, stat as stat3 } from "fs/promises";
|
|
2999
|
+
import { join as join6, resolve as resolve8 } from "path";
|
|
3000
|
+
var STAGING_LOCK_STALE_MS = 1e4;
|
|
3137
3001
|
var HookStagingStore = class {
|
|
3138
3002
|
directory;
|
|
3139
3003
|
operations = Promise.resolve();
|
|
3140
3004
|
constructor(repositoryRoot = process.cwd()) {
|
|
3141
|
-
this.directory =
|
|
3005
|
+
this.directory = join6(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
|
|
3142
3006
|
}
|
|
3143
3007
|
async add(sessionId, paths) {
|
|
3144
3008
|
await this.serialize(async () => {
|
|
3145
3009
|
const clean = paths.map(normalize2).filter(Boolean);
|
|
3146
3010
|
if (clean.length === 0) return;
|
|
3147
|
-
await
|
|
3011
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
3148
3012
|
await this.withFileLock(sessionId, async () => {
|
|
3149
3013
|
const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean])].sort();
|
|
3150
3014
|
await writeAtomic(
|
|
@@ -3157,7 +3021,7 @@ var HookStagingStore = class {
|
|
|
3157
3021
|
}
|
|
3158
3022
|
async read(sessionId) {
|
|
3159
3023
|
try {
|
|
3160
|
-
const parsed = JSON.parse(await
|
|
3024
|
+
const parsed = JSON.parse(await readFile6(this.filePath(sessionId), "utf8"));
|
|
3161
3025
|
return Array.isArray(parsed?.paths) ? parsed.paths.filter((p) => typeof p === "string") : [];
|
|
3162
3026
|
} catch {
|
|
3163
3027
|
return [];
|
|
@@ -3166,7 +3030,7 @@ var HookStagingStore = class {
|
|
|
3166
3030
|
/** Return the staged paths and clear them. */
|
|
3167
3031
|
async drain(sessionId) {
|
|
3168
3032
|
return this.serialize(async () => {
|
|
3169
|
-
await
|
|
3033
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
3170
3034
|
return this.withFileLock(sessionId, async () => {
|
|
3171
3035
|
const path = this.filePath(sessionId);
|
|
3172
3036
|
const draining = `${path}.${process.pid}.${Date.now()}.drain`;
|
|
@@ -3177,7 +3041,7 @@ var HookStagingStore = class {
|
|
|
3177
3041
|
throw error;
|
|
3178
3042
|
}
|
|
3179
3043
|
try {
|
|
3180
|
-
const parsed = JSON.parse(await
|
|
3044
|
+
const parsed = JSON.parse(await readFile6(draining, "utf8"));
|
|
3181
3045
|
return Array.isArray(parsed?.paths) ? parsed.paths.filter((item) => typeof item === "string") : [];
|
|
3182
3046
|
} finally {
|
|
3183
3047
|
await rm3(draining, { force: true });
|
|
@@ -3187,15 +3051,15 @@ var HookStagingStore = class {
|
|
|
3187
3051
|
}
|
|
3188
3052
|
/** Claim a batch without consuming edits staged while capture runs. */
|
|
3189
3053
|
async claim(sessionId) {
|
|
3190
|
-
await
|
|
3054
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
3191
3055
|
const captureLock = `${this.filePath(sessionId)}.capture-lock`;
|
|
3192
3056
|
for (let attempt = 0; ; attempt++) {
|
|
3193
3057
|
try {
|
|
3194
|
-
await
|
|
3058
|
+
await mkdir4(captureLock);
|
|
3195
3059
|
break;
|
|
3196
3060
|
} catch (error) {
|
|
3197
3061
|
if (error.code !== "EEXIST") throw error;
|
|
3198
|
-
const age = Date.now() - (await
|
|
3062
|
+
const age = Date.now() - (await stat3(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
|
|
3199
3063
|
if (age > 6e4) {
|
|
3200
3064
|
await rm3(captureLock, { recursive: true, force: true });
|
|
3201
3065
|
continue;
|
|
@@ -3208,13 +3072,13 @@ var HookStagingStore = class {
|
|
|
3208
3072
|
return await this.withFileLock(sessionId, async () => {
|
|
3209
3073
|
const claimed = this.claimPath(sessionId);
|
|
3210
3074
|
try {
|
|
3211
|
-
return parsePaths(await
|
|
3075
|
+
return parsePaths(await readFile6(claimed, "utf8"));
|
|
3212
3076
|
} catch (error) {
|
|
3213
3077
|
if (error.code !== "ENOENT") throw error;
|
|
3214
3078
|
}
|
|
3215
3079
|
try {
|
|
3216
3080
|
await rename2(this.filePath(sessionId), claimed);
|
|
3217
|
-
return parsePaths(await
|
|
3081
|
+
return parsePaths(await readFile6(claimed, "utf8"));
|
|
3218
3082
|
} catch (error) {
|
|
3219
3083
|
if (error.code === "ENOENT") return [];
|
|
3220
3084
|
throw error;
|
|
@@ -3235,7 +3099,7 @@ var HookStagingStore = class {
|
|
|
3235
3099
|
/** Restore the claimed batch for retry, merging edits that arrived during capture. */
|
|
3236
3100
|
async rollback(sessionId) {
|
|
3237
3101
|
await this.withFileLock(sessionId, async () => {
|
|
3238
|
-
const claimed = await
|
|
3102
|
+
const claimed = await readFile6(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
|
|
3239
3103
|
const merged = [.../* @__PURE__ */ new Set([...claimed, ...await this.read(sessionId)])].sort();
|
|
3240
3104
|
if (merged.length > 0)
|
|
3241
3105
|
await writeAtomic(
|
|
@@ -3254,10 +3118,18 @@ var HookStagingStore = class {
|
|
|
3254
3118
|
const lock = `${this.filePath(sessionId)}.lock`;
|
|
3255
3119
|
for (let attempt = 0; ; attempt++) {
|
|
3256
3120
|
try {
|
|
3257
|
-
await
|
|
3121
|
+
await mkdir4(lock);
|
|
3258
3122
|
break;
|
|
3259
3123
|
} catch (error) {
|
|
3260
3124
|
if (error.code !== "EEXIST" || attempt >= 200) throw error;
|
|
3125
|
+
const age = await stat3(lock).then(
|
|
3126
|
+
(info) => Date.now() - info.mtimeMs,
|
|
3127
|
+
() => 0
|
|
3128
|
+
);
|
|
3129
|
+
if (age > STAGING_LOCK_STALE_MS) {
|
|
3130
|
+
await rm3(lock, { recursive: true, force: true });
|
|
3131
|
+
continue;
|
|
3132
|
+
}
|
|
3261
3133
|
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
|
|
3262
3134
|
}
|
|
3263
3135
|
}
|
|
@@ -3282,7 +3154,7 @@ var HookStagingStore = class {
|
|
|
3282
3154
|
}
|
|
3283
3155
|
filePath(sessionId) {
|
|
3284
3156
|
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "default";
|
|
3285
|
-
return
|
|
3157
|
+
return join6(this.directory, `${safe}.json`);
|
|
3286
3158
|
}
|
|
3287
3159
|
};
|
|
3288
3160
|
function normalize2(path) {
|
|
@@ -3295,6 +3167,7 @@ function parsePaths(raw) {
|
|
|
3295
3167
|
|
|
3296
3168
|
// src/hooks/runtime.ts
|
|
3297
3169
|
var CONTEXT_BLOCK_MAX_CHARS = 1800;
|
|
3170
|
+
var HOOK_TASK_FILES = 2;
|
|
3298
3171
|
var PROMPT_FACTS = 3;
|
|
3299
3172
|
var CONTEXT_HOOK_BUDGET_MS = 15e3;
|
|
3300
3173
|
async function resolveNamespace(repositoryRoot) {
|
|
@@ -3304,7 +3177,7 @@ async function resolveNamespace(repositoryRoot) {
|
|
|
3304
3177
|
}
|
|
3305
3178
|
async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
|
|
3306
3179
|
const started = Date.now();
|
|
3307
|
-
const repositoryRoot =
|
|
3180
|
+
const repositoryRoot = hookRoot(input.cwd);
|
|
3308
3181
|
const namespace = await resolveNamespace(repositoryRoot);
|
|
3309
3182
|
const bootstrap = await buildBootstrapContext({ repositoryRoot, namespace });
|
|
3310
3183
|
let beforeEdit = null;
|
|
@@ -3313,7 +3186,8 @@ async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
|
|
|
3313
3186
|
beforeEdit = await buildBeforeEditContext({
|
|
3314
3187
|
repositoryRoot,
|
|
3315
3188
|
namespace,
|
|
3316
|
-
|
|
3189
|
+
// Git changes are the targets only without a prompt; a prompt that
|
|
3190
|
+
// matches no file gets none (an explicit useGit would force them back).
|
|
3317
3191
|
...input.prompt ? { task: input.prompt } : {},
|
|
3318
3192
|
...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
|
|
3319
3193
|
...semantics?.reranker ? { reranker: semantics.reranker } : {}
|
|
@@ -3358,25 +3232,29 @@ function recallTrace(input, beforeEdit, emitted, text, started, late) {
|
|
|
3358
3232
|
tokensOut: estimateTokens(text),
|
|
3359
3233
|
...input.session_id ? { session: input.session_id } : {},
|
|
3360
3234
|
detail: {
|
|
3361
|
-
prompt: prompt
|
|
3235
|
+
prompt: clip2(prompt, 120),
|
|
3362
3236
|
facts: facts.map((fact) => fact.summary.slice(0, 100)),
|
|
3363
3237
|
dense: facts.filter((fact) => fact.reason.includes("semantic")).length,
|
|
3364
3238
|
lexical: facts.filter((fact) => /task term|target path/.test(fact.reason)).length,
|
|
3365
3239
|
files: (beforeEdit?.targetPaths ?? []).slice(0, 3),
|
|
3366
3240
|
sections: emitted.length,
|
|
3241
|
+
...text.trim() ? { block: clip2(redactSensitive(text.trim()), 2e3) } : {},
|
|
3367
3242
|
...late ? { dropped: "built after the hook budget" } : {}
|
|
3368
3243
|
}
|
|
3369
3244
|
};
|
|
3370
3245
|
}
|
|
3246
|
+
function clip2(text, max) {
|
|
3247
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
3248
|
+
}
|
|
3371
3249
|
async function runStageHook(input) {
|
|
3372
|
-
const repositoryRoot =
|
|
3250
|
+
const repositoryRoot = hookRoot(input.cwd);
|
|
3373
3251
|
const paths = extractEditedPaths(input.tool_input).map((path) => toRepoRelative(repositoryRoot, path)).filter((path) => path !== null);
|
|
3374
3252
|
await new HookStagingStore(repositoryRoot).add(input.session_id ?? "default", paths);
|
|
3375
3253
|
return paths;
|
|
3376
3254
|
}
|
|
3377
3255
|
async function llmMemoryOptions(repositoryRoot) {
|
|
3378
3256
|
try {
|
|
3379
|
-
return await (await import("./memory-
|
|
3257
|
+
return await (await import("./memory-TGHVTBME.js")).llmCaptureOptions(repositoryRoot);
|
|
3380
3258
|
} catch {
|
|
3381
3259
|
return {};
|
|
3382
3260
|
}
|
|
@@ -3385,7 +3263,7 @@ var INVESTIGATION_MIN_CHARS = 200;
|
|
|
3385
3263
|
var NAMES_CODE = /[\w.-]+\/[\w./-]*\.\w{1,5}\b|\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|py|go|rs|java|rb|php|cs|c|cpp|h|swift|kt)\b|`[^`\s]+`/;
|
|
3386
3264
|
async function runCaptureHook(input) {
|
|
3387
3265
|
const captureStarted = Date.now();
|
|
3388
|
-
const repositoryRoot =
|
|
3266
|
+
const repositoryRoot = hookRoot(input.cwd);
|
|
3389
3267
|
const staging = new HookStagingStore(repositoryRoot);
|
|
3390
3268
|
const sessionId = input.session_id ?? "default";
|
|
3391
3269
|
const claimed = await staging.claim(sessionId);
|
|
@@ -3393,12 +3271,12 @@ async function runCaptureHook(input) {
|
|
|
3393
3271
|
const changedFiles = claimed;
|
|
3394
3272
|
try {
|
|
3395
3273
|
const prompts = input.transcript_path ? await turnUserPrompts(input.transcript_path) : [];
|
|
3396
|
-
const { rules, revocations } = extractUserRules(prompts);
|
|
3274
|
+
const { rules, revocations, policies } = extractUserRules(prompts);
|
|
3397
3275
|
let investigation = false;
|
|
3398
3276
|
if (changedFiles.length === 0) {
|
|
3399
3277
|
const assistant = input.transcript_path ? await lastAssistantMessage(input.transcript_path) : input.last_assistant_message?.trim() ?? "";
|
|
3400
3278
|
investigation = assistant.length >= INVESTIGATION_MIN_CHARS && NAMES_CODE.test(assistant);
|
|
3401
|
-
if (!investigation && rules.length === 0 && revocations.length === 0) {
|
|
3279
|
+
if (!investigation && rules.length === 0 && revocations.length === 0 && policies.length === 0) {
|
|
3402
3280
|
await staging.acknowledge(sessionId);
|
|
3403
3281
|
return { changedFiles, result: null };
|
|
3404
3282
|
}
|
|
@@ -3423,6 +3301,7 @@ async function runCaptureHook(input) {
|
|
|
3423
3301
|
sessionId,
|
|
3424
3302
|
...verification.length > 0 ? { verification } : {},
|
|
3425
3303
|
...rules.length > 0 ? { userRules: rules } : {},
|
|
3304
|
+
...policies.length > 0 ? { userPolicies: policies } : {},
|
|
3426
3305
|
...revocations.length > 0 ? { revokedRules: revocations } : {},
|
|
3427
3306
|
...investigation ? { investigation } : {},
|
|
3428
3307
|
// Without edits, only the user's own rules and a handoff are kept: an
|
|
@@ -3455,40 +3334,107 @@ async function runCaptureHook(input) {
|
|
|
3455
3334
|
}
|
|
3456
3335
|
}
|
|
3457
3336
|
var SYNC_BUDGET_MS = 5 * 6e4;
|
|
3458
|
-
async function runSyncHook(repositoryRoot) {
|
|
3337
|
+
async function runSyncHook(repositoryRoot, options = {}) {
|
|
3459
3338
|
const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
|
|
3460
3339
|
if (!config.postgresUrl) return null;
|
|
3461
|
-
const
|
|
3462
|
-
const
|
|
3463
|
-
|
|
3340
|
+
const lock = join7(repositoryRoot, ".codebuddy", "cache", "sync.lock");
|
|
3341
|
+
const dirty = join7(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
|
|
3342
|
+
await mkdir5(dirname5(lock), { recursive: true });
|
|
3343
|
+
if (!await takeLock(lock)) {
|
|
3344
|
+
await writeFile(dirty, "").catch(() => void 0);
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
const { createRuntime } = await import("./operations-7BTAAUHF.js");
|
|
3348
|
+
const runtime = await createRuntime(config, { repositoryRoot }).catch(async (error) => {
|
|
3349
|
+
await rm4(lock, { recursive: true, force: true });
|
|
3350
|
+
throw error;
|
|
3351
|
+
});
|
|
3464
3352
|
try {
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3353
|
+
let result;
|
|
3354
|
+
let round = 0;
|
|
3355
|
+
do {
|
|
3356
|
+
await rm4(dirty, { force: true });
|
|
3357
|
+
const started = Date.now();
|
|
3358
|
+
const reindex = await runtime.memory.reindexMemory(options.respace ? {} : { respace: false });
|
|
3359
|
+
const drained = await pTimeout2(runtime.memory.getWorker().drain(), {
|
|
3360
|
+
milliseconds: SYNC_BUDGET_MS
|
|
3361
|
+
}).then(
|
|
3362
|
+
() => true,
|
|
3363
|
+
() => false
|
|
3364
|
+
);
|
|
3365
|
+
await recordTrace(repositoryRoot, {
|
|
3366
|
+
kind: "sync",
|
|
3367
|
+
name: "sync",
|
|
3368
|
+
ms: Date.now() - started,
|
|
3369
|
+
ok: drained,
|
|
3370
|
+
detail: {
|
|
3371
|
+
imported: reindex.facts.imported,
|
|
3372
|
+
updated: reindex.facts.updated,
|
|
3373
|
+
reembedding: reindex.reembedding ?? 0,
|
|
3374
|
+
failures: reindex.failures.length
|
|
3375
|
+
}
|
|
3376
|
+
});
|
|
3377
|
+
result = { reindex, drained };
|
|
3378
|
+
round += 1;
|
|
3379
|
+
} while (round < 3 && await stat4(dirty).then(
|
|
3469
3380
|
() => true,
|
|
3470
3381
|
() => false
|
|
3471
|
-
);
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3382
|
+
));
|
|
3383
|
+
if (config.provider.type === "local") {
|
|
3384
|
+
const started = Date.now();
|
|
3385
|
+
const vectors = await syncFileVectors(repositoryRoot, SYNC_BUDGET_MS);
|
|
3386
|
+
if (vectors) {
|
|
3387
|
+
await recordTrace(repositoryRoot, {
|
|
3388
|
+
kind: "sync",
|
|
3389
|
+
name: "file-vectors",
|
|
3390
|
+
ms: Date.now() - started,
|
|
3391
|
+
ok: true,
|
|
3392
|
+
detail: { ...vectors }
|
|
3393
|
+
});
|
|
3482
3394
|
}
|
|
3483
|
-
}
|
|
3484
|
-
return
|
|
3395
|
+
}
|
|
3396
|
+
return result;
|
|
3485
3397
|
} finally {
|
|
3486
3398
|
await runtime.close();
|
|
3399
|
+
await rm4(lock, { recursive: true, force: true });
|
|
3487
3400
|
}
|
|
3488
3401
|
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3402
|
+
var SYNC_LOCK_STALE_MS = SYNC_BUDGET_MS + 6e4;
|
|
3403
|
+
async function takeLock(path) {
|
|
3404
|
+
try {
|
|
3405
|
+
await mkdir5(path);
|
|
3406
|
+
return true;
|
|
3407
|
+
} catch {
|
|
3408
|
+
const age = await stat4(path).then(
|
|
3409
|
+
(info) => Date.now() - info.mtimeMs,
|
|
3410
|
+
() => 0
|
|
3411
|
+
);
|
|
3412
|
+
if (age < SYNC_LOCK_STALE_MS) return false;
|
|
3413
|
+
await rm4(path, { recursive: true, force: true });
|
|
3414
|
+
return mkdir5(path).then(
|
|
3415
|
+
() => true,
|
|
3416
|
+
() => false
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
async function runResetHook(input, emit) {
|
|
3421
|
+
const repositoryRoot = hookRoot(input.cwd);
|
|
3422
|
+
const sessionId = input.session_id ?? "default";
|
|
3423
|
+
const ledger = new SessionCapsuleLedger(repositoryRoot);
|
|
3424
|
+
await ledger.reset(sessionId);
|
|
3425
|
+
if (input.hook_event_name !== "SessionStart") return "";
|
|
3426
|
+
const namespace = await resolveNamespace(repositoryRoot);
|
|
3427
|
+
const bootstrap = await buildBootstrapContext({ repositoryRoot, namespace });
|
|
3428
|
+
const { text, emitted } = await renderContextBlock(
|
|
3429
|
+
bootstrap,
|
|
3430
|
+
null,
|
|
3431
|
+
ledger,
|
|
3432
|
+
sessionId,
|
|
3433
|
+
input.session_id !== void 0
|
|
3434
|
+
);
|
|
3435
|
+
if (text.trim()) await emit?.(text);
|
|
3436
|
+
await ledger.markSent(sessionId, emitted);
|
|
3437
|
+
return text;
|
|
3492
3438
|
}
|
|
3493
3439
|
function extractEditedPaths(toolInput) {
|
|
3494
3440
|
if (!toolInput) return [];
|
|
@@ -3532,7 +3478,7 @@ function isVerificationCommand(command) {
|
|
|
3532
3478
|
async function turnVerification(transcriptPath) {
|
|
3533
3479
|
let raw;
|
|
3534
3480
|
try {
|
|
3535
|
-
raw = await
|
|
3481
|
+
raw = await readFile7(transcriptPath, "utf8");
|
|
3536
3482
|
} catch {
|
|
3537
3483
|
return [];
|
|
3538
3484
|
}
|
|
@@ -3584,7 +3530,7 @@ var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
|
|
|
3584
3530
|
async function turnUserPrompts(transcriptPath) {
|
|
3585
3531
|
let raw;
|
|
3586
3532
|
try {
|
|
3587
|
-
raw = await
|
|
3533
|
+
raw = await readFile7(transcriptPath, "utf8");
|
|
3588
3534
|
} catch {
|
|
3589
3535
|
return [];
|
|
3590
3536
|
}
|
|
@@ -3615,7 +3561,7 @@ async function turnUserPrompts(transcriptPath) {
|
|
|
3615
3561
|
}
|
|
3616
3562
|
async function lastAssistantMessage(transcriptPath) {
|
|
3617
3563
|
try {
|
|
3618
|
-
const raw = await
|
|
3564
|
+
const raw = await readFile7(transcriptPath, "utf8");
|
|
3619
3565
|
let latest = "";
|
|
3620
3566
|
for (const line of raw.split("\n")) {
|
|
3621
3567
|
const trimmed = line.trim();
|
|
@@ -3759,7 +3705,7 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
|
|
|
3759
3705
|
items.push({
|
|
3760
3706
|
priority: 3,
|
|
3761
3707
|
text: bound(
|
|
3762
|
-
`${targetLabel}: ${beforeEdit.targetPaths.slice(0, 8).join(", ")}` + (tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""),
|
|
3708
|
+
`${targetLabel}: ${beforeEdit.targetPaths.slice(0, beforeEdit.pathSource === "task" ? HOOK_TASK_FILES : 8).join(", ")}` + (tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""),
|
|
3763
3709
|
500
|
|
3764
3710
|
)
|
|
3765
3711
|
});
|
|
@@ -3802,1286 +3748,12 @@ function describeKeys(keys) {
|
|
|
3802
3748
|
}
|
|
3803
3749
|
var kinds = /* @__PURE__ */ new Set(["fact", "incident", "handoff", "rule", "procedure"]);
|
|
3804
3750
|
|
|
3805
|
-
// src/suggest/engine.ts
|
|
3806
|
-
import { constants } from "fs";
|
|
3807
|
-
import { access } from "fs/promises";
|
|
3808
|
-
import { extname, join as join8, resolve as resolve10 } from "path";
|
|
3809
|
-
|
|
3810
|
-
// src/suggest/types.ts
|
|
3811
|
-
var SEVERITY_RANK = {
|
|
3812
|
-
high: 3,
|
|
3813
|
-
medium: 2,
|
|
3814
|
-
low: 1,
|
|
3815
|
-
info: 0
|
|
3816
|
-
};
|
|
3817
|
-
|
|
3818
|
-
// src/suggest/engine.ts
|
|
3819
|
-
var RISK_SUGGEST_THRESHOLD = 50;
|
|
3820
|
-
var BLAST_RADIUS_MEDIUM = 5;
|
|
3821
|
-
var BLAST_RADIUS_HIGH = 10;
|
|
3822
|
-
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
3823
|
-
async function generateSuggestions(input) {
|
|
3824
|
-
const repositoryRoot = resolve10(input.repositoryRoot ?? process.cwd());
|
|
3825
|
-
const namespace = input.namespace;
|
|
3826
|
-
const targetPaths = await resolveRiskPaths({
|
|
3827
|
-
repositoryRoot,
|
|
3828
|
-
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
3829
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
3830
|
-
...input.useGit !== void 0 ? { useGit: input.useGit } : {}
|
|
3831
|
-
});
|
|
3832
|
-
const planStore = new PlanFileStore(repositoryRoot);
|
|
3833
|
-
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
3834
|
-
const [risks, plan, incidents, map, policyFile] = await Promise.all([
|
|
3835
|
-
assessRisk({
|
|
3836
|
-
repositoryRoot,
|
|
3837
|
-
namespace,
|
|
3838
|
-
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
3839
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
3840
|
-
...input.useGit !== void 0 ? { useGit: input.useGit } : {}
|
|
3841
|
-
}),
|
|
3842
|
-
resolveRelevantPlan2(planStore, namespace, input.planId),
|
|
3843
|
-
memoryStore.findIncidentFactsForPaths({ namespace, paths: targetPaths }),
|
|
3844
|
-
buildArchitectureMap(repositoryRoot),
|
|
3845
|
-
readPolicyFile(join8(repositoryRoot, ".codebuddy", "policies.yaml"))
|
|
3846
|
-
]);
|
|
3847
|
-
const suggestions = [];
|
|
3848
|
-
for (const item of risks.items) {
|
|
3849
|
-
if (item.score < RISK_SUGGEST_THRESHOLD) continue;
|
|
3850
|
-
suggestions.push({
|
|
3851
|
-
id: `risk:${item.path}`,
|
|
3852
|
-
category: "risk",
|
|
3853
|
-
severity: item.score >= 75 ? "high" : "medium",
|
|
3854
|
-
title: `Review high-risk change to ${item.path}`,
|
|
3855
|
-
detail: item.reason,
|
|
3856
|
-
path: item.path,
|
|
3857
|
-
evidence: [
|
|
3858
|
-
{ kind: "risk_score", detail: `score ${item.score} (${item.category})` },
|
|
3859
|
-
...item.evidence.length > 0 ? [{ kind: "risk_evidence", detail: `${item.evidence.length} evidence item(s)` }] : []
|
|
3860
|
-
],
|
|
3861
|
-
commandHint: `codebuddy risk assess --paths ${item.path} --explain`
|
|
3862
|
-
});
|
|
3863
|
-
}
|
|
3864
|
-
for (const incident of incidents) {
|
|
3865
|
-
const path = incident.paths[0] ?? "(unknown)";
|
|
3866
|
-
suggestions.push({
|
|
3867
|
-
id: `incident:${path}`,
|
|
3868
|
-
category: "incident",
|
|
3869
|
-
severity: "high",
|
|
3870
|
-
title: `Recurring incident hotspot: ${path}`,
|
|
3871
|
-
detail: `${incident.subject} \u2014 proceed carefully; this area has an unresolved incident.`,
|
|
3872
|
-
path,
|
|
3873
|
-
evidence: [{ kind: "incident", detail: `${incident.id} severity=${incident.severity}` }]
|
|
3874
|
-
});
|
|
3875
|
-
}
|
|
3876
|
-
const inbound = inboundDegrees(map);
|
|
3877
|
-
for (const path of targetPaths) {
|
|
3878
|
-
const dependents = inbound.get(path) ?? 0;
|
|
3879
|
-
if (dependents < BLAST_RADIUS_MEDIUM) continue;
|
|
3880
|
-
suggestions.push({
|
|
3881
|
-
id: `architecture:${path}`,
|
|
3882
|
-
category: "architecture",
|
|
3883
|
-
severity: dependents >= BLAST_RADIUS_HIGH ? "high" : "medium",
|
|
3884
|
-
title: `High blast radius: ${path}`,
|
|
3885
|
-
detail: `${dependents} module(s) import ${path}; a change here ripples widely.`,
|
|
3886
|
-
path,
|
|
3887
|
-
evidence: [{ kind: "dependents", detail: `${dependents} inbound import(s)` }],
|
|
3888
|
-
commandHint: `codebuddy map dependents ${path}`
|
|
3889
|
-
});
|
|
3890
|
-
}
|
|
3891
|
-
if (plan) {
|
|
3892
|
-
const planned = new Set(plan.filesToTouch.map((file) => normalize3(file.path)));
|
|
3893
|
-
const strays = targetPaths.filter((path) => !planned.has(normalize3(path)));
|
|
3894
|
-
if (planned.size > 0 && strays.length > 0) {
|
|
3895
|
-
suggestions.push({
|
|
3896
|
-
id: `plan_divergence:${plan.id}`,
|
|
3897
|
-
category: "plan_divergence",
|
|
3898
|
-
severity: "medium",
|
|
3899
|
-
title: `Change set diverges from plan ${plan.id}`,
|
|
3900
|
-
detail: `${strays.length} target file(s) are outside the active plan's scope: ${strays.slice(0, 5).join(", ")}.`,
|
|
3901
|
-
evidence: [
|
|
3902
|
-
{ kind: "plan", detail: `plan ${plan.id} (${plan.status})` },
|
|
3903
|
-
{ kind: "out_of_scope", detail: strays.slice(0, 5).join(", ") }
|
|
3904
|
-
],
|
|
3905
|
-
commandHint: `codebuddy plan show ${plan.id}`
|
|
3906
|
-
});
|
|
3907
|
-
}
|
|
3908
|
-
}
|
|
3909
|
-
for (const path of targetPaths) {
|
|
3910
|
-
if (!isTestableCode(path)) continue;
|
|
3911
|
-
if (await hasSiblingTest(repositoryRoot, path)) continue;
|
|
3912
|
-
suggestions.push({
|
|
3913
|
-
id: `missing_test:${path}`,
|
|
3914
|
-
category: "missing_test",
|
|
3915
|
-
severity: "low",
|
|
3916
|
-
title: `No test found for ${path}`,
|
|
3917
|
-
detail: "Consider adding a test alongside this change.",
|
|
3918
|
-
path,
|
|
3919
|
-
evidence: [{ kind: "no_test", detail: "no *.test.* / *.spec.* sibling found" }]
|
|
3920
|
-
});
|
|
3921
|
-
}
|
|
3922
|
-
for (const rule of policyFile.rules) {
|
|
3923
|
-
const prefix = literalPrefix(rule.pattern);
|
|
3924
|
-
if (!prefix) continue;
|
|
3925
|
-
if (await pathExists(join8(repositoryRoot, prefix))) continue;
|
|
3926
|
-
suggestions.push({
|
|
3927
|
-
id: `stale_policy:${rule.id}`,
|
|
3928
|
-
category: "stale_policy",
|
|
3929
|
-
severity: "low",
|
|
3930
|
-
title: `Stale policy rule "${rule.id}"`,
|
|
3931
|
-
detail: `Pattern "${rule.pattern}" targets "${prefix}", which does not exist.`,
|
|
3932
|
-
evidence: [{ kind: "policy_rule", detail: `${rule.id} \u2192 ${rule.pattern}` }]
|
|
3933
|
-
});
|
|
3934
|
-
}
|
|
3935
|
-
const deduped = dedupe(suggestions).sort(compareSuggestions);
|
|
3936
|
-
const limited = input.limit ? deduped.slice(0, input.limit) : deduped;
|
|
3937
|
-
return {
|
|
3938
|
-
suggestions: limited,
|
|
3939
|
-
stats: buildStats(targetPaths.length, limited)
|
|
3940
|
-
};
|
|
3941
|
-
}
|
|
3942
|
-
async function resolveRelevantPlan2(store, namespace, planId) {
|
|
3943
|
-
if (planId) {
|
|
3944
|
-
try {
|
|
3945
|
-
return await store.readPlan(planId);
|
|
3946
|
-
} catch (error) {
|
|
3947
|
-
if (error.code === "ENOENT") return null;
|
|
3948
|
-
throw error;
|
|
3949
|
-
}
|
|
3950
|
-
}
|
|
3951
|
-
return new PlanLifecycle(store).current(namespace);
|
|
3952
|
-
}
|
|
3953
|
-
function inboundDegrees(map) {
|
|
3954
|
-
const inbound = /* @__PURE__ */ new Map();
|
|
3955
|
-
for (const edge of map.edges) inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);
|
|
3956
|
-
return inbound;
|
|
3957
|
-
}
|
|
3958
|
-
function isTestableCode(path) {
|
|
3959
|
-
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(path)) return false;
|
|
3960
|
-
if (path.endsWith(".d.ts")) return false;
|
|
3961
|
-
if (path.includes("__tests__/")) return false;
|
|
3962
|
-
return CODE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
3963
|
-
}
|
|
3964
|
-
async function hasSiblingTest(repositoryRoot, path) {
|
|
3965
|
-
const ext = extname(path);
|
|
3966
|
-
const base = path.slice(0, path.length - ext.length);
|
|
3967
|
-
const dir = base.includes("/") ? base.slice(0, base.lastIndexOf("/")) : "";
|
|
3968
|
-
const name = base.includes("/") ? base.slice(base.lastIndexOf("/") + 1) : base;
|
|
3969
|
-
const candidates = [
|
|
3970
|
-
`${base}.test${ext}`,
|
|
3971
|
-
`${base}.spec${ext}`,
|
|
3972
|
-
`${dir ? `${dir}/` : ""}__tests__/${name}${ext}`,
|
|
3973
|
-
`${dir ? `${dir}/` : ""}__tests__/${name}.test${ext}`
|
|
3974
|
-
];
|
|
3975
|
-
for (const candidate of candidates) {
|
|
3976
|
-
if (await pathExists(join8(repositoryRoot, candidate))) return true;
|
|
3977
|
-
}
|
|
3978
|
-
return false;
|
|
3979
|
-
}
|
|
3980
|
-
function literalPrefix(pattern) {
|
|
3981
|
-
const normalized = normalize3(pattern);
|
|
3982
|
-
const wildcard = normalized.search(/[*?[]/);
|
|
3983
|
-
if (wildcard === -1) return normalized.replace(/\/+$/, "");
|
|
3984
|
-
const beforeWildcard = normalized.slice(0, wildcard);
|
|
3985
|
-
const lastSlash = beforeWildcard.lastIndexOf("/");
|
|
3986
|
-
return lastSlash === -1 ? "" : beforeWildcard.slice(0, lastSlash);
|
|
3987
|
-
}
|
|
3988
|
-
function dedupe(suggestions) {
|
|
3989
|
-
const byId = /* @__PURE__ */ new Map();
|
|
3990
|
-
for (const suggestion of suggestions) {
|
|
3991
|
-
const existing = byId.get(suggestion.id);
|
|
3992
|
-
if (!existing || SEVERITY_RANK[suggestion.severity] > SEVERITY_RANK[existing.severity]) {
|
|
3993
|
-
byId.set(suggestion.id, suggestion);
|
|
3994
|
-
}
|
|
3995
|
-
}
|
|
3996
|
-
return [...byId.values()];
|
|
3997
|
-
}
|
|
3998
|
-
function compareSuggestions(left, right) {
|
|
3999
|
-
const severity = SEVERITY_RANK[right.severity] - SEVERITY_RANK[left.severity];
|
|
4000
|
-
if (severity !== 0) return severity;
|
|
4001
|
-
if (left.category !== right.category) return left.category.localeCompare(right.category);
|
|
4002
|
-
return (left.path ?? "").localeCompare(right.path ?? "");
|
|
4003
|
-
}
|
|
4004
|
-
function buildStats(pathsConsidered, suggestions) {
|
|
4005
|
-
const byCategory = {};
|
|
4006
|
-
const bySeverity = {};
|
|
4007
|
-
for (const suggestion of suggestions) {
|
|
4008
|
-
byCategory[suggestion.category] = (byCategory[suggestion.category] ?? 0) + 1;
|
|
4009
|
-
bySeverity[suggestion.severity] = (bySeverity[suggestion.severity] ?? 0) + 1;
|
|
4010
|
-
}
|
|
4011
|
-
return { pathsConsidered, produced: suggestions.length, byCategory, bySeverity };
|
|
4012
|
-
}
|
|
4013
|
-
async function pathExists(path) {
|
|
4014
|
-
try {
|
|
4015
|
-
await access(path, constants.F_OK);
|
|
4016
|
-
return true;
|
|
4017
|
-
} catch {
|
|
4018
|
-
return false;
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
4021
|
-
function normalize3(path) {
|
|
4022
|
-
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
// src/mcp/tool-result.ts
|
|
4026
|
-
function serializeToolResult(value, format = process.env.CODEBUDDY_RESPONSE_FORMAT ?? "both") {
|
|
4027
|
-
if (format !== "text" && format !== "both") {
|
|
4028
|
-
throw new Error('CODEBUDDY_RESPONSE_FORMAT must be "text" or "both"');
|
|
4029
|
-
}
|
|
4030
|
-
return {
|
|
4031
|
-
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
4032
|
-
...format === "both" && value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}
|
|
4033
|
-
};
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
// src/mcp/tools/index.ts
|
|
4037
|
-
var INPUT_LIMITS = {
|
|
4038
|
-
contentChars: 32e3,
|
|
4039
|
-
batchItems: 100,
|
|
4040
|
-
summaryChars: 32e3,
|
|
4041
|
-
planBodyChars: 64e3,
|
|
4042
|
-
planBriefChars: 4e3,
|
|
4043
|
-
titleChars: 300,
|
|
4044
|
-
arrayItems: 100,
|
|
4045
|
-
pathChars: 1024,
|
|
4046
|
-
taskChars: 2e3,
|
|
4047
|
-
agentIdChars: 200
|
|
4048
|
-
};
|
|
4049
|
-
var deliverySchema = {
|
|
4050
|
-
sessionId: z6.string().min(1).max(200).optional(),
|
|
4051
|
-
ack: z6.string().regex(/^dlv_[a-z0-9]+$/).optional(),
|
|
4052
|
-
full: z6.boolean().optional()
|
|
4053
|
-
};
|
|
4054
|
-
var rememberSchema = {
|
|
4055
|
-
sessionId: z6.string().min(1).max(200).optional(),
|
|
4056
|
-
content: z6.string().min(1).max(INPUT_LIMITS.contentChars),
|
|
4057
|
-
type: z6.enum(["interaction", "fact", "summary"]).optional(),
|
|
4058
|
-
idempotencyKey: z6.string().min(1).max(200).optional(),
|
|
4059
|
-
agentId: z6.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
4060
|
-
};
|
|
4061
|
-
var recallSchema = {
|
|
4062
|
-
sessionId: z6.string().min(1).max(200),
|
|
4063
|
-
query: z6.string().min(1).max(INPUT_LIMITS.taskChars),
|
|
4064
|
-
budget: z6.number().int().positive().optional(),
|
|
4065
|
-
callerModel: z6.string().min(1).max(200).optional(),
|
|
4066
|
-
conflictMode: z6.enum(["all", "latest", "highest_confidence"]).optional()
|
|
4067
|
-
};
|
|
4068
|
-
var shareSchema = {
|
|
4069
|
-
from: z6.string().min(1).max(INPUT_LIMITS.agentIdChars).optional(),
|
|
4070
|
-
to_namespace: z6.string().min(1).max(INPUT_LIMITS.agentIdChars),
|
|
4071
|
-
factIds: z6.array(z6.string().min(1).max(100)).min(1).max(INPUT_LIMITS.arrayItems),
|
|
4072
|
-
mode: z6.enum(["reference", "snapshot"]).optional(),
|
|
4073
|
-
/** Snapshot mode: the target project's root; its Markdown store receives the copy. */
|
|
4074
|
-
to_project_root: z6.string().min(1).max(INPUT_LIMITS.pathChars).optional(),
|
|
4075
|
-
agentId: z6.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
4076
|
-
};
|
|
4077
|
-
var unshareSchema = {
|
|
4078
|
-
to_namespace: z6.string().min(1).max(INPUT_LIMITS.agentIdChars),
|
|
4079
|
-
factIds: z6.array(z6.string().min(1).max(100)).min(1).max(INPUT_LIMITS.arrayItems),
|
|
4080
|
-
agentId: z6.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
4081
|
-
};
|
|
4082
|
-
var plannedFileSchema2 = z6.object({
|
|
4083
|
-
path: z6.string().min(1).max(INPUT_LIMITS.pathChars),
|
|
4084
|
-
action: z6.enum(["create", "edit", "delete"]),
|
|
4085
|
-
notes: z6.string().max(INPUT_LIMITS.taskChars).optional()
|
|
4086
|
-
});
|
|
4087
|
-
var plannedTestSchema2 = z6.object({
|
|
4088
|
-
path: z6.string().min(1).max(INPUT_LIMITS.pathChars),
|
|
4089
|
-
description: z6.string().max(INPUT_LIMITS.taskChars).optional()
|
|
4090
|
-
});
|
|
4091
|
-
var planPatchSchema = {
|
|
4092
|
-
title: z6.string().min(1).max(INPUT_LIMITS.titleChars).optional(),
|
|
4093
|
-
brief: z6.string().max(INPUT_LIMITS.planBriefChars).optional(),
|
|
4094
|
-
body: z6.string().max(INPUT_LIMITS.planBodyChars).optional(),
|
|
4095
|
-
filesToTouch: z6.array(plannedFileSchema2).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4096
|
-
testsToAdd: z6.array(plannedTestSchema2).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4097
|
-
outOfScope: z6.array(z6.string().max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4098
|
-
risks: z6.array(z6.string().max(INPUT_LIMITS.taskChars)).max(INPUT_LIMITS.arrayItems).optional()
|
|
4099
|
-
};
|
|
4100
|
-
var mcpToolInputSchemas = {
|
|
4101
|
-
remember: z6.object(rememberSchema),
|
|
4102
|
-
remember_batch: z6.object({ items: z6.array(z6.object(rememberSchema)).min(1) }),
|
|
4103
|
-
recall: z6.object(recallSchema),
|
|
4104
|
-
list_facts: z6.object({
|
|
4105
|
-
subject: z6.string().optional(),
|
|
4106
|
-
limit: z6.number().int().positive().max(100).default(50),
|
|
4107
|
-
cursor: z6.string().optional()
|
|
4108
|
-
}),
|
|
4109
|
-
list_namespaces: z6.object({}),
|
|
4110
|
-
forget: z6.object({ id: z6.string().min(1) }),
|
|
4111
|
-
share: z6.object(shareSchema),
|
|
4112
|
-
unshare: z6.object(unshareSchema),
|
|
4113
|
-
plan_current: z6.object({}),
|
|
4114
|
-
plan_create: z6.object({
|
|
4115
|
-
title: z6.string().min(1).max(INPUT_LIMITS.titleChars),
|
|
4116
|
-
brief: z6.string().min(1).max(INPUT_LIMITS.planBriefChars),
|
|
4117
|
-
body: z6.string().max(INPUT_LIMITS.planBodyChars).optional(),
|
|
4118
|
-
filesToTouch: z6.array(plannedFileSchema2).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4119
|
-
testsToAdd: z6.array(plannedTestSchema2).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4120
|
-
outOfScope: z6.array(z6.string().max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4121
|
-
risks: z6.array(z6.string().max(INPUT_LIMITS.taskChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4122
|
-
agentId: z6.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
4123
|
-
}),
|
|
4124
|
-
plan_amend: z6.object({ id: z6.string().min(1), patch: z6.object(planPatchSchema) }),
|
|
4125
|
-
plan_status: z6.object({
|
|
4126
|
-
id: z6.string().min(1),
|
|
4127
|
-
status: z6.enum(["approved", "executing", "complete", "abandoned"]),
|
|
4128
|
-
commitSha: z6.string().optional(),
|
|
4129
|
-
notes: z6.string().optional(),
|
|
4130
|
-
reason: z6.string().optional()
|
|
4131
|
-
}),
|
|
4132
|
-
risk_assess: z6.object({
|
|
4133
|
-
planId: z6.string().min(1).max(100).optional(),
|
|
4134
|
-
paths: z6.array(z6.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4135
|
-
evidence: z6.enum(["overview", "source"]).optional(),
|
|
4136
|
-
useGit: z6.boolean().optional(),
|
|
4137
|
-
limit: z6.number().int().positive().max(100).optional()
|
|
4138
|
-
}),
|
|
4139
|
-
map_query: z6.object({
|
|
4140
|
-
from: z6.string().min(1).max(INPUT_LIMITS.pathChars).optional(),
|
|
4141
|
-
to: z6.string().min(1).max(INPUT_LIMITS.pathChars).optional(),
|
|
4142
|
-
limit: z6.number().int().positive().max(100).optional()
|
|
4143
|
-
}),
|
|
4144
|
-
map_neighbours: z6.object({
|
|
4145
|
-
module: z6.string().min(1).max(INPUT_LIMITS.pathChars),
|
|
4146
|
-
direction: z6.enum(["in", "out", "both"]).default("both")
|
|
4147
|
-
}),
|
|
4148
|
-
context_bootstrap: z6.object({ ...deliverySchema }),
|
|
4149
|
-
context_before_edit: z6.object({
|
|
4150
|
-
task: z6.string().min(1).max(INPUT_LIMITS.taskChars).optional(),
|
|
4151
|
-
paths: z6.array(z6.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4152
|
-
planId: z6.string().min(1).max(100).optional(),
|
|
4153
|
-
evidence: z6.enum(["overview", "source"]).optional(),
|
|
4154
|
-
useGit: z6.boolean().optional(),
|
|
4155
|
-
tokenBudget: z6.number().int().positive().max(1e5).optional(),
|
|
4156
|
-
...deliverySchema
|
|
4157
|
-
}),
|
|
4158
|
-
context_pack: z6.object({
|
|
4159
|
-
task: z6.string().min(1).max(INPUT_LIMITS.taskChars),
|
|
4160
|
-
paths: z6.array(z6.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4161
|
-
planId: z6.string().min(1).max(100).optional(),
|
|
4162
|
-
evidence: z6.enum(["overview", "source"]).optional(),
|
|
4163
|
-
useGit: z6.boolean().optional(),
|
|
4164
|
-
tokenBudget: z6.number().int().positive().max(1e5).optional(),
|
|
4165
|
-
...deliverySchema
|
|
4166
|
-
}),
|
|
4167
|
-
context_after_turn: z6.object({
|
|
4168
|
-
summary: z6.string().min(1).max(INPUT_LIMITS.summaryChars),
|
|
4169
|
-
changedFiles: z6.array(z6.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4170
|
-
planId: z6.string().min(1).max(100).optional(),
|
|
4171
|
-
taskType: z6.string().min(1).max(100).optional(),
|
|
4172
|
-
agentId: z6.string().min(1).max(INPUT_LIMITS.agentIdChars).optional(),
|
|
4173
|
-
sessionId: z6.string().min(1).max(200).optional(),
|
|
4174
|
-
verification: z6.array(z6.string().min(1).max(500)).max(20).optional(),
|
|
4175
|
-
commitSha: z6.string().min(7).max(64).optional()
|
|
4176
|
-
}),
|
|
4177
|
-
code_suggestions: z6.object({
|
|
4178
|
-
paths: z6.array(z6.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
4179
|
-
planId: z6.string().min(1).max(100).optional(),
|
|
4180
|
-
evidence: z6.enum(["overview", "source"]).optional(),
|
|
4181
|
-
useGit: z6.boolean().optional(),
|
|
4182
|
-
limit: z6.number().int().positive().max(100).optional()
|
|
4183
|
-
}),
|
|
4184
|
-
health: z6.object({})
|
|
4185
|
-
};
|
|
4186
|
-
var DATABASE_TOOLS = [
|
|
4187
|
-
"remember",
|
|
4188
|
-
"remember_batch",
|
|
4189
|
-
"recall",
|
|
4190
|
-
"list_facts",
|
|
4191
|
-
"list_namespaces",
|
|
4192
|
-
"forget",
|
|
4193
|
-
"share",
|
|
4194
|
-
"unshare"
|
|
4195
|
-
];
|
|
4196
|
-
var FILE_TOOLS = Object.keys(mcpToolInputSchemas).filter(
|
|
4197
|
-
(name) => !DATABASE_TOOLS.includes(name)
|
|
4198
|
-
);
|
|
4199
|
-
var DEGRADED_FALLBACK = "PostgreSQL is unavailable, so this call did nothing. File-based tools still work; read the repository directly and say recall is unavailable instead of guessing. Run `codebuddy db doctor` to diagnose; the next call after retryInMs reconnects.";
|
|
4200
|
-
var CORE_TOOLS = [
|
|
4201
|
-
"context_pack",
|
|
4202
|
-
"context_bootstrap",
|
|
4203
|
-
"context_before_edit",
|
|
4204
|
-
"context_after_turn",
|
|
4205
|
-
"code_suggestions",
|
|
4206
|
-
"health"
|
|
4207
|
-
];
|
|
4208
|
-
function resolveToolProfile(value = process.env.CODEBUDDY_TOOL_PROFILE) {
|
|
4209
|
-
if (value === void 0 || value === "" || value === "full") return "full";
|
|
4210
|
-
if (value === "core") return "core";
|
|
4211
|
-
throw new Error(`CODEBUDDY_TOOL_PROFILE must be "core" or "full", got "${value}"`);
|
|
4212
|
-
}
|
|
4213
|
-
var PLAN_BODY_LIMIT = 8e3;
|
|
4214
|
-
function serializePlan(plan) {
|
|
4215
|
-
if (!plan) return { plan: null };
|
|
4216
|
-
const body = plan.body.length > PLAN_BODY_LIMIT ? `${plan.body.slice(0, PLAN_BODY_LIMIT)}
|
|
4217
|
-
|
|
4218
|
-
\u2026[truncated]` : plan.body;
|
|
4219
|
-
return { plan: { ...plan, body } };
|
|
4220
|
-
}
|
|
4221
|
-
function registerCodeBuddyTools(server, options, profile = "full") {
|
|
4222
|
-
const exposed = (name) => profile === "full" || CORE_TOOLS.includes(name);
|
|
4223
|
-
const config = "database" in options ? options.config : options.memory.config;
|
|
4224
|
-
const traceRoot = config.projectRoot ?? process.cwd();
|
|
4225
|
-
const register = (name, toolConfig, callback) => {
|
|
4226
|
-
const traced = (async (...args) => {
|
|
4227
|
-
const started = Date.now();
|
|
4228
|
-
const input = args.length > 1 ? args[0] : void 0;
|
|
4229
|
-
const trace = (ok, output) => void recordTrace(traceRoot, {
|
|
4230
|
-
kind: "tool",
|
|
4231
|
-
name,
|
|
4232
|
-
ms: Date.now() - started,
|
|
4233
|
-
ok,
|
|
4234
|
-
tokensIn: estimateTokens(input),
|
|
4235
|
-
tokensOut: estimateTokens(output),
|
|
4236
|
-
...typeof input?.sessionId === "string" ? { session: input.sessionId } : {}
|
|
4237
|
-
});
|
|
4238
|
-
try {
|
|
4239
|
-
const result = await callback(...args);
|
|
4240
|
-
trace(result?.isError !== true, result?.content?.map((part) => part.text ?? "").join(""));
|
|
4241
|
-
return result;
|
|
4242
|
-
} catch (error) {
|
|
4243
|
-
trace(false);
|
|
4244
|
-
throw error;
|
|
4245
|
-
}
|
|
4246
|
-
});
|
|
4247
|
-
const tool = server.registerTool(name, toolConfig, traced);
|
|
4248
|
-
if (!exposed(name)) tool.remove();
|
|
4249
|
-
return tool;
|
|
4250
|
-
};
|
|
4251
|
-
const json = serializeToolResult;
|
|
4252
|
-
const database = "database" in options ? options.database : { get: async () => options, status: () => ({ available: true, state: "ready" }) };
|
|
4253
|
-
const viaDatabase = async (tool, run) => {
|
|
4254
|
-
let db;
|
|
4255
|
-
try {
|
|
4256
|
-
db = await database.get();
|
|
4257
|
-
} catch {
|
|
4258
|
-
return {
|
|
4259
|
-
...json({ degraded: true, tool, database: database.status(), fallback: DEGRADED_FALLBACK }),
|
|
4260
|
-
isError: true
|
|
4261
|
-
};
|
|
4262
|
-
}
|
|
4263
|
-
return json(await run(db));
|
|
4264
|
-
};
|
|
4265
|
-
const namespace = config.namespace;
|
|
4266
|
-
const projectRoot = config.projectRoot ?? process.cwd();
|
|
4267
|
-
const rankContextFacts = async (task, candidates) => {
|
|
4268
|
-
if (database.status().state !== "ready") return {};
|
|
4269
|
-
try {
|
|
4270
|
-
const { memory } = await database.get();
|
|
4271
|
-
return await memory.rankContextFacts(
|
|
4272
|
-
task,
|
|
4273
|
-
candidates.map(({ id }) => id)
|
|
4274
|
-
);
|
|
4275
|
-
} catch {
|
|
4276
|
-
return {};
|
|
4277
|
-
}
|
|
4278
|
-
};
|
|
4279
|
-
const reranker = configuredReranker(projectRoot).catch(() => void 0);
|
|
4280
|
-
const rerankerOption = async () => {
|
|
4281
|
-
const configured = await reranker;
|
|
4282
|
-
return configured ? { reranker: configured } : {};
|
|
4283
|
-
};
|
|
4284
|
-
const planStore = new PlanFileStore(projectRoot);
|
|
4285
|
-
const planLifecycle = new PlanLifecycle(planStore);
|
|
4286
|
-
const toFiles = (items) => items?.map((item) => ({ path: item.path, action: item.action, notes: item.notes ?? "" }));
|
|
4287
|
-
const toTests = (items) => items?.map((item) => ({ path: item.path, description: item.description ?? "" }));
|
|
4288
|
-
register(
|
|
4289
|
-
"remember",
|
|
4290
|
-
{
|
|
4291
|
-
title: "Remember",
|
|
4292
|
-
description: "Store one CodeBuddy memory item.",
|
|
4293
|
-
inputSchema: mcpToolInputSchemas.remember.shape
|
|
4294
|
-
},
|
|
4295
|
-
async (input) => viaDatabase("remember", (db) => db.memory.remember(toRememberInput(input)))
|
|
4296
|
-
);
|
|
4297
|
-
register(
|
|
4298
|
-
"remember_batch",
|
|
4299
|
-
{
|
|
4300
|
-
title: "Remember batch",
|
|
4301
|
-
description: "Store multiple CodeBuddy memory items.",
|
|
4302
|
-
inputSchema: mcpToolInputSchemas.remember_batch.shape
|
|
4303
|
-
},
|
|
4304
|
-
async (input) => viaDatabase("remember_batch", async (db) => ({
|
|
4305
|
-
items: await db.memory.rememberBatch(input.items.map(toRememberInput))
|
|
4306
|
-
}))
|
|
4307
|
-
);
|
|
4308
|
-
register(
|
|
4309
|
-
"recall",
|
|
4310
|
-
{
|
|
4311
|
-
title: "Recall",
|
|
4312
|
-
description: "Recall prompt-ready CodeBuddy context.",
|
|
4313
|
-
inputSchema: mcpToolInputSchemas.recall.shape
|
|
4314
|
-
},
|
|
4315
|
-
async (input) => viaDatabase("recall", (db) => db.memory.recall(toRecallInput(input)))
|
|
4316
|
-
);
|
|
4317
|
-
register(
|
|
4318
|
-
"list_facts",
|
|
4319
|
-
{
|
|
4320
|
-
title: "List facts",
|
|
4321
|
-
description: "List facts in the configured namespace.",
|
|
4322
|
-
inputSchema: mcpToolInputSchemas.list_facts.shape
|
|
4323
|
-
},
|
|
4324
|
-
async (input) => viaDatabase(
|
|
4325
|
-
"list_facts",
|
|
4326
|
-
(db) => listFactsPage(
|
|
4327
|
-
db.memory,
|
|
4328
|
-
db.repository,
|
|
4329
|
-
cleanOptional(input)
|
|
4330
|
-
)
|
|
4331
|
-
)
|
|
4332
|
-
);
|
|
4333
|
-
register(
|
|
4334
|
-
"list_namespaces",
|
|
4335
|
-
{
|
|
4336
|
-
title: "List namespaces",
|
|
4337
|
-
description: "List CodeBuddy namespaces.",
|
|
4338
|
-
inputSchema: mcpToolInputSchemas.list_namespaces.shape
|
|
4339
|
-
},
|
|
4340
|
-
async () => viaDatabase("list_namespaces", async (db) => ({
|
|
4341
|
-
namespaces: await db.repository.listNamespaces()
|
|
4342
|
-
}))
|
|
4343
|
-
);
|
|
4344
|
-
register(
|
|
4345
|
-
"forget",
|
|
4346
|
-
{
|
|
4347
|
-
title: "Forget",
|
|
4348
|
-
description: "Delete an interaction or fact by id.",
|
|
4349
|
-
inputSchema: mcpToolInputSchemas.forget.shape
|
|
4350
|
-
},
|
|
4351
|
-
async (input) => viaDatabase("forget", (db) => db.memory.forget(input.id))
|
|
4352
|
-
);
|
|
4353
|
-
register(
|
|
4354
|
-
"share",
|
|
4355
|
-
{
|
|
4356
|
-
title: "Share",
|
|
4357
|
-
description: "Share facts from the configured namespace to another namespace. Reference (default) is a live, revocable grant; snapshot copies the fact into the target project's Markdown store (requires to_project_root).",
|
|
4358
|
-
inputSchema: mcpToolInputSchemas.share.shape
|
|
4359
|
-
},
|
|
4360
|
-
async (input) => viaDatabase(
|
|
4361
|
-
"share",
|
|
4362
|
-
(db) => db.memory.share({
|
|
4363
|
-
from: input.from ?? namespace,
|
|
4364
|
-
to: input.to_namespace,
|
|
4365
|
-
factIds: input.factIds,
|
|
4366
|
-
...input.mode ? { mode: input.mode } : {},
|
|
4367
|
-
...input.to_project_root ? { targetProjectRoot: input.to_project_root } : {},
|
|
4368
|
-
...input.agentId ? { agentId: input.agentId } : {}
|
|
4369
|
-
})
|
|
4370
|
-
)
|
|
4371
|
-
);
|
|
4372
|
-
register(
|
|
4373
|
-
"unshare",
|
|
4374
|
-
{
|
|
4375
|
-
title: "Unshare",
|
|
4376
|
-
description: "Revoke live reference shares of configured-namespace facts to another namespace.",
|
|
4377
|
-
inputSchema: mcpToolInputSchemas.unshare.shape
|
|
4378
|
-
},
|
|
4379
|
-
async (input) => viaDatabase(
|
|
4380
|
-
"unshare",
|
|
4381
|
-
(db) => db.memory.unshare({
|
|
4382
|
-
to: input.to_namespace,
|
|
4383
|
-
factIds: input.factIds,
|
|
4384
|
-
...input.agentId ? { agentId: input.agentId } : {}
|
|
4385
|
-
})
|
|
4386
|
-
)
|
|
4387
|
-
);
|
|
4388
|
-
register(
|
|
4389
|
-
"plan_current",
|
|
4390
|
-
{
|
|
4391
|
-
title: "Current plan",
|
|
4392
|
-
description: "Return the active (approved or executing) plan for this namespace, or null.",
|
|
4393
|
-
inputSchema: mcpToolInputSchemas.plan_current.shape
|
|
4394
|
-
},
|
|
4395
|
-
async () => {
|
|
4396
|
-
const [current, policy] = await Promise.all([
|
|
4397
|
-
planLifecycle.current(namespace),
|
|
4398
|
-
loadPlanPolicy(projectRoot)
|
|
4399
|
-
]);
|
|
4400
|
-
return json({ ...serializePlan(current), policy });
|
|
4401
|
-
}
|
|
4402
|
-
);
|
|
4403
|
-
register(
|
|
4404
|
-
"plan_create",
|
|
4405
|
-
{
|
|
4406
|
-
title: "Create plan",
|
|
4407
|
-
description: "Create a draft plan in this namespace.",
|
|
4408
|
-
inputSchema: mcpToolInputSchemas.plan_create.shape
|
|
4409
|
-
},
|
|
4410
|
-
async (input) => {
|
|
4411
|
-
const files = toFiles(input.filesToTouch);
|
|
4412
|
-
const tests = toTests(input.testsToAdd);
|
|
4413
|
-
const created = await planLifecycle.create({
|
|
4414
|
-
namespace,
|
|
4415
|
-
title: input.title,
|
|
4416
|
-
brief: input.brief,
|
|
4417
|
-
...input.body !== void 0 ? { body: input.body } : {},
|
|
4418
|
-
...files ? { filesToTouch: files } : {},
|
|
4419
|
-
...tests ? { testsToAdd: tests } : {},
|
|
4420
|
-
...input.outOfScope !== void 0 ? { outOfScope: input.outOfScope } : {},
|
|
4421
|
-
...input.risks !== void 0 ? { risks: input.risks } : {},
|
|
4422
|
-
...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
|
|
4423
|
-
});
|
|
4424
|
-
return json(serializePlan(created));
|
|
4425
|
-
}
|
|
4426
|
-
);
|
|
4427
|
-
register(
|
|
4428
|
-
"plan_amend",
|
|
4429
|
-
{
|
|
4430
|
-
title: "Amend plan",
|
|
4431
|
-
description: "Amend a draft or executing plan's content fields.",
|
|
4432
|
-
inputSchema: mcpToolInputSchemas.plan_amend.shape
|
|
4433
|
-
},
|
|
4434
|
-
async (input) => {
|
|
4435
|
-
const patch = input.patch;
|
|
4436
|
-
const files = toFiles(patch.filesToTouch);
|
|
4437
|
-
const tests = toTests(patch.testsToAdd);
|
|
4438
|
-
const amended = await planLifecycle.amend(input.id, {
|
|
4439
|
-
...patch.title !== void 0 ? { title: patch.title } : {},
|
|
4440
|
-
...patch.brief !== void 0 ? { brief: patch.brief } : {},
|
|
4441
|
-
...patch.body !== void 0 ? { body: patch.body } : {},
|
|
4442
|
-
...files ? { filesToTouch: files } : {},
|
|
4443
|
-
...tests ? { testsToAdd: tests } : {},
|
|
4444
|
-
...patch.outOfScope !== void 0 ? { outOfScope: patch.outOfScope } : {},
|
|
4445
|
-
...patch.risks !== void 0 ? { risks: patch.risks } : {}
|
|
4446
|
-
});
|
|
4447
|
-
return json(serializePlan(amended));
|
|
4448
|
-
}
|
|
4449
|
-
);
|
|
4450
|
-
register(
|
|
4451
|
-
"plan_status",
|
|
4452
|
-
{
|
|
4453
|
-
title: "Transition plan",
|
|
4454
|
-
description: "Move a plan through its lifecycle: approved, executing, complete, abandoned.",
|
|
4455
|
-
inputSchema: mcpToolInputSchemas.plan_status.shape
|
|
4456
|
-
},
|
|
4457
|
-
async (input) => {
|
|
4458
|
-
let next;
|
|
4459
|
-
if (input.status === "approved") next = await planLifecycle.approve(input.id);
|
|
4460
|
-
else if (input.status === "executing") next = await planLifecycle.start(input.id);
|
|
4461
|
-
else if (input.status === "complete")
|
|
4462
|
-
next = await planLifecycle.complete(input.id, {
|
|
4463
|
-
...input.commitSha !== void 0 ? { commitSha: input.commitSha } : {},
|
|
4464
|
-
...input.notes !== void 0 ? { notes: input.notes } : {}
|
|
4465
|
-
});
|
|
4466
|
-
else
|
|
4467
|
-
next = await planLifecycle.abandon(
|
|
4468
|
-
input.id,
|
|
4469
|
-
input.reason !== void 0 ? { reason: input.reason } : {}
|
|
4470
|
-
);
|
|
4471
|
-
return json(serializePlan(next));
|
|
4472
|
-
}
|
|
4473
|
-
);
|
|
4474
|
-
register(
|
|
4475
|
-
"risk_assess",
|
|
4476
|
-
{
|
|
4477
|
-
title: "Assess risk",
|
|
4478
|
-
description: "Assess evidence-backed risk for a plan, explicit paths, or git changes.",
|
|
4479
|
-
inputSchema: mcpToolInputSchemas.risk_assess.shape
|
|
4480
|
-
},
|
|
4481
|
-
async (input) => json(
|
|
4482
|
-
await assessRisk({
|
|
4483
|
-
repositoryRoot: projectRoot,
|
|
4484
|
-
namespace,
|
|
4485
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
4486
|
-
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
4487
|
-
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
4488
|
-
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
4489
|
-
})
|
|
4490
|
-
)
|
|
4491
|
-
);
|
|
4492
|
-
register(
|
|
4493
|
-
"map_query",
|
|
4494
|
-
{
|
|
4495
|
-
title: "Query architecture map",
|
|
4496
|
-
description: "Query lightweight module import edges in the current repository.",
|
|
4497
|
-
inputSchema: mcpToolInputSchemas.map_query.shape
|
|
4498
|
-
},
|
|
4499
|
-
async (input) => {
|
|
4500
|
-
const graph = await cachedMap();
|
|
4501
|
-
return json(
|
|
4502
|
-
queryMap(graph, {
|
|
4503
|
-
...input.from !== void 0 ? { from: input.from } : {},
|
|
4504
|
-
...input.to !== void 0 ? { to: input.to } : {},
|
|
4505
|
-
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
4506
|
-
})
|
|
4507
|
-
);
|
|
4508
|
-
}
|
|
4509
|
-
);
|
|
4510
|
-
register(
|
|
4511
|
-
"map_neighbours",
|
|
4512
|
-
{
|
|
4513
|
-
title: "Module neighbours",
|
|
4514
|
-
description: "Return inbound/outbound neighbours for a repository module.",
|
|
4515
|
-
inputSchema: mcpToolInputSchemas.map_neighbours.shape
|
|
4516
|
-
},
|
|
4517
|
-
async (input) => json(neighbours(await cachedMap(), input.module, input.direction))
|
|
4518
|
-
);
|
|
4519
|
-
const tokenBudget = config.tokenBudget;
|
|
4520
|
-
const cachedMap = async () => {
|
|
4521
|
-
const { manifest } = await loadFreshIndex(projectRoot);
|
|
4522
|
-
const cached = await loadArchitectureMap(projectRoot, manifest);
|
|
4523
|
-
if (cached.map.modules.length > 0) return cached.map;
|
|
4524
|
-
return buildArchitectureMap(projectRoot);
|
|
4525
|
-
};
|
|
4526
|
-
register(
|
|
4527
|
-
"context_bootstrap",
|
|
4528
|
-
{
|
|
4529
|
-
title: "Bootstrap project context",
|
|
4530
|
-
description: "Return compact project awareness for the start of a turn: identity, active plan and policy, top policy rules, incident hotspots, and an architecture summary. Prefer this over a manual recall at session start. Optional sessionId enables session-aware delivery: pass the previous response's delivery.deliveryId as ack and unchanged sections are omitted (listed in delivery.unchanged); pass full=true to get everything again.",
|
|
4531
|
-
inputSchema: mcpToolInputSchemas.context_bootstrap.shape
|
|
4532
|
-
},
|
|
4533
|
-
async (input) => json(
|
|
4534
|
-
await applySessionDelivery(
|
|
4535
|
-
projectRoot,
|
|
4536
|
-
await buildBootstrapContext({
|
|
4537
|
-
repositoryRoot: projectRoot,
|
|
4538
|
-
namespace,
|
|
4539
|
-
...tokenBudget !== void 0 ? { tokenBudget } : {}
|
|
4540
|
-
}),
|
|
4541
|
-
deliveryOptions(input)
|
|
4542
|
-
)
|
|
4543
|
-
)
|
|
4544
|
-
);
|
|
4545
|
-
register(
|
|
4546
|
-
"context_before_edit",
|
|
4547
|
-
{
|
|
4548
|
-
title: "Context before edit",
|
|
4549
|
-
description: "Assemble relevant context before changing code: resolves target files from paths, a plan, or git, then returns the relevant plan, matching policy rules, incident memory, an evidence-backed risk assessment, and import neighbours. Optional sessionId enables session-aware delivery: pass the previous response's delivery.deliveryId as ack and unchanged sections are omitted (listed in delivery.unchanged); pass full=true to get everything again.",
|
|
4550
|
-
inputSchema: mcpToolInputSchemas.context_before_edit.shape
|
|
4551
|
-
},
|
|
4552
|
-
async (input) => json(
|
|
4553
|
-
await applySessionDelivery(
|
|
4554
|
-
projectRoot,
|
|
4555
|
-
await buildBeforeEditContext({
|
|
4556
|
-
repositoryRoot: projectRoot,
|
|
4557
|
-
namespace,
|
|
4558
|
-
semanticRanker: rankContextFacts,
|
|
4559
|
-
...await rerankerOption(),
|
|
4560
|
-
...input.evidence ? { evidence: input.evidence } : {},
|
|
4561
|
-
...input.task !== void 0 ? { task: input.task } : {},
|
|
4562
|
-
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
4563
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
4564
|
-
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
4565
|
-
...input.tokenBudget !== void 0 ? { tokenBudget: input.tokenBudget } : {},
|
|
4566
|
-
...tokenBudget !== void 0 && input.tokenBudget === void 0 ? { tokenBudget } : {}
|
|
4567
|
-
}),
|
|
4568
|
-
deliveryOptions(input)
|
|
4569
|
-
)
|
|
4570
|
-
)
|
|
4571
|
-
);
|
|
4572
|
-
register(
|
|
4573
|
-
"context_pack",
|
|
4574
|
-
{
|
|
4575
|
-
title: "Build task context pack",
|
|
4576
|
-
description: "Build the smallest useful context pack for a coding task. Automatically finds relevant files from the local index when paths are omitted, then includes only bounded symbols, policies, risks, incidents, architecture neighbours, and relevant team facts within the requested token budget. Use this once at the start of a task instead of manually chaining context, risk, map, and memory tools. Request evidence=source with exact paths when short excerpts omit needed behavior; source excerpts remain bounded by the token budget. Optional sessionId enables session-aware delivery: pass the previous response's delivery.deliveryId as ack and unchanged sections are omitted (listed in delivery.unchanged); pass full=true to get everything again.",
|
|
4577
|
-
inputSchema: mcpToolInputSchemas.context_pack.shape
|
|
4578
|
-
},
|
|
4579
|
-
async (input) => json(
|
|
4580
|
-
await applySessionDelivery(
|
|
4581
|
-
projectRoot,
|
|
4582
|
-
await buildBeforeEditContext({
|
|
4583
|
-
repositoryRoot: projectRoot,
|
|
4584
|
-
namespace,
|
|
4585
|
-
task: input.task,
|
|
4586
|
-
semanticRanker: rankContextFacts,
|
|
4587
|
-
...await rerankerOption(),
|
|
4588
|
-
...input.evidence ? { evidence: input.evidence } : {},
|
|
4589
|
-
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
4590
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
4591
|
-
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
4592
|
-
tokenBudget: input.tokenBudget ?? tokenBudget ?? 4e3
|
|
4593
|
-
}),
|
|
4594
|
-
deliveryOptions(input)
|
|
4595
|
-
)
|
|
4596
|
-
)
|
|
4597
|
-
);
|
|
4598
|
-
register(
|
|
4599
|
-
"context_after_turn",
|
|
4600
|
-
{
|
|
4601
|
-
title: "Capture context after a turn",
|
|
4602
|
-
description: "Extract durable knowledge from a turn summary. Confident facts/decisions/incidents are auto-saved as Markdown; temporary notes, low-confidence, and sensitive items are queued for review under .codebuddy/memory/review/; chatter is ignored. Never saves sensitive content automatically. Turns that changed files or report verification also record a bounded worklog handoff under .codebuddy/worklog/ so later chats can recover the work.",
|
|
4603
|
-
inputSchema: mcpToolInputSchemas.context_after_turn.shape
|
|
4604
|
-
},
|
|
4605
|
-
async (input) => {
|
|
4606
|
-
const result = await captureAfterTurn(
|
|
4607
|
-
{
|
|
4608
|
-
summary: input.summary,
|
|
4609
|
-
namespace,
|
|
4610
|
-
...input.changedFiles !== void 0 ? { changedFiles: input.changedFiles } : {},
|
|
4611
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
4612
|
-
...input.taskType !== void 0 ? { taskType: input.taskType } : {},
|
|
4613
|
-
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
4614
|
-
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
4615
|
-
...input.verification !== void 0 ? { verification: input.verification } : {},
|
|
4616
|
-
...input.commitSha !== void 0 ? { commitSha: input.commitSha } : {}
|
|
4617
|
-
},
|
|
4618
|
-
{
|
|
4619
|
-
repositoryRoot: projectRoot,
|
|
4620
|
-
...(input.changedFiles?.length ?? 0) > 0 ? await llmMemoryOptions(projectRoot) : {}
|
|
4621
|
-
}
|
|
4622
|
-
);
|
|
4623
|
-
const db = await database.get().catch(() => void 0);
|
|
4624
|
-
await db?.memory.reindexMemory();
|
|
4625
|
-
return json({
|
|
4626
|
-
saved: result.saved.map((d) => ({
|
|
4627
|
-
id: d.ref,
|
|
4628
|
-
class: d.candidate.class,
|
|
4629
|
-
subject: d.candidate.subject
|
|
4630
|
-
})),
|
|
4631
|
-
queuedForReview: result.queuedForReview.map((d) => ({
|
|
4632
|
-
id: d.ref,
|
|
4633
|
-
class: d.candidate.class,
|
|
4634
|
-
reason: d.reason,
|
|
4635
|
-
sensitive: d.sensitive
|
|
4636
|
-
})),
|
|
4637
|
-
ignored: result.ignored.length,
|
|
4638
|
-
reasons: result.reasons,
|
|
4639
|
-
extractor: result.extractor,
|
|
4640
|
-
worklogId: result.worklogId,
|
|
4641
|
-
...db ? {} : { degraded: true, database: database.status() }
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4644
|
-
);
|
|
4645
|
-
register(
|
|
4646
|
-
"code_suggestions",
|
|
4647
|
-
{
|
|
4648
|
-
title: "Code suggestions",
|
|
4649
|
-
description: "Return read-only, evidence-backed code-quality suggestions for a change set (paths, a plan, or git). Composes risk, plan divergence, architecture blast radius, incident history, missing tests, and stale policies. Never edits code.",
|
|
4650
|
-
inputSchema: mcpToolInputSchemas.code_suggestions.shape
|
|
4651
|
-
},
|
|
4652
|
-
async (input) => json(
|
|
4653
|
-
await generateSuggestions({
|
|
4654
|
-
repositoryRoot: projectRoot,
|
|
4655
|
-
namespace,
|
|
4656
|
-
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
4657
|
-
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
4658
|
-
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
4659
|
-
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
4660
|
-
})
|
|
4661
|
-
)
|
|
4662
|
-
);
|
|
4663
|
-
register(
|
|
4664
|
-
"health",
|
|
4665
|
-
{
|
|
4666
|
-
title: "Health",
|
|
4667
|
-
description: "Report which CodeBuddy tools are available right now: file-based tools always work; database tools (recall, remember, facts) need PostgreSQL. Does not wait on a connection attempt.",
|
|
4668
|
-
inputSchema: mcpToolInputSchemas.health.shape
|
|
4669
|
-
},
|
|
4670
|
-
async () => json({
|
|
4671
|
-
database: database.status(),
|
|
4672
|
-
tools: {
|
|
4673
|
-
profile,
|
|
4674
|
-
file: FILE_TOOLS.filter(exposed),
|
|
4675
|
-
database: DATABASE_TOOLS.filter(exposed)
|
|
4676
|
-
}
|
|
4677
|
-
})
|
|
4678
|
-
);
|
|
4679
|
-
}
|
|
4680
|
-
function deliveryOptions(input) {
|
|
4681
|
-
return cleanOptional(input);
|
|
4682
|
-
}
|
|
4683
|
-
function toRememberInput(input) {
|
|
4684
|
-
return cleanOptional(input);
|
|
4685
|
-
}
|
|
4686
|
-
function toRecallInput(input) {
|
|
4687
|
-
return cleanOptional(input);
|
|
4688
|
-
}
|
|
4689
|
-
function cleanOptional(input) {
|
|
4690
|
-
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
// src/mcp/server.ts
|
|
4694
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4695
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4696
|
-
import pino from "pino";
|
|
4697
|
-
|
|
4698
|
-
// src/automation/project-sync.ts
|
|
4699
|
-
import { watch as fsWatch2 } from "fs";
|
|
4700
|
-
import { join as join9, relative as relative6, resolve as resolve12, sep as sep6 } from "path";
|
|
4701
|
-
|
|
4702
|
-
// src/indexer/watch.ts
|
|
4703
|
-
import { watch as fsWatch } from "fs";
|
|
4704
|
-
import { resolve as resolve11 } from "path";
|
|
4705
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
4706
|
-
function createCoalescer(fn, delayMs) {
|
|
4707
|
-
let timer = null;
|
|
4708
|
-
let running = null;
|
|
4709
|
-
let dirty = false;
|
|
4710
|
-
let stopped = false;
|
|
4711
|
-
let firstTriggerAt = null;
|
|
4712
|
-
let maxWaitTimer = null;
|
|
4713
|
-
const clearTimers = () => {
|
|
4714
|
-
if (timer) {
|
|
4715
|
-
clearTimeout(timer);
|
|
4716
|
-
timer = null;
|
|
4717
|
-
}
|
|
4718
|
-
if (maxWaitTimer) {
|
|
4719
|
-
clearTimeout(maxWaitTimer);
|
|
4720
|
-
maxWaitTimer = null;
|
|
4721
|
-
}
|
|
4722
|
-
firstTriggerAt = null;
|
|
4723
|
-
};
|
|
4724
|
-
const run = () => {
|
|
4725
|
-
if (stopped || running) return;
|
|
4726
|
-
clearTimers();
|
|
4727
|
-
dirty = false;
|
|
4728
|
-
running = (async () => {
|
|
4729
|
-
try {
|
|
4730
|
-
await fn();
|
|
4731
|
-
} finally {
|
|
4732
|
-
running = null;
|
|
4733
|
-
if (dirty) schedule();
|
|
4734
|
-
}
|
|
4735
|
-
})();
|
|
4736
|
-
};
|
|
4737
|
-
const schedule = () => {
|
|
4738
|
-
if (stopped) return;
|
|
4739
|
-
dirty = true;
|
|
4740
|
-
if (firstTriggerAt === null) {
|
|
4741
|
-
firstTriggerAt = Date.now();
|
|
4742
|
-
maxWaitTimer = setTimeout(run, MAX_WAIT_MS);
|
|
4743
|
-
}
|
|
4744
|
-
if (timer) clearTimeout(timer);
|
|
4745
|
-
timer = setTimeout(run, delayMs);
|
|
4746
|
-
};
|
|
4747
|
-
return {
|
|
4748
|
-
trigger: schedule,
|
|
4749
|
-
stop: async () => {
|
|
4750
|
-
stopped = true;
|
|
4751
|
-
clearTimers();
|
|
4752
|
-
await running?.catch(() => void 0);
|
|
4753
|
-
}
|
|
4754
|
-
};
|
|
4755
|
-
}
|
|
4756
|
-
var MAX_WAIT_MS = 5e3;
|
|
4757
|
-
function isIgnoredEvent(filename, ignoredPaths = []) {
|
|
4758
|
-
if (!filename) return false;
|
|
4759
|
-
const normalized = filename.replace(/\\/g, "/");
|
|
4760
|
-
return ignoredPaths.some((path) => normalized === path || normalized.startsWith(`${path}/`)) || normalized.startsWith(".codebuddy/") || normalized.includes("/.git/") || normalized.startsWith(".git/") || normalized.includes("/node_modules/") || normalized.startsWith("node_modules/") || normalized.endsWith(".tmp");
|
|
4761
|
-
}
|
|
4762
|
-
async function watchRepository(options = {}) {
|
|
4763
|
-
const repositoryRoot = resolve11(options.repositoryRoot ?? process.cwd());
|
|
4764
|
-
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
4765
|
-
const runIndex = async () => {
|
|
4766
|
-
try {
|
|
4767
|
-
const result = await indexRepository({
|
|
4768
|
-
repositoryRoot,
|
|
4769
|
-
...options.now ? { now: options.now } : {}
|
|
4770
|
-
});
|
|
4771
|
-
options.onIndex?.(result);
|
|
4772
|
-
} catch (error) {
|
|
4773
|
-
options.onError?.(error);
|
|
4774
|
-
}
|
|
4775
|
-
};
|
|
4776
|
-
await runIndex();
|
|
4777
|
-
const coalescer = createCoalescer(runIndex, debounceMs);
|
|
4778
|
-
const watcher = fsWatch(repositoryRoot, { recursive: true }, (_event, filename) => {
|
|
4779
|
-
if (isIgnoredEvent(filename, options.ignoredPaths)) return;
|
|
4780
|
-
coalescer.trigger();
|
|
4781
|
-
});
|
|
4782
|
-
return new Promise((resolvePromise) => {
|
|
4783
|
-
const stop = () => {
|
|
4784
|
-
watcher.close();
|
|
4785
|
-
void coalescer.stop().then(() => resolvePromise());
|
|
4786
|
-
};
|
|
4787
|
-
if (options.signal) {
|
|
4788
|
-
if (options.signal.aborted) return stop();
|
|
4789
|
-
options.signal.addEventListener("abort", stop, { once: true });
|
|
4790
|
-
}
|
|
4791
|
-
watcher.on("error", () => stop());
|
|
4792
|
-
});
|
|
4793
|
-
}
|
|
4794
|
-
|
|
4795
|
-
// src/automation/project-sync.ts
|
|
4796
|
-
function startProjectSync(options) {
|
|
4797
|
-
const repositoryRoot = resolve12(options.repositoryRoot);
|
|
4798
|
-
const logger = options.logger ?? console;
|
|
4799
|
-
const controller = new AbortController();
|
|
4800
|
-
let watcher = null;
|
|
4801
|
-
let closing = false;
|
|
4802
|
-
let memoryWatcher = null;
|
|
4803
|
-
let memoryRefresh = null;
|
|
4804
|
-
const backgroundRefresh = (task) => {
|
|
4805
|
-
let current = null;
|
|
4806
|
-
let pending = false;
|
|
4807
|
-
const schedule = () => {
|
|
4808
|
-
if (closing) return;
|
|
4809
|
-
if (current) {
|
|
4810
|
-
pending = true;
|
|
4811
|
-
return;
|
|
4812
|
-
}
|
|
4813
|
-
current = (async () => {
|
|
4814
|
-
do {
|
|
4815
|
-
pending = false;
|
|
4816
|
-
await task();
|
|
4817
|
-
} while (pending && !closing);
|
|
4818
|
-
})().finally(() => {
|
|
4819
|
-
current = null;
|
|
4820
|
-
if (pending && !closing) schedule();
|
|
4821
|
-
});
|
|
4822
|
-
};
|
|
4823
|
-
return { schedule, settled: async () => current?.catch(() => void 0) };
|
|
4824
|
-
};
|
|
4825
|
-
const graphRefresh = backgroundRefresh(
|
|
4826
|
-
() => refreshGraph().catch(
|
|
4827
|
-
(error) => logger.error(`[sync] Graphify refresh failed: ${error.message}`)
|
|
4828
|
-
)
|
|
4829
|
-
);
|
|
4830
|
-
const zvecRefresh = backgroundRefresh(async () => {
|
|
4831
|
-
const result = await refreshZvecIndex(repositoryRoot, { signal: controller.signal }).catch(
|
|
4832
|
-
(error) => ({
|
|
4833
|
-
status: "failed",
|
|
4834
|
-
reason: error.message
|
|
4835
|
-
})
|
|
4836
|
-
);
|
|
4837
|
-
if (result.status !== "disabled") logger.info(`[sync] ${result.reason}`);
|
|
4838
|
-
});
|
|
4839
|
-
const ready = (async () => {
|
|
4840
|
-
await indexRepository({ repositoryRoot });
|
|
4841
|
-
zvecRefresh.schedule();
|
|
4842
|
-
watchMemory();
|
|
4843
|
-
if (options.graphify?.enabled) {
|
|
4844
|
-
await refreshGraph().catch(
|
|
4845
|
-
(error) => logger.error(`[sync] Graphify startup refresh failed: ${error.message}`)
|
|
4846
|
-
);
|
|
4847
|
-
}
|
|
4848
|
-
watcher = watchRepository({
|
|
4849
|
-
repositoryRoot,
|
|
4850
|
-
signal: controller.signal,
|
|
4851
|
-
...options.graphify?.enabled ? {
|
|
4852
|
-
ignoredPaths: [
|
|
4853
|
-
options.graphify.graphPath ? relative6(repositoryRoot, resolve12(repositoryRoot, options.graphify.graphPath)).split(sep6).join("/") : "graphify-out"
|
|
4854
|
-
]
|
|
4855
|
-
} : {},
|
|
4856
|
-
onIndex: (result) => {
|
|
4857
|
-
if (result.added + result.changed + result.removed === 0) return;
|
|
4858
|
-
zvecRefresh.schedule();
|
|
4859
|
-
if (options.graphify?.enabled) graphRefresh.schedule();
|
|
4860
|
-
},
|
|
4861
|
-
onError: (error) => logger.error(`[sync] repository watch failed: ${error.message}`)
|
|
4862
|
-
});
|
|
4863
|
-
void watcher.catch(
|
|
4864
|
-
(error) => logger.error(`[sync] live repository watch unavailable: ${error.message}`)
|
|
4865
|
-
);
|
|
4866
|
-
})().catch((error) => {
|
|
4867
|
-
logger.error(`[sync] startup synchronization failed: ${error.message}`);
|
|
4868
|
-
throw error;
|
|
4869
|
-
});
|
|
4870
|
-
function watchMemory() {
|
|
4871
|
-
const onMemoryChange = options.onMemoryChange;
|
|
4872
|
-
if (closing || !onMemoryChange) return;
|
|
4873
|
-
try {
|
|
4874
|
-
memoryRefresh = createCoalescer(
|
|
4875
|
-
() => onMemoryChange().catch(
|
|
4876
|
-
(error) => logger.error(`[sync] memory reindex failed: ${error.message}`)
|
|
4877
|
-
),
|
|
4878
|
-
300
|
|
4879
|
-
);
|
|
4880
|
-
memoryWatcher = fsWatch2(
|
|
4881
|
-
join9(repositoryRoot, ".codebuddy", "memory"),
|
|
4882
|
-
{ recursive: true },
|
|
4883
|
-
(_event, filename) => {
|
|
4884
|
-
const name = filename?.replace(/\\/g, "/") ?? "";
|
|
4885
|
-
if (/^(facts\/fact|summaries\/sum)_[a-z0-9]+\.md$/.test(name)) memoryRefresh?.trigger();
|
|
4886
|
-
}
|
|
4887
|
-
);
|
|
4888
|
-
memoryWatcher.on("error", () => memoryWatcher?.close());
|
|
4889
|
-
} catch (error) {
|
|
4890
|
-
logger.error(`[sync] live memory watch unavailable: ${error.message}`);
|
|
4891
|
-
}
|
|
4892
|
-
}
|
|
4893
|
-
async function refreshGraph() {
|
|
4894
|
-
const result = await generateGraphifyGraph({
|
|
4895
|
-
repositoryRoot,
|
|
4896
|
-
...options.graphify?.graphPath ? { graphPath: options.graphify.graphPath } : {}
|
|
4897
|
-
});
|
|
4898
|
-
logger.info(`[sync] Graphify updated (${result.nodes} nodes, ${result.edges} edges)`);
|
|
4899
|
-
}
|
|
4900
|
-
return {
|
|
4901
|
-
ready,
|
|
4902
|
-
async close() {
|
|
4903
|
-
closing = true;
|
|
4904
|
-
controller.abort();
|
|
4905
|
-
memoryRefresh?.stop();
|
|
4906
|
-
memoryWatcher?.close();
|
|
4907
|
-
await watcher?.catch(() => void 0);
|
|
4908
|
-
await Promise.all([graphRefresh.settled(), zvecRefresh.settled()]);
|
|
4909
|
-
}
|
|
4910
|
-
};
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
// src/core/project-root.ts
|
|
4914
|
-
import { realpath as realpath2, stat as stat5 } from "fs/promises";
|
|
4915
|
-
import { homedir } from "os";
|
|
4916
|
-
import { isAbsolute as isAbsolute2, parse, resolve as resolve13 } from "path";
|
|
4917
|
-
async function resolveInitializedProjectRoot(configuredRoot, cwd = process.cwd()) {
|
|
4918
|
-
if (configuredRoot !== void 0 && !isAbsolute2(configuredRoot)) {
|
|
4919
|
-
throw new Error("CODEBUDDY_PROJECT_ROOT must be an absolute path to an initialized project.");
|
|
4920
|
-
}
|
|
4921
|
-
const selected = resolve13(configuredRoot ?? cwd);
|
|
4922
|
-
const root = await realpath2(selected);
|
|
4923
|
-
if (root === parse(root).root || root === await realpath2(homedir())) {
|
|
4924
|
-
throw new Error(
|
|
4925
|
-
"Select an initialized project folder, not the filesystem root or home folder."
|
|
4926
|
-
);
|
|
4927
|
-
}
|
|
4928
|
-
const initialized = await stat5(configPath(root)).then(
|
|
4929
|
-
(info) => info.isFile(),
|
|
4930
|
-
() => false
|
|
4931
|
-
);
|
|
4932
|
-
if (!initialized) {
|
|
4933
|
-
throw new Error(
|
|
4934
|
-
`CodeBuddy will not scan ${root}: run codebuddy init in your project folder first. For a desktop client, re-register CodeBuddy from that folder to save its project path.`
|
|
4935
|
-
);
|
|
4936
|
-
}
|
|
4937
|
-
return root;
|
|
4938
|
-
}
|
|
4939
|
-
|
|
4940
|
-
// src/mcp/database.ts
|
|
4941
|
-
function createLazyDatabase(connect, options = {}) {
|
|
4942
|
-
const retryMs = options.retryMs ?? 5e3;
|
|
4943
|
-
let runtime;
|
|
4944
|
-
let pending;
|
|
4945
|
-
let failure;
|
|
4946
|
-
let closed = false;
|
|
4947
|
-
return {
|
|
4948
|
-
get() {
|
|
4949
|
-
if (runtime) return Promise.resolve(runtime);
|
|
4950
|
-
if (closed) return Promise.reject(new Error("CodeBuddy MCP server is shutting down."));
|
|
4951
|
-
if (pending) return pending;
|
|
4952
|
-
if (failure && Date.now() - failure.at < retryMs) {
|
|
4953
|
-
return Promise.reject(new Error(failure.error));
|
|
4954
|
-
}
|
|
4955
|
-
pending = connect().then(
|
|
4956
|
-
async (handle) => {
|
|
4957
|
-
if (closed) {
|
|
4958
|
-
await handle.close();
|
|
4959
|
-
throw new Error("CodeBuddy MCP server is shutting down.");
|
|
4960
|
-
}
|
|
4961
|
-
runtime = handle;
|
|
4962
|
-
failure = void 0;
|
|
4963
|
-
return handle;
|
|
4964
|
-
},
|
|
4965
|
-
(error) => {
|
|
4966
|
-
failure = { error: describeError(error), at: Date.now() };
|
|
4967
|
-
throw error;
|
|
4968
|
-
}
|
|
4969
|
-
).finally(() => {
|
|
4970
|
-
pending = void 0;
|
|
4971
|
-
});
|
|
4972
|
-
return pending;
|
|
4973
|
-
},
|
|
4974
|
-
status() {
|
|
4975
|
-
if (runtime) return { available: true, state: "ready" };
|
|
4976
|
-
if (pending) return { available: false, state: "connecting" };
|
|
4977
|
-
if (!failure) return { available: false, state: "idle" };
|
|
4978
|
-
return {
|
|
4979
|
-
available: false,
|
|
4980
|
-
state: "unavailable",
|
|
4981
|
-
error: failure.error,
|
|
4982
|
-
retryInMs: Math.max(0, failure.at + retryMs - Date.now())
|
|
4983
|
-
};
|
|
4984
|
-
},
|
|
4985
|
-
async close() {
|
|
4986
|
-
closed = true;
|
|
4987
|
-
await pending?.catch(() => void 0);
|
|
4988
|
-
const current = runtime;
|
|
4989
|
-
runtime = void 0;
|
|
4990
|
-
await current?.close();
|
|
4991
|
-
}
|
|
4992
|
-
};
|
|
4993
|
-
}
|
|
4994
|
-
function describeError(error) {
|
|
4995
|
-
const err = error;
|
|
4996
|
-
const text = err?.message || err?.code || String(error);
|
|
4997
|
-
return String(redactSecrets(text)).slice(0, 300);
|
|
4998
|
-
}
|
|
4999
|
-
|
|
5000
|
-
// src/mcp/server.ts
|
|
5001
|
-
async function startMcpServer(options = {}) {
|
|
5002
|
-
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }, process.stderr);
|
|
5003
|
-
const projectRoot = await resolveInitializedProjectRoot(
|
|
5004
|
-
options.config?.projectRoot ?? process.env.CODEBUDDY_PROJECT_ROOT
|
|
5005
|
-
);
|
|
5006
|
-
const config = {
|
|
5007
|
-
...options.config ?? await loadRuntimeConfig(projectRoot, { requireDatabase: false }),
|
|
5008
|
-
projectRoot
|
|
5009
|
-
};
|
|
5010
|
-
(config.postgresUrl ? codeBuddyConfigSchema : codeBuddyConfigSchema.omit({ postgresUrl: true })).parse(config);
|
|
5011
|
-
const toolProfile = resolveToolProfile();
|
|
5012
|
-
const importMemory = async (memory) => {
|
|
5013
|
-
try {
|
|
5014
|
-
const result = await memory.reindexMemory();
|
|
5015
|
-
for (const failure of result.failures) {
|
|
5016
|
-
logger.error(`[sync] memory file skipped (${failure.id}): ${failure.error}`);
|
|
5017
|
-
}
|
|
5018
|
-
} catch (error) {
|
|
5019
|
-
logger.error(`[sync] Markdown memory import failed: ${error.message}`);
|
|
5020
|
-
}
|
|
5021
|
-
};
|
|
5022
|
-
const database = createLazyDatabase(async () => {
|
|
5023
|
-
try {
|
|
5024
|
-
const runtime = await createRuntime(config, { autoBootstrap: true });
|
|
5025
|
-
await importMemory(runtime.memory);
|
|
5026
|
-
logger.info("PostgreSQL connected; memory tools available");
|
|
5027
|
-
return runtime;
|
|
5028
|
-
} catch (error) {
|
|
5029
|
-
logger.warn(
|
|
5030
|
-
`PostgreSQL unavailable; serving file-based tools only: ${error.message}`
|
|
5031
|
-
);
|
|
5032
|
-
throw error;
|
|
5033
|
-
}
|
|
5034
|
-
});
|
|
5035
|
-
void database.get().catch(() => void 0);
|
|
5036
|
-
const sync = startProjectSync({
|
|
5037
|
-
repositoryRoot: projectRoot,
|
|
5038
|
-
...config.graphify ? { graphify: config.graphify } : {},
|
|
5039
|
-
logger,
|
|
5040
|
-
// Hand edits re-import live once PostgreSQL is connected; until then the
|
|
5041
|
-
// next successful connect imports everything.
|
|
5042
|
-
onMemoryChange: async () => {
|
|
5043
|
-
if (database.status().state === "ready") await importMemory((await database.get()).memory);
|
|
5044
|
-
}
|
|
5045
|
-
});
|
|
5046
|
-
const server = new McpServer({ name: "codebuddy", version: VERSION });
|
|
5047
|
-
registerCodeBuddyTools(server, { config, database }, toolProfile);
|
|
5048
|
-
const shutdown = async () => {
|
|
5049
|
-
logger.info("codebuddy MCP server shutting down");
|
|
5050
|
-
await server.close();
|
|
5051
|
-
await sync.close();
|
|
5052
|
-
await database.close();
|
|
5053
|
-
};
|
|
5054
|
-
let stopping;
|
|
5055
|
-
const stop = () => {
|
|
5056
|
-
stopping ??= shutdown().finally(() => process.exit(0));
|
|
5057
|
-
};
|
|
5058
|
-
process.once("SIGINT", stop);
|
|
5059
|
-
process.once("SIGTERM", stop);
|
|
5060
|
-
process.stdin.once("end", stop);
|
|
5061
|
-
try {
|
|
5062
|
-
await sync.ready;
|
|
5063
|
-
await server.connect(new StdioServerTransport());
|
|
5064
|
-
} catch (error) {
|
|
5065
|
-
await sync.close();
|
|
5066
|
-
await database.close();
|
|
5067
|
-
throw error;
|
|
5068
|
-
}
|
|
5069
|
-
logger.info(`codebuddy MCP server started over stdio (tool profile: ${toolProfile})`);
|
|
5070
|
-
}
|
|
5071
|
-
|
|
5072
3751
|
export {
|
|
5073
|
-
migrateMemoryToFiles,
|
|
5074
3752
|
PLAN_STATUSES,
|
|
5075
3753
|
ACTIVE_PLAN_STATUSES,
|
|
5076
3754
|
PlanLockError,
|
|
5077
3755
|
PlanValidationError,
|
|
5078
3756
|
PlanFileStore,
|
|
5079
|
-
PlanTransitionError,
|
|
5080
|
-
PlanNotFoundError,
|
|
5081
|
-
ActivePlanExistsError,
|
|
5082
|
-
PlanLifecycle,
|
|
5083
|
-
watchRepository,
|
|
5084
|
-
VERSION,
|
|
5085
3757
|
RiskAssessor,
|
|
5086
3758
|
createChurnSignal,
|
|
5087
3759
|
createIncidentSignal,
|
|
@@ -5089,24 +3761,32 @@ export {
|
|
|
5089
3761
|
readPolicyFile,
|
|
5090
3762
|
assessRisk,
|
|
5091
3763
|
resolveRiskPaths,
|
|
3764
|
+
PlanTransitionError,
|
|
3765
|
+
PlanNotFoundError,
|
|
3766
|
+
ActivePlanExistsError,
|
|
3767
|
+
PlanLifecycle,
|
|
3768
|
+
enforceContextBudget,
|
|
5092
3769
|
WorklogStore,
|
|
5093
3770
|
buildBootstrapContext,
|
|
5094
3771
|
buildBeforeEditContext,
|
|
5095
3772
|
ReviewStore,
|
|
3773
|
+
captureAfterTurn,
|
|
5096
3774
|
approveReviewItem,
|
|
5097
|
-
|
|
5098
|
-
readTraces,
|
|
5099
|
-
parseTraceLines,
|
|
3775
|
+
configuredReranker,
|
|
5100
3776
|
openHookSemantics,
|
|
3777
|
+
CONTEXT_HOOK_BUDGET_MS,
|
|
5101
3778
|
runContextHook,
|
|
5102
3779
|
runStageHook,
|
|
3780
|
+
llmMemoryOptions,
|
|
5103
3781
|
runCaptureHook,
|
|
5104
3782
|
runSyncHook,
|
|
5105
3783
|
runResetHook,
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
3784
|
+
extractEditedPaths,
|
|
3785
|
+
deriveSummary,
|
|
3786
|
+
turnVerification,
|
|
3787
|
+
verificationLines,
|
|
3788
|
+
turnUserPrompts,
|
|
3789
|
+
lastAssistantMessage,
|
|
3790
|
+
renderContextBlock
|
|
5111
3791
|
};
|
|
5112
|
-
//# sourceMappingURL=chunk-
|
|
3792
|
+
//# sourceMappingURL=chunk-YJIA2MFL.js.map
|