@orquentra/codebuddy 2.6.2 → 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/dist/{chunk-IBLJOHJL.js → chunk-3QGW53GD.js} +2 -2
- package/dist/{chunk-NERKZSSE.js → chunk-5EDGAZQN.js} +438 -68
- package/dist/chunk-5EDGAZQN.js.map +1 -0
- package/dist/{chunk-JFDRBPYF.js → chunk-6JCKMIQY.js} +2 -2
- package/dist/{chunk-2XDPKFUW.js → chunk-AK2UAE32.js} +46 -20
- 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-QBDJXGG5.js → chunk-AQHPEJR6.js} +93 -22
- package/dist/chunk-AQHPEJR6.js.map +1 -0
- package/dist/{chunk-OWWCDRXW.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-IHQM2ZDV.js → chunk-DFV2SK6C.js} +1 -1
- package/dist/{chunk-IHQM2ZDV.js.map → chunk-DFV2SK6C.js.map} +1 -1
- package/dist/{chunk-IMFUYEZA.js → chunk-EEUFU7KV.js} +81 -54
- package/dist/chunk-EEUFU7KV.js.map +1 -0
- package/dist/{chunk-5MQOE4J6.js → chunk-ESZXCZIN.js} +3 -3
- package/dist/{chunk-J2EAZ4MA.js → chunk-GKNYMNFM.js} +22 -25
- package/dist/chunk-GKNYMNFM.js.map +1 -0
- package/dist/{chunk-JPHJBCZB.js → chunk-HPOR6YXA.js} +3 -2
- package/dist/chunk-HPOR6YXA.js.map +1 -0
- package/dist/{chunk-S6VEG4S2.js → chunk-JUAFCLRB.js} +28 -5
- package/dist/chunk-JUAFCLRB.js.map +1 -0
- package/dist/{chunk-5KZY6OTG.js → chunk-KNR2P2UY.js} +33 -83
- package/dist/chunk-KNR2P2UY.js.map +1 -0
- package/dist/{chunk-B6CO63SQ.js → chunk-LCSBL5TT.js} +21 -9
- package/dist/chunk-LCSBL5TT.js.map +1 -0
- package/dist/{chunk-5FW3PMNP.js → chunk-MSUAVR3R.js} +76 -11
- package/dist/chunk-MSUAVR3R.js.map +1 -0
- package/dist/{chunk-RAJJSCIP.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-R7CD6EXS.js → chunk-YJIA2MFL.js} +572 -377
- package/dist/chunk-YJIA2MFL.js.map +1 -0
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{cloud-7WM3YLI4.js → cloud-LXZUR3U2.js} +3 -3
- package/dist/{codebuddy-B_pCaQrV.d.ts → codebuddy-DO7ZhoWn.d.ts} +15 -1
- package/dist/config-GEZFLXK6.js +12 -0
- package/dist/index.d.ts +46 -3
- package/dist/index.js +22 -19
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-command-CWJAVQ5G.js +18 -0
- package/dist/{local-5MZURLCZ.js → local-ST4WMRCG.js} +3 -3
- package/dist/{main-MTBQZGJV.js → main-AHNGHC4L.js} +213 -183
- package/dist/main-AHNGHC4L.js.map +1 -0
- package/dist/{memory-KIC5FJVW.js → memory-TGHVTBME.js} +9 -8
- package/dist/memory-TGHVTBME.js.map +1 -0
- package/dist/operations-7BTAAUHF.js +26 -0
- package/dist/pretool-command-3SXQSMAO.js +13 -0
- package/dist/{rerank-5QZ3PNUW.js → rerank-IQAEWLQR.js} +6 -5
- package/dist/rerank-IQAEWLQR.js.map +1 -0
- package/dist/{runtime-CUBOOPZO.js → runtime-E7JQZ2BZ.js} +14 -11
- package/dist/summaries-XV3JGGCC.js +20 -0
- package/dist/{zvec-MSRKT7IV.js → zvec-KP4KNB7Y.js} +10 -3
- package/package.json +1 -1
- 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-2XDPKFUW.js.map +0 -1
- package/dist/chunk-5FW3PMNP.js.map +0 -1
- package/dist/chunk-5KZY6OTG.js.map +0 -1
- package/dist/chunk-B6CO63SQ.js.map +0 -1
- package/dist/chunk-GDJH7XJC.js.map +0 -1
- package/dist/chunk-IMFUYEZA.js.map +0 -1
- package/dist/chunk-J2EAZ4MA.js.map +0 -1
- package/dist/chunk-JPHJBCZB.js.map +0 -1
- package/dist/chunk-NERKZSSE.js.map +0 -1
- package/dist/chunk-OWWCDRXW.js.map +0 -1
- package/dist/chunk-PQK3GKZH.js +0 -98
- package/dist/chunk-PQK3GKZH.js.map +0 -1
- package/dist/chunk-QBDJXGG5.js.map +0 -1
- package/dist/chunk-R7CD6EXS.js.map +0 -1
- package/dist/chunk-S6VEG4S2.js.map +0 -1
- package/dist/chunk-XBJQKCLU.js.map +0 -1
- package/dist/config-DEVHUDIU.js +0 -11
- package/dist/lean-command-HTQCPH76.js +0 -15
- package/dist/main-MTBQZGJV.js.map +0 -1
- package/dist/memory-KIC5FJVW.js.map +0 -1
- package/dist/operations-POCHY3WR.js +0 -25
- package/dist/pretool-command-AX6JXP25.js +0 -10
- package/dist/rerank-5QZ3PNUW.js.map +0 -1
- package/dist/summaries-SB5L2TSW.js +0 -18
- /package/dist/{chunk-IBLJOHJL.js.map → chunk-3QGW53GD.js.map} +0 -0
- /package/dist/{chunk-JFDRBPYF.js.map → chunk-6JCKMIQY.js.map} +0 -0
- /package/dist/{chunk-5MQOE4J6.js.map → chunk-ESZXCZIN.js.map} +0 -0
- /package/dist/{chunk-RAJJSCIP.js.map → chunk-SFMIMLXR.js.map} +0 -0
- /package/dist/{cloud-7WM3YLI4.js.map → cloud-LXZUR3U2.js.map} +0 -0
- /package/dist/{config-DEVHUDIU.js.map → config-GEZFLXK6.js.map} +0 -0
- /package/dist/{lean-command-HTQCPH76.js.map → lean-command-CWJAVQ5G.js.map} +0 -0
- /package/dist/{local-5MZURLCZ.js.map → local-ST4WMRCG.js.map} +0 -0
- /package/dist/{operations-POCHY3WR.js.map → operations-7BTAAUHF.js.map} +0 -0
- /package/dist/{pretool-command-AX6JXP25.js.map → pretool-command-3SXQSMAO.js.map} +0 -0
- /package/dist/{runtime-CUBOOPZO.js.map → runtime-E7JQZ2BZ.js.map} +0 -0
- /package/dist/{summaries-SB5L2TSW.js.map → summaries-XV3JGGCC.js.map} +0 -0
- /package/dist/{zvec-MSRKT7IV.js.map → zvec-KP4KNB7Y.js.map} +0 -0
|
@@ -1,14 +1,30 @@
|
|
|
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
|
-
findCoveringTest
|
|
12
|
+
findCoveringTest
|
|
13
|
+
} from "./chunk-GKNYMNFM.js";
|
|
14
|
+
import {
|
|
10
15
|
hookRoot
|
|
11
|
-
} from "./chunk-
|
|
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";
|
|
12
28
|
import {
|
|
13
29
|
TASK_PATH_LIMIT,
|
|
14
30
|
countTerms,
|
|
@@ -18,50 +34,46 @@ import {
|
|
|
18
34
|
loadProjectArchitectureMap,
|
|
19
35
|
namedSegments,
|
|
20
36
|
readBoundedSource,
|
|
37
|
+
syncFileVectors,
|
|
21
38
|
taskTerms
|
|
22
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-5EDGAZQN.js";
|
|
23
40
|
import {
|
|
24
41
|
extractSymbolTable,
|
|
25
42
|
supportsSymbols
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
import {
|
|
28
|
-
INCIDENT_SEVERITIES,
|
|
29
|
-
MemoryFileStore,
|
|
30
|
-
isEligibleMemory,
|
|
31
|
-
lexicalTerms,
|
|
32
|
-
termOverlap
|
|
33
|
-
} from "./chunk-S6VEG4S2.js";
|
|
43
|
+
} from "./chunk-Y7D7PUBE.js";
|
|
34
44
|
import {
|
|
45
|
+
REDACTED_LINE,
|
|
35
46
|
claimText,
|
|
47
|
+
isCredentialPath,
|
|
48
|
+
loadKnownSecretValues,
|
|
36
49
|
redactPersisted,
|
|
37
50
|
redactSensitive,
|
|
51
|
+
redactSourceText,
|
|
38
52
|
scanFields,
|
|
39
53
|
scanSensitive
|
|
40
|
-
} from "./chunk-
|
|
41
|
-
import {
|
|
42
|
-
generateEntityId
|
|
43
|
-
} from "./chunk-XBJQKCLU.js";
|
|
54
|
+
} from "./chunk-AL6VBTNA.js";
|
|
44
55
|
import {
|
|
45
56
|
DEFAULT_RERANK_MODELS
|
|
46
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-HPOR6YXA.js";
|
|
47
58
|
import {
|
|
48
|
-
assertNoSymlinkBetween,
|
|
49
|
-
composeMarkdown,
|
|
50
59
|
loadPlanPolicy,
|
|
51
60
|
loadRuntimeConfig,
|
|
61
|
+
readConfigFile
|
|
62
|
+
} from "./chunk-KNR2P2UY.js";
|
|
63
|
+
import {
|
|
64
|
+
assertNoSymlinkBetween,
|
|
65
|
+
composeMarkdown,
|
|
52
66
|
parseFrontMatter,
|
|
53
|
-
readConfigFile,
|
|
54
67
|
writeAtomic
|
|
55
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-CG5B3LYG.js";
|
|
56
69
|
|
|
57
70
|
// src/hooks/runtime.ts
|
|
58
|
-
import { mkdir as
|
|
59
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
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";
|
|
60
73
|
import pTimeout2 from "p-timeout";
|
|
61
74
|
|
|
62
75
|
// src/context/engine.ts
|
|
63
76
|
import { execFile } from "child_process";
|
|
64
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
65
77
|
import { join as join4, resolve as resolve5 } from "path";
|
|
66
78
|
import { promisify } from "util";
|
|
67
79
|
|
|
@@ -307,193 +319,10 @@ async function releaseOwnedLock(lockPath) {
|
|
|
307
319
|
}
|
|
308
320
|
}
|
|
309
321
|
|
|
310
|
-
// src/core/plan-lifecycle.ts
|
|
311
|
-
var TRANSITIONS = {
|
|
312
|
-
draft: ["approved", "abandoned"],
|
|
313
|
-
approved: ["executing", "abandoned"],
|
|
314
|
-
executing: ["complete", "abandoned"],
|
|
315
|
-
complete: [],
|
|
316
|
-
abandoned: []
|
|
317
|
-
};
|
|
318
|
-
var AMENDABLE = ["draft", "executing"];
|
|
319
|
-
var PlanTransitionError = class extends Error {
|
|
320
|
-
constructor(from, to) {
|
|
321
|
-
super(`Illegal plan transition: ${from} \u2192 ${to}.`);
|
|
322
|
-
this.name = "PlanTransitionError";
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
var PlanNotFoundError = class extends Error {
|
|
326
|
-
constructor(id) {
|
|
327
|
-
super(`Plan ${id} not found.`);
|
|
328
|
-
this.name = "PlanNotFoundError";
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
var ActivePlanExistsError = class extends Error {
|
|
332
|
-
activePlanId;
|
|
333
|
-
constructor(namespace, activePlanId) {
|
|
334
|
-
super(
|
|
335
|
-
`Namespace "${namespace}" already has an active plan (${activePlanId}). Complete or abandon it before approving another.`
|
|
336
|
-
);
|
|
337
|
-
this.name = "ActivePlanExistsError";
|
|
338
|
-
this.activePlanId = activePlanId;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
var DEFAULT_BODY = [
|
|
342
|
-
"# Goal",
|
|
343
|
-
"",
|
|
344
|
-
"(what this plan achieves)",
|
|
345
|
-
"",
|
|
346
|
-
"# Approach",
|
|
347
|
-
"",
|
|
348
|
-
"(ordered steps)",
|
|
349
|
-
"",
|
|
350
|
-
"# Notes",
|
|
351
|
-
"",
|
|
352
|
-
"(freeform; ignored by tooling)"
|
|
353
|
-
].join("\n");
|
|
354
|
-
var PlanLifecycle = class {
|
|
355
|
-
store;
|
|
356
|
-
constructor(store) {
|
|
357
|
-
this.store = store;
|
|
358
|
-
}
|
|
359
|
-
async create(input) {
|
|
360
|
-
const id = generateEntityId("pln");
|
|
361
|
-
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
362
|
-
await this.store.writePlan({
|
|
363
|
-
id,
|
|
364
|
-
namespace: input.namespace,
|
|
365
|
-
title: input.title,
|
|
366
|
-
status: "draft",
|
|
367
|
-
brief: input.brief,
|
|
368
|
-
filesToTouch: input.filesToTouch ?? [],
|
|
369
|
-
testsToAdd: input.testsToAdd ?? [],
|
|
370
|
-
outOfScope: input.outOfScope ?? [],
|
|
371
|
-
risks: input.risks ?? [],
|
|
372
|
-
createdAt: now,
|
|
373
|
-
createdByAgent: input.createdByAgent ?? null,
|
|
374
|
-
approvedAt: null,
|
|
375
|
-
executingAt: null,
|
|
376
|
-
completedAt: null,
|
|
377
|
-
abandonedAt: null,
|
|
378
|
-
commitSha: null,
|
|
379
|
-
abandonReason: null,
|
|
380
|
-
body: input.body ?? DEFAULT_BODY
|
|
381
|
-
});
|
|
382
|
-
return this.store.readPlan(id);
|
|
383
|
-
}
|
|
384
|
-
async approve(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
385
|
-
const current = await this.require(id);
|
|
386
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
387
|
-
const fresh = await this.require(id);
|
|
388
|
-
this.assertTransition(fresh.status, "approved");
|
|
389
|
-
const active = await this.store.findActivePlan(fresh.namespace);
|
|
390
|
-
if (active && active.id !== id) {
|
|
391
|
-
throw new ActivePlanExistsError(fresh.namespace, active.id);
|
|
392
|
-
}
|
|
393
|
-
return this.rewrite(fresh, { status: "approved", approvedAt: now });
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
async start(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
397
|
-
const current = await this.require(id);
|
|
398
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
399
|
-
const fresh = await this.require(id);
|
|
400
|
-
this.assertTransition(fresh.status, "executing");
|
|
401
|
-
return this.rewrite(fresh, { status: "executing", executingAt: now });
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
async complete(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
405
|
-
const current = await this.require(id);
|
|
406
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
407
|
-
const fresh = await this.require(id);
|
|
408
|
-
this.assertTransition(fresh.status, "complete");
|
|
409
|
-
return this.rewrite(fresh, {
|
|
410
|
-
status: "complete",
|
|
411
|
-
completedAt: now,
|
|
412
|
-
...options.commitSha !== void 0 ? { commitSha: options.commitSha } : {}
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
async abandon(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
417
|
-
const current = await this.require(id);
|
|
418
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
419
|
-
const fresh = await this.require(id);
|
|
420
|
-
this.assertTransition(fresh.status, "abandoned");
|
|
421
|
-
return this.rewrite(fresh, {
|
|
422
|
-
status: "abandoned",
|
|
423
|
-
abandonedAt: now,
|
|
424
|
-
...options.reason !== void 0 ? { abandonReason: options.reason } : {}
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
async amend(id, patch) {
|
|
429
|
-
const current = await this.require(id);
|
|
430
|
-
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
431
|
-
const fresh = await this.require(id);
|
|
432
|
-
if (!AMENDABLE.includes(fresh.status)) {
|
|
433
|
-
throw new Error(
|
|
434
|
-
`Plan ${id} is ${fresh.status}; only draft or executing plans can be amended.`
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
return this.rewrite(fresh, {
|
|
438
|
-
...patch.title !== void 0 ? { title: patch.title } : {},
|
|
439
|
-
...patch.brief !== void 0 ? { brief: patch.brief } : {},
|
|
440
|
-
...patch.filesToTouch !== void 0 ? { filesToTouch: patch.filesToTouch } : {},
|
|
441
|
-
...patch.testsToAdd !== void 0 ? { testsToAdd: patch.testsToAdd } : {},
|
|
442
|
-
...patch.outOfScope !== void 0 ? { outOfScope: patch.outOfScope } : {},
|
|
443
|
-
...patch.risks !== void 0 ? { risks: patch.risks } : {},
|
|
444
|
-
...patch.body !== void 0 ? { body: patch.body } : {}
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
async current(namespace) {
|
|
449
|
-
return this.store.findActivePlan(namespace);
|
|
450
|
-
}
|
|
451
|
-
assertTransition(from, to) {
|
|
452
|
-
if (!TRANSITIONS[from].includes(to)) {
|
|
453
|
-
throw new PlanTransitionError(from, to);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
async require(id) {
|
|
457
|
-
try {
|
|
458
|
-
return await this.store.readPlan(id);
|
|
459
|
-
} catch (error) {
|
|
460
|
-
if (error.code === "ENOENT") {
|
|
461
|
-
throw new PlanNotFoundError(id);
|
|
462
|
-
}
|
|
463
|
-
throw error;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
/** Rewrite the plan file with a partial set of changed fields. */
|
|
467
|
-
async rewrite(plan, changes) {
|
|
468
|
-
const next = { ...plan, ...changes };
|
|
469
|
-
await this.store.writePlan({
|
|
470
|
-
id: next.id,
|
|
471
|
-
namespace: next.namespace,
|
|
472
|
-
title: next.title,
|
|
473
|
-
status: next.status,
|
|
474
|
-
brief: next.brief,
|
|
475
|
-
filesToTouch: next.filesToTouch,
|
|
476
|
-
testsToAdd: next.testsToAdd,
|
|
477
|
-
outOfScope: next.outOfScope,
|
|
478
|
-
risks: next.risks,
|
|
479
|
-
createdAt: next.createdAt,
|
|
480
|
-
createdByAgent: next.createdByAgent,
|
|
481
|
-
approvedAt: next.approvedAt,
|
|
482
|
-
executingAt: next.executingAt,
|
|
483
|
-
completedAt: next.completedAt,
|
|
484
|
-
abandonedAt: next.abandonedAt,
|
|
485
|
-
commitSha: next.commitSha,
|
|
486
|
-
abandonReason: next.abandonReason,
|
|
487
|
-
body: next.body
|
|
488
|
-
});
|
|
489
|
-
return this.store.readPlan(next.id);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
|
|
493
322
|
// src/risk/service.ts
|
|
494
323
|
import { spawn as spawn2 } from "child_process";
|
|
495
324
|
import { realpath as realpath2 } from "fs/promises";
|
|
496
|
-
import { relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
325
|
+
import { dirname as dirname2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
497
326
|
|
|
498
327
|
// src/risk/assessor.ts
|
|
499
328
|
var RiskAssessor = class {
|
|
@@ -514,7 +343,7 @@ var RiskAssessor = class {
|
|
|
514
343
|
const byPath = /* @__PURE__ */ new Map();
|
|
515
344
|
const perSignal = /* @__PURE__ */ new Map();
|
|
516
345
|
for (const { signal, contributions } of settled) {
|
|
517
|
-
const
|
|
346
|
+
const stat5 = perSignal.get(signal.id) ?? {
|
|
518
347
|
id: signal.id,
|
|
519
348
|
category: signal.category,
|
|
520
349
|
contributions: 0,
|
|
@@ -525,10 +354,10 @@ var RiskAssessor = class {
|
|
|
525
354
|
const list = byPath.get(contribution.path) ?? [];
|
|
526
355
|
list.push({ signal, contribution });
|
|
527
356
|
byPath.set(contribution.path, list);
|
|
528
|
-
|
|
529
|
-
|
|
357
|
+
stat5.contributions += 1;
|
|
358
|
+
stat5.totalWeight += contribution.weight;
|
|
530
359
|
}
|
|
531
|
-
perSignal.set(signal.id,
|
|
360
|
+
perSignal.set(signal.id, stat5);
|
|
532
361
|
}
|
|
533
362
|
const items = [];
|
|
534
363
|
for (const [path, contributions] of byPath) {
|
|
@@ -594,9 +423,10 @@ function round2(value) {
|
|
|
594
423
|
|
|
595
424
|
// src/risk/signals/churn.ts
|
|
596
425
|
import { spawn } from "child_process";
|
|
597
|
-
import { realpath } from "fs/promises";
|
|
426
|
+
import { readFile as readFile2, realpath } from "fs/promises";
|
|
598
427
|
import { resolve as resolve2 } from "path";
|
|
599
428
|
var GIT_LOG_CONCURRENCY = 4;
|
|
429
|
+
var DAY_MS = 864e5;
|
|
600
430
|
function createChurnSignal(options) {
|
|
601
431
|
return {
|
|
602
432
|
id: "git-churn",
|
|
@@ -611,20 +441,56 @@ function createChurnSignal(options) {
|
|
|
611
441
|
if (root.code !== 0 || resolve2(root.stdout.trim()) !== expected) {
|
|
612
442
|
return [];
|
|
613
443
|
}
|
|
614
|
-
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;
|
|
615
459
|
const logs = [];
|
|
460
|
+
const sizes = [];
|
|
616
461
|
for (let start = 0; start < input.paths.length; start += GIT_LOG_CONCURRENCY) {
|
|
617
462
|
const batch = input.paths.slice(start, start + GIT_LOG_CONCURRENCY);
|
|
618
463
|
logs.push(
|
|
619
464
|
...await Promise.all(
|
|
620
|
-
batch.map((path) => gitLogForPath(options.repositoryRoot, path,
|
|
465
|
+
batch.map((path) => gitLogForPath(options.repositoryRoot, path, range))
|
|
621
466
|
)
|
|
622
467
|
);
|
|
468
|
+
if (lifetime) {
|
|
469
|
+
sizes.push(
|
|
470
|
+
...await Promise.all(
|
|
471
|
+
batch.map((path) => lineCount(options.repositoryRoot, path, revision))
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
623
475
|
}
|
|
624
476
|
const contributions = [];
|
|
625
477
|
for (const [index, path] of input.paths.entries()) {
|
|
626
478
|
const commits = logs[index] ?? [];
|
|
627
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
|
+
}
|
|
628
494
|
const authors = new Set(commits.map((commit) => commit.author));
|
|
629
495
|
const reverts = commits.filter((commit) => /revert|rollback|backout/i.test(commit.message));
|
|
630
496
|
const weight = Math.min(100, commits.length * 8 + authors.size * 12 + reverts.length * 20);
|
|
@@ -632,23 +498,38 @@ function createChurnSignal(options) {
|
|
|
632
498
|
path,
|
|
633
499
|
weight,
|
|
634
500
|
reason: `${path} changed ${commits.length} time${commits.length === 1 ? "" : "s"} in the recent git history by ${authors.size} author${authors.size === 1 ? "" : "s"}.`,
|
|
635
|
-
evidence: commits
|
|
636
|
-
kind: "git_commit",
|
|
637
|
-
sha: commit.sha,
|
|
638
|
-
message: commit.message,
|
|
639
|
-
ts: commit.ts
|
|
640
|
-
}))
|
|
501
|
+
evidence: evidenceOf(commits)
|
|
641
502
|
});
|
|
642
503
|
}
|
|
643
504
|
return contributions;
|
|
644
505
|
}
|
|
645
506
|
};
|
|
646
507
|
}
|
|
647
|
-
|
|
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) {
|
|
648
528
|
const result = await runGit(repositoryRoot, [
|
|
649
529
|
"log",
|
|
650
530
|
"--follow",
|
|
651
|
-
|
|
531
|
+
"--max-count=1000",
|
|
532
|
+
...range,
|
|
652
533
|
"--format=%H%x1f%an%x1f%cI%x1f%s%x1e",
|
|
653
534
|
"--",
|
|
654
535
|
path
|
|
@@ -734,7 +615,7 @@ function severityWeight(severity) {
|
|
|
734
615
|
}
|
|
735
616
|
|
|
736
617
|
// src/risk/signals/policy.ts
|
|
737
|
-
import { readFile as
|
|
618
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
738
619
|
import { join as join2 } from "path";
|
|
739
620
|
import { parse as parseYaml2 } from "yaml";
|
|
740
621
|
import { z as z2 } from "zod";
|
|
@@ -786,7 +667,7 @@ function createPolicySignal(options) {
|
|
|
786
667
|
}
|
|
787
668
|
async function readPolicyFile(path) {
|
|
788
669
|
try {
|
|
789
|
-
const raw = await
|
|
670
|
+
const raw = await readFile3(path, "utf8");
|
|
790
671
|
return policyFileSchema.parse(parseYaml2(raw) ?? {});
|
|
791
672
|
} catch (error) {
|
|
792
673
|
if (error.code === "ENOENT") return { rules: [] };
|
|
@@ -822,10 +703,17 @@ async function assessRisk(input) {
|
|
|
822
703
|
};
|
|
823
704
|
}
|
|
824
705
|
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
706
|
+
const churn = (await readConfigFile(repositoryRoot).catch(() => ({ risk: void 0 }))).risk;
|
|
825
707
|
const signals = input.signals ?? [
|
|
826
708
|
createIncidentSignal({ store: memoryStore }),
|
|
827
709
|
createPolicySignal({ repositoryRoot }),
|
|
828
|
-
...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
|
+
]
|
|
829
717
|
];
|
|
830
718
|
const assessment = await new RiskAssessor(signals).assess({
|
|
831
719
|
paths,
|
|
@@ -859,15 +747,25 @@ async function resolveRiskPaths(input) {
|
|
|
859
747
|
}
|
|
860
748
|
const realRoot = await realpath2(input.repositoryRoot).catch(() => resolve3(input.repositoryRoot));
|
|
861
749
|
const inside = await Promise.all(
|
|
862
|
-
[...paths].map(
|
|
863
|
-
const real = await realpath2(resolve3(input.repositoryRoot, path)).catch(() => null);
|
|
864
|
-
return real === null || real === realRoot || real.startsWith(`${realRoot}${sep2}`);
|
|
865
|
-
})
|
|
750
|
+
[...paths].map((path) => realPathInside(realRoot, resolve3(input.repositoryRoot, path)))
|
|
866
751
|
);
|
|
867
752
|
const resolved = [...paths].filter((_, index) => inside[index]).sort();
|
|
868
753
|
return resolved.length > MAX_RESOLVED_PATHS ? resolved.slice(0, MAX_RESOLVED_PATHS) : resolved;
|
|
869
754
|
}
|
|
870
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
|
+
}
|
|
871
769
|
function containedRelativePath(root, path) {
|
|
872
770
|
const trimmed = normalizePath2(path);
|
|
873
771
|
if (!trimmed || trimmed.includes("\0")) return null;
|
|
@@ -878,16 +776,18 @@ function containedRelativePath(root, path) {
|
|
|
878
776
|
}
|
|
879
777
|
async function changedGitPaths(repositoryRoot) {
|
|
880
778
|
const [tracked, untracked] = await Promise.all([
|
|
881
|
-
|
|
882
|
-
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"])
|
|
883
782
|
]);
|
|
884
|
-
return [...tracked.stdout.split("\
|
|
783
|
+
return [...tracked.stdout.split("\0"), ...untracked.stdout.split("\0")].filter((path) => path && !path.split("/").some((segment) => segment.startsWith(".")));
|
|
885
784
|
}
|
|
886
785
|
function runGit2(cwd, args) {
|
|
887
786
|
return new Promise((resolveResult, reject) => {
|
|
888
787
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
889
788
|
let stdout = "";
|
|
890
789
|
let stderr = "";
|
|
790
|
+
child.stdout.setEncoding("utf8");
|
|
891
791
|
child.stdout.on("data", (chunk) => {
|
|
892
792
|
stdout += String(chunk);
|
|
893
793
|
});
|
|
@@ -899,13 +799,206 @@ function runGit2(cwd, args) {
|
|
|
899
799
|
});
|
|
900
800
|
}
|
|
901
801
|
function normalizePath2(path) {
|
|
902
|
-
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
802
|
+
return (sep2 === "\\" ? path.replace(/\\/g, "/") : path).replace(/^\.\//, "");
|
|
903
803
|
}
|
|
904
804
|
|
|
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";
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
var PlanNotFoundError = class extends Error {
|
|
821
|
+
constructor(id) {
|
|
822
|
+
super(`Plan ${id} not found.`);
|
|
823
|
+
this.name = "PlanNotFoundError";
|
|
824
|
+
}
|
|
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;
|
|
834
|
+
}
|
|
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;
|
|
853
|
+
}
|
|
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);
|
|
879
|
+
}
|
|
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
|
+
});
|
|
891
|
+
}
|
|
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
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
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);
|
|
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
|
|
993
|
+
});
|
|
994
|
+
return this.store.readPlan(next.id);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
905
998
|
// src/worklog/store.ts
|
|
906
999
|
import { createHash } from "crypto";
|
|
907
|
-
import { mkdir as mkdir2, readdir as readdir2, readFile as
|
|
908
|
-
import { dirname as
|
|
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";
|
|
909
1002
|
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
910
1003
|
import { z as z3 } from "zod";
|
|
911
1004
|
var LIMITS = {
|
|
@@ -958,7 +1051,7 @@ var WorklogStore = class {
|
|
|
958
1051
|
if (existing) return existing.id;
|
|
959
1052
|
const id = generateEntityId("wlg", now.getTime());
|
|
960
1053
|
const path = this.entryPath(id);
|
|
961
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
1054
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname3(path));
|
|
962
1055
|
await mkdir2(this.directory, { recursive: true, mode: 448 });
|
|
963
1056
|
const frontMatter = stringifyYaml2({
|
|
964
1057
|
schemaVersion: 1,
|
|
@@ -981,7 +1074,7 @@ var WorklogStore = class {
|
|
|
981
1074
|
async read(id) {
|
|
982
1075
|
const path = this.entryPath(id);
|
|
983
1076
|
await assertNoSymlinkBetween(this.repositoryRoot, path);
|
|
984
|
-
const parsed = parseFrontMatter(await
|
|
1077
|
+
const parsed = parseFrontMatter(await readFile4(path, "utf8"));
|
|
985
1078
|
return {
|
|
986
1079
|
...entrySchema.parse(parseYaml3(parsed.frontMatter)),
|
|
987
1080
|
summary: parsed.content.trim(),
|
|
@@ -1319,11 +1412,13 @@ async function buildBeforeEditContext(input) {
|
|
|
1319
1412
|
const mapPromise = loadProjectArchitectureMap(repositoryRoot, manifest);
|
|
1320
1413
|
const discovery = !hasExplicitTargets && input.task ? await discoverTaskPaths(repositoryRoot, input.task, manifest, mapPromise) : { paths: [], hits: [], explain: [] };
|
|
1321
1414
|
const taskPaths = discovery.paths;
|
|
1322
|
-
const
|
|
1415
|
+
const askedTask = Boolean(input.task?.trim());
|
|
1416
|
+
const targetPaths = hasExplicitTargets ? resolvedPaths : taskPaths.length > 0 ? taskPaths : askedTask && input.useGit !== true ? [] : resolvedPaths;
|
|
1323
1417
|
const discoveredFromTask = taskPaths.length > 0;
|
|
1324
1418
|
const pathSource = classifyPathSource(input, targetPaths.length, discoveredFromTask);
|
|
1325
1419
|
const planStore = new PlanFileStore(repositoryRoot);
|
|
1326
1420
|
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
1421
|
+
const knownSecrets = await loadKnownSecretValues(repositoryRoot);
|
|
1327
1422
|
const [referencedPlan, policy, policyFile, incidents, facts, risks, map, diffs, handoffs] = await Promise.all([
|
|
1328
1423
|
resolveRelevantPlan(planStore, namespace, input.planId),
|
|
1329
1424
|
loadPlanPolicy(repositoryRoot),
|
|
@@ -1338,7 +1433,7 @@ async function buildBeforeEditContext(input) {
|
|
|
1338
1433
|
useGit: !hasExplicitTargets && input.useGit !== false
|
|
1339
1434
|
}),
|
|
1340
1435
|
mapPromise,
|
|
1341
|
-
input.useGit === false ? Promise.resolve([]) : readGitDiffs(repositoryRoot, targetPaths),
|
|
1436
|
+
input.useGit === false ? Promise.resolve([]) : readGitDiffs(repositoryRoot, targetPaths, knownSecrets),
|
|
1342
1437
|
recallHandoffs(repositoryRoot, {
|
|
1343
1438
|
namespace,
|
|
1344
1439
|
paths: targetPaths,
|
|
@@ -1354,6 +1449,11 @@ async function buildBeforeEditContext(input) {
|
|
|
1354
1449
|
);
|
|
1355
1450
|
}
|
|
1356
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
|
+
}
|
|
1357
1457
|
const plan = summarizePlan(referencedPlan);
|
|
1358
1458
|
if (plan) explain.push(`plan: ${plan.id} (${plan.status})`);
|
|
1359
1459
|
const policyRules = summarizeMatchingPolicyRules(policyFile.rules, targetPaths);
|
|
@@ -1376,7 +1476,8 @@ async function buildBeforeEditContext(input) {
|
|
|
1376
1476
|
input.task ?? "",
|
|
1377
1477
|
discovery.hits,
|
|
1378
1478
|
"symbols" in discovery ? discovery.symbols ?? {} : {},
|
|
1379
|
-
input.evidence ?? "overview"
|
|
1479
|
+
input.evidence ?? "overview",
|
|
1480
|
+
knownSecrets
|
|
1380
1481
|
);
|
|
1381
1482
|
if (symbols.length > 0) {
|
|
1382
1483
|
const spans = symbols.reduce((total, file) => total + (file.spans?.length ?? 0), 0);
|
|
@@ -1543,7 +1644,7 @@ function computeNeighbours(map, paths) {
|
|
|
1543
1644
|
}
|
|
1544
1645
|
return result;
|
|
1545
1646
|
}
|
|
1546
|
-
async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, picked = {}, evidence = "overview") {
|
|
1647
|
+
async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, picked = {}, evidence = "overview", knownSecrets = []) {
|
|
1547
1648
|
const terms = taskTerms(task);
|
|
1548
1649
|
const tokens = identifierTokens(task);
|
|
1549
1650
|
const result = [];
|
|
@@ -1552,9 +1653,9 @@ async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, pic
|
|
|
1552
1653
|
for (const path of targetPaths.slice(0, LIMITS2.symbolFiles)) {
|
|
1553
1654
|
const source = await readBoundedSource(repositoryRoot, path);
|
|
1554
1655
|
if (source === null) continue;
|
|
1555
|
-
const lines = source.split(/\r?\n/);
|
|
1656
|
+
const lines = redactSourceText(source, knownSecrets).split(/\r?\n/);
|
|
1556
1657
|
const fileHits = hits.filter((hit2) => hit2.path === path && hit2.endLine <= lines.length);
|
|
1557
|
-
const table = supportsSymbols(path) ? extractSymbolTable(source) : [];
|
|
1658
|
+
const table = supportsSymbols(path) ? extractSymbolTable(source, path) : [];
|
|
1558
1659
|
const credited = /* @__PURE__ */ new Map();
|
|
1559
1660
|
for (const hit2 of fileHits) {
|
|
1560
1661
|
const owner = table.filter((symbol) => hit2.startLine <= symbol.endLine && hit2.endLine >= symbol.startLine).sort(
|
|
@@ -1570,7 +1671,9 @@ async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, pic
|
|
|
1570
1671
|
}).sort(
|
|
1571
1672
|
(left, right) => right.score - left.score || left.symbol.startLine - right.symbol.startLine
|
|
1572
1673
|
);
|
|
1573
|
-
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
|
+
);
|
|
1574
1677
|
const best = ranked[0]?.score ? ranked[0] : void 0;
|
|
1575
1678
|
const hit = best ? best.hit : fileHits[0];
|
|
1576
1679
|
const range = best?.symbol ?? hit;
|
|
@@ -1639,20 +1742,38 @@ function headInSignature(lines, symbol) {
|
|
|
1639
1742
|
const line = (lines[symbol.startLine - 1] ?? "").trim();
|
|
1640
1743
|
return !head.endsWith("\u2026") && line.startsWith(head) && /^[\s{=>(]*$/.test(line.slice(head.length));
|
|
1641
1744
|
}
|
|
1642
|
-
async function readGitDiffs(repositoryRoot, paths) {
|
|
1745
|
+
async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
|
|
1643
1746
|
if (paths.length === 0) return [];
|
|
1644
1747
|
const sections = [];
|
|
1645
1748
|
try {
|
|
1646
1749
|
const { stdout } = await execFileAsync(
|
|
1647
1750
|
"git",
|
|
1648
|
-
[
|
|
1751
|
+
[
|
|
1752
|
+
"-c",
|
|
1753
|
+
"core.quotePath=false",
|
|
1754
|
+
"diff",
|
|
1755
|
+
"HEAD",
|
|
1756
|
+
"--no-ext-diff",
|
|
1757
|
+
"--unified=3",
|
|
1758
|
+
"--",
|
|
1759
|
+
...paths
|
|
1760
|
+
],
|
|
1649
1761
|
{ cwd: repositoryRoot, maxBuffer: 2e6 }
|
|
1650
1762
|
);
|
|
1651
1763
|
sections.push(stdout);
|
|
1652
1764
|
} catch {
|
|
1653
1765
|
for (const args of [
|
|
1654
|
-
[
|
|
1655
|
-
|
|
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]
|
|
1656
1777
|
]) {
|
|
1657
1778
|
try {
|
|
1658
1779
|
const { stdout } = await execFileAsync("git", args, {
|
|
@@ -1664,21 +1785,59 @@ async function readGitDiffs(repositoryRoot, paths) {
|
|
|
1664
1785
|
}
|
|
1665
1786
|
}
|
|
1666
1787
|
}
|
|
1667
|
-
const parsed = parseGitDiff(sections.join("\n"));
|
|
1788
|
+
const parsed = parseGitDiff(sections.join("\n"), knownSecrets);
|
|
1668
1789
|
const known = new Set(parsed.map((diff) => diff.path));
|
|
1669
1790
|
const untracked = await listUntrackedPaths(repositoryRoot, paths);
|
|
1670
1791
|
for (const path of untracked) {
|
|
1671
1792
|
if (known.has(path)) continue;
|
|
1672
|
-
const patch = await buildUntrackedPatch(repositoryRoot, path);
|
|
1793
|
+
const patch = isCredentialPath(path) ? withheldPatch(path) : await buildUntrackedPatch(repositoryRoot, path, knownSecrets);
|
|
1673
1794
|
if (patch) parsed.push(patch);
|
|
1674
1795
|
}
|
|
1675
1796
|
return parsed.slice(0, MAX_DIFF_FILES);
|
|
1676
1797
|
}
|
|
1677
|
-
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 = []) {
|
|
1678
1834
|
return diff.split(/(?=^diff --git )/m).filter((section) => section.startsWith("diff --git ")).flatMap((section) => {
|
|
1679
|
-
const header = section
|
|
1835
|
+
const header = diffHeaderPaths(section);
|
|
1680
1836
|
if (!header?.[2]) return [];
|
|
1681
|
-
|
|
1837
|
+
if (isCredentialPath(header[1] ?? "") || isCredentialPath(header[2])) {
|
|
1838
|
+
return [withheldPatch(header[2])];
|
|
1839
|
+
}
|
|
1840
|
+
const rawPatch = redactSourceText(section.trim(), knownSecrets, true);
|
|
1682
1841
|
const patch = rawPatch.slice(0, MAX_DIFF_CHARS_PER_FILE).trim();
|
|
1683
1842
|
return patch ? [
|
|
1684
1843
|
{
|
|
@@ -1693,20 +1852,20 @@ async function listUntrackedPaths(repositoryRoot, paths) {
|
|
|
1693
1852
|
try {
|
|
1694
1853
|
const { stdout } = await execFileAsync(
|
|
1695
1854
|
"git",
|
|
1696
|
-
["ls-files", "--others", "--exclude-standard", "--", ...paths],
|
|
1855
|
+
["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths],
|
|
1697
1856
|
{ cwd: repositoryRoot, maxBuffer: 1e6 }
|
|
1698
1857
|
);
|
|
1699
|
-
return stdout.split(
|
|
1858
|
+
return stdout.split("\0").filter(Boolean);
|
|
1700
1859
|
} catch {
|
|
1701
1860
|
return [];
|
|
1702
1861
|
}
|
|
1703
1862
|
}
|
|
1704
|
-
async function buildUntrackedPatch(repositoryRoot, path) {
|
|
1863
|
+
async function buildUntrackedPatch(repositoryRoot, path, knownSecrets) {
|
|
1705
1864
|
try {
|
|
1706
|
-
const content = await
|
|
1707
|
-
if (content
|
|
1865
|
+
const content = await readBoundedSource(repositoryRoot, path);
|
|
1866
|
+
if (content === null) return null;
|
|
1708
1867
|
const lines = content.split(/\r?\n/);
|
|
1709
|
-
const body = lines.map((line) => `+${line}`).join("\n");
|
|
1868
|
+
const body = redactSourceText(lines.map((line) => `+${line}`).join("\n"), knownSecrets, true);
|
|
1710
1869
|
const rawPatch = [
|
|
1711
1870
|
`diff --git a/${path} b/${path}`,
|
|
1712
1871
|
"new file mode 100644",
|
|
@@ -1750,11 +1909,27 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
1750
1909
|
const incidentText = new Set(
|
|
1751
1910
|
eligible.filter((fact) => fact.category === "incident").map((fact) => normalizeText(fact.content || fact.object))
|
|
1752
1911
|
);
|
|
1753
|
-
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) => {
|
|
1754
1914
|
const text = `${fact.subject} ${fact.predicate} ${fact.object} ${fact.content}`;
|
|
1755
|
-
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;
|
|
1756
1923
|
const pathScore = fact.paths.some((path) => targetPaths.includes(path)) ? 5 : 0;
|
|
1757
|
-
|
|
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
|
+
};
|
|
1758
1933
|
});
|
|
1759
1934
|
const summarize = (fact, relevance, reason) => ({
|
|
1760
1935
|
id: fact.id,
|
|
@@ -1830,10 +2005,10 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
1830
2005
|
}
|
|
1831
2006
|
selected.push(candidate);
|
|
1832
2007
|
}
|
|
1833
|
-
return selected.map(({ fact, taskScore, pathScore, score }) => {
|
|
2008
|
+
return selected.map(({ fact, taskScore, pathScore, score, admitted }) => {
|
|
1834
2009
|
const evidence = [
|
|
1835
2010
|
pathScore > 0 ? "matches a target path" : "",
|
|
1836
|
-
taskScore > 0 ? `shares ${taskScore / 3} task term(s)` : "",
|
|
2011
|
+
admitted && taskScore > 0 ? `shares ${taskScore / 3} task term(s)` : "",
|
|
1837
2012
|
cosine(fact.id) !== void 0 ? `semantic ${cosine(fact.id)?.toFixed(2)}` : ""
|
|
1838
2013
|
].filter(Boolean);
|
|
1839
2014
|
return summarize(
|
|
@@ -1844,12 +2019,6 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
1844
2019
|
);
|
|
1845
2020
|
});
|
|
1846
2021
|
}
|
|
1847
|
-
function relevanceScore(value, task) {
|
|
1848
|
-
const terms = lexicalTerms(task).filter((term) => term.length > 2);
|
|
1849
|
-
if (terms.length === 0) return 0;
|
|
1850
|
-
const haystack = new Set(value.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []);
|
|
1851
|
-
return terms.filter((term) => haystack.has(term)).length * 3;
|
|
1852
|
-
}
|
|
1853
2022
|
function normalizeText(value) {
|
|
1854
2023
|
return value.toLowerCase().replace(/[^a-z0-9_./-]+/g, " ").trim();
|
|
1855
2024
|
}
|
|
@@ -1896,6 +2065,21 @@ var RULES = [
|
|
|
1896
2065
|
confidence: 0.8,
|
|
1897
2066
|
regex: EXPLICIT_RESOLUTION
|
|
1898
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
|
+
},
|
|
1899
2083
|
{
|
|
1900
2084
|
class: "decision",
|
|
1901
2085
|
subject: "decision",
|
|
@@ -1915,11 +2099,20 @@ var RULES = [
|
|
|
1915
2099
|
subject: "project",
|
|
1916
2100
|
predicate: "fact",
|
|
1917
2101
|
confidence: 0.55,
|
|
1918
|
-
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
|
|
1919
2103
|
}
|
|
1920
2104
|
];
|
|
1921
2105
|
var INCIDENT_REGEX = RULES.find((rule) => rule.predicate === "occurred").regex;
|
|
1922
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
|
+
}
|
|
1923
2116
|
var NEGATION = /\b(no|not|never|none|nothing|nobody|without|zero|neither|nor|cannot)\b(?!\s+longer)|\w+n['’]t\b/i;
|
|
1924
2117
|
var EXAMPLE = /\b(?:e\.g|i\.e|for example|for instance|such as|example|sample|hypothetical)\b/i;
|
|
1925
2118
|
var QUOTED = /"[^"]*"|\u201c[^\u201d]*\u201d|`[^`]*`/g;
|
|
@@ -1946,17 +2139,19 @@ var SEVERITY_MARKERS = [
|
|
|
1946
2139
|
function deterministicExtract(input) {
|
|
1947
2140
|
const changedFiles = (input.changedFiles ?? []).filter(Boolean);
|
|
1948
2141
|
const candidates = [];
|
|
1949
|
-
for (const sentence of splitSentences(input.summary)) {
|
|
2142
|
+
for (const sentence of splitSentences(input.summary).map(stripDecoration)) {
|
|
1950
2143
|
const classified = classify(sentence);
|
|
1951
2144
|
const doubt = classified?.class === "incident" ? incidentDoubt(sentence) : null;
|
|
1952
|
-
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 ? {
|
|
1953
2147
|
class: "note",
|
|
1954
2148
|
subject: "speculation",
|
|
1955
2149
|
predicate: "consider",
|
|
1956
2150
|
confidence: 0.3,
|
|
1957
2151
|
regex: SPECULATIVE
|
|
1958
2152
|
} : classified;
|
|
1959
|
-
|
|
2153
|
+
const passing = (rule?.class === "decision" || rule?.class === "fact") && NOT_DURABLE.test(sentence);
|
|
2154
|
+
if (!rule || passing) {
|
|
1960
2155
|
candidates.push(ignoredCandidate(sentence));
|
|
1961
2156
|
continue;
|
|
1962
2157
|
}
|
|
@@ -2094,7 +2289,7 @@ function matchResolution(fixText, pathCandidates, unresolved) {
|
|
|
2094
2289
|
|
|
2095
2290
|
// src/memory-extract/review-store.ts
|
|
2096
2291
|
import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, rename, rm as rm2 } from "fs/promises";
|
|
2097
|
-
import { dirname as
|
|
2292
|
+
import { dirname as dirname4, join as join5, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
|
|
2098
2293
|
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
2099
2294
|
import { z as z5 } from "zod";
|
|
2100
2295
|
var reviewSchema = z5.object({
|
|
@@ -2137,7 +2332,7 @@ var ReviewStore = class {
|
|
|
2137
2332
|
const input = redactPersisted(raw);
|
|
2138
2333
|
const path = this.itemPath(input.id);
|
|
2139
2334
|
await this.prepareDirectory();
|
|
2140
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
2335
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname4(path));
|
|
2141
2336
|
const frontMatter = stringifyYaml3({
|
|
2142
2337
|
schemaVersion: 1,
|
|
2143
2338
|
id: input.id,
|
|
@@ -2232,7 +2427,7 @@ var ReviewStore = class {
|
|
|
2232
2427
|
return absolute;
|
|
2233
2428
|
}
|
|
2234
2429
|
async prepareDirectory() {
|
|
2235
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
2430
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname4(this.reviewDirectory));
|
|
2236
2431
|
await mkdir3(this.reviewDirectory, { recursive: true, mode: 448 });
|
|
2237
2432
|
}
|
|
2238
2433
|
};
|
|
@@ -2243,7 +2438,8 @@ var INJECTED_BLOCK = /^##\s*CodeBuddy\b/m;
|
|
|
2243
2438
|
var FENCED_CODE = /```[\s\S]*?(?:```|$)/g;
|
|
2244
2439
|
var QUOTE_LINE = /^\s*>.*$/gm;
|
|
2245
2440
|
var RULE_PREFIX = /^(?:also,?\s+|please,?\s+)?(?:from now on|going forward|(?:a\s+)?rule\b[^:.]{0,60}:)[,:]?\s*/i;
|
|
2246
|
-
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;
|
|
2247
2443
|
var QUESTION_START = /^(?:why|how|what|when|where|which|who|can|could|would|will|do|does|did|is|are|should)\b/i;
|
|
2248
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;
|
|
2249
2445
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
@@ -2271,21 +2467,31 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
2271
2467
|
function extractUserRules(prompts) {
|
|
2272
2468
|
const rules = /* @__PURE__ */ new Map();
|
|
2273
2469
|
const revocations = /* @__PURE__ */ new Map();
|
|
2470
|
+
const policies = /* @__PURE__ */ new Map();
|
|
2274
2471
|
for (const prompt of prompts) {
|
|
2275
2472
|
const own = (prompt.split(INJECTED_BLOCK)[0] ?? "").replace(FENCED_CODE, "\n").replace(QUOTE_LINE, "");
|
|
2276
2473
|
for (const raw of own.split(/(?<=[.!?])\s+|\n+/)) {
|
|
2277
|
-
const sentence = raw.trim();
|
|
2474
|
+
const sentence = stripDecoration(raw.trim());
|
|
2278
2475
|
if (!sentence || sentence.length > MAX_RULE_CHARS || sentence.endsWith("?")) continue;
|
|
2279
2476
|
if (QUESTION_START.test(sentence)) continue;
|
|
2280
2477
|
const key = sentence.toLowerCase().replace(/\s+/g, " ").replace(/[.!]+$/, "");
|
|
2281
2478
|
if (REVOCATION_MARKER.test(sentence)) {
|
|
2282
2479
|
if (!revocations.has(key)) revocations.set(key, sentence);
|
|
2283
|
-
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
if (HEDGED.test(sentence) || NOT_DURABLE.test(sentence)) continue;
|
|
2483
|
+
if (isRule(sentence)) {
|
|
2284
2484
|
if (!rules.has(key)) rules.set(key, sentence);
|
|
2485
|
+
} else if (POLICY.test(sentence) && !policies.has(key)) {
|
|
2486
|
+
policies.set(key, sentence);
|
|
2285
2487
|
}
|
|
2286
2488
|
}
|
|
2287
2489
|
}
|
|
2288
|
-
return {
|
|
2490
|
+
return {
|
|
2491
|
+
rules: [...rules.values()],
|
|
2492
|
+
revocations: [...revocations.values()],
|
|
2493
|
+
policies: [...policies.values()]
|
|
2494
|
+
};
|
|
2289
2495
|
}
|
|
2290
2496
|
function isRule(sentence) {
|
|
2291
2497
|
const rest = sentence.replace(RULE_PREFIX, "");
|
|
@@ -2307,6 +2513,7 @@ function revokes(revocation, rule) {
|
|
|
2307
2513
|
// src/memory-extract/engine.ts
|
|
2308
2514
|
var AUTO_SAVE_THRESHOLD = 0.7;
|
|
2309
2515
|
var STATED_CONFIDENCE = 0.9;
|
|
2516
|
+
var POLICY_CONFIDENCE = 0.6;
|
|
2310
2517
|
async function captureAfterTurn(input, options = {}) {
|
|
2311
2518
|
const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
|
|
2312
2519
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -2316,7 +2523,8 @@ async function captureAfterTurn(input, options = {}) {
|
|
|
2316
2523
|
const source = extracted.source;
|
|
2317
2524
|
const candidates = [
|
|
2318
2525
|
...extracted.candidates,
|
|
2319
|
-
...(input.userRules ?? []).map(ruleCandidate),
|
|
2526
|
+
...(input.userRules ?? []).map((rule) => ruleCandidate(rule)),
|
|
2527
|
+
...(input.userPolicies ?? []).map((policy) => ruleCandidate(policy, POLICY_CONFIDENCE)),
|
|
2320
2528
|
...await procedureCandidates(input, repositoryRoot)
|
|
2321
2529
|
];
|
|
2322
2530
|
const saved = [];
|
|
@@ -2391,14 +2599,14 @@ async function recordWorklog(input, saved, queued, repositoryRoot, now, reasons)
|
|
|
2391
2599
|
return null;
|
|
2392
2600
|
}
|
|
2393
2601
|
}
|
|
2394
|
-
function ruleCandidate(rule) {
|
|
2602
|
+
function ruleCandidate(rule, confidence = STATED_CONFIDENCE) {
|
|
2395
2603
|
return {
|
|
2396
2604
|
class: "decision",
|
|
2397
2605
|
content: rule,
|
|
2398
2606
|
subject: "rule",
|
|
2399
2607
|
predicate: "rule",
|
|
2400
2608
|
object: rule,
|
|
2401
|
-
confidence
|
|
2609
|
+
confidence,
|
|
2402
2610
|
paths: [],
|
|
2403
2611
|
severity: null
|
|
2404
2612
|
};
|
|
@@ -2720,50 +2928,6 @@ async function approveReviewItem(id, options = {}) {
|
|
|
2720
2928
|
return factId;
|
|
2721
2929
|
}
|
|
2722
2930
|
|
|
2723
|
-
// src/tracing/log.ts
|
|
2724
|
-
import { appendFile, mkdir as mkdir4, readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
2725
|
-
import { dirname as dirname4, join as join6 } from "path";
|
|
2726
|
-
var MAX_BYTES = 4 * 1024 * 1024;
|
|
2727
|
-
function tracePath(repositoryRoot) {
|
|
2728
|
-
return join6(repositoryRoot, ".codebuddy", "cache", "traces.jsonl");
|
|
2729
|
-
}
|
|
2730
|
-
async function recordTrace(repositoryRoot, event) {
|
|
2731
|
-
if (process.env.CODEBUDDY_TRACE === "0") return;
|
|
2732
|
-
const path = tracePath(repositoryRoot);
|
|
2733
|
-
try {
|
|
2734
|
-
await mkdir4(dirname4(path), { recursive: true });
|
|
2735
|
-
await appendFile(path, `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event })}
|
|
2736
|
-
`);
|
|
2737
|
-
if ((await stat3(path)).size > MAX_BYTES) {
|
|
2738
|
-
const lines = (await readFile6(path, "utf8")).split("\n").filter(Boolean);
|
|
2739
|
-
await writeAtomic(path, `${lines.slice(Math.floor(lines.length / 2)).join("\n")}
|
|
2740
|
-
`);
|
|
2741
|
-
}
|
|
2742
|
-
} catch {
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
async function readTraces(repositoryRoot, limit = 2e3) {
|
|
2746
|
-
let raw;
|
|
2747
|
-
try {
|
|
2748
|
-
raw = await readFile6(tracePath(repositoryRoot), "utf8");
|
|
2749
|
-
} catch {
|
|
2750
|
-
return [];
|
|
2751
|
-
}
|
|
2752
|
-
return parseTraceLines(raw).slice(-limit);
|
|
2753
|
-
}
|
|
2754
|
-
function parseTraceLines(raw) {
|
|
2755
|
-
const events = [];
|
|
2756
|
-
for (const line of raw.split("\n")) {
|
|
2757
|
-
if (!line.trim()) continue;
|
|
2758
|
-
try {
|
|
2759
|
-
const event = JSON.parse(line);
|
|
2760
|
-
if (typeof event.ts === "string" && typeof event.name === "string") events.push(event);
|
|
2761
|
-
} catch {
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
return events;
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
2931
|
// src/hooks/semantic.ts
|
|
2768
2932
|
import pTimeout from "p-timeout";
|
|
2769
2933
|
var CONNECT_BUDGET_MS = 2500;
|
|
@@ -2779,7 +2943,7 @@ async function configuredReranker(repositoryRoot) {
|
|
|
2779
2943
|
if (provider === "local") {
|
|
2780
2944
|
const revision = model === DEFAULT_RERANK_MODELS.local.id ? DEFAULT_RERANK_MODELS.local.revision : void 0;
|
|
2781
2945
|
return async (task, documents) => {
|
|
2782
|
-
const { rerankLocally } = await import("./local-
|
|
2946
|
+
const { rerankLocally } = await import("./local-ST4WMRCG.js");
|
|
2783
2947
|
return bounded(
|
|
2784
2948
|
rerankLocally({ id: model, ...revision ? { revision } : {} }, task, documents)
|
|
2785
2949
|
);
|
|
@@ -2787,7 +2951,7 @@ async function configuredReranker(repositoryRoot) {
|
|
|
2787
2951
|
}
|
|
2788
2952
|
const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file.provider?.apiKey;
|
|
2789
2953
|
if (!apiKey) return void 0;
|
|
2790
|
-
const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-
|
|
2954
|
+
const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-LXZUR3U2.js");
|
|
2791
2955
|
const call = provider === "openrouter" ? rerankOpenRouter : rerankHuggingFace;
|
|
2792
2956
|
return (task, documents) => call({ apiKey, model, timeoutMs: RERANK_BUDGET_MS }, task, documents);
|
|
2793
2957
|
}
|
|
@@ -2796,7 +2960,7 @@ async function openHookSemantics(repositoryRoot) {
|
|
|
2796
2960
|
const connect = async () => {
|
|
2797
2961
|
const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
|
|
2798
2962
|
if (!config.postgresUrl) return null;
|
|
2799
|
-
const { createRuntime } = await import("./operations-
|
|
2963
|
+
const { createRuntime } = await import("./operations-7BTAAUHF.js");
|
|
2800
2964
|
const opening = createRuntime(config, {
|
|
2801
2965
|
repositoryRoot,
|
|
2802
2966
|
startWorker: false,
|
|
@@ -2831,20 +2995,20 @@ async function openHookSemantics(repositoryRoot) {
|
|
|
2831
2995
|
}
|
|
2832
2996
|
|
|
2833
2997
|
// src/hooks/staging.ts
|
|
2834
|
-
import { mkdir as
|
|
2835
|
-
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";
|
|
2836
3000
|
var STAGING_LOCK_STALE_MS = 1e4;
|
|
2837
3001
|
var HookStagingStore = class {
|
|
2838
3002
|
directory;
|
|
2839
3003
|
operations = Promise.resolve();
|
|
2840
3004
|
constructor(repositoryRoot = process.cwd()) {
|
|
2841
|
-
this.directory =
|
|
3005
|
+
this.directory = join6(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
|
|
2842
3006
|
}
|
|
2843
3007
|
async add(sessionId, paths) {
|
|
2844
3008
|
await this.serialize(async () => {
|
|
2845
3009
|
const clean = paths.map(normalize2).filter(Boolean);
|
|
2846
3010
|
if (clean.length === 0) return;
|
|
2847
|
-
await
|
|
3011
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
2848
3012
|
await this.withFileLock(sessionId, async () => {
|
|
2849
3013
|
const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean])].sort();
|
|
2850
3014
|
await writeAtomic(
|
|
@@ -2857,7 +3021,7 @@ var HookStagingStore = class {
|
|
|
2857
3021
|
}
|
|
2858
3022
|
async read(sessionId) {
|
|
2859
3023
|
try {
|
|
2860
|
-
const parsed = JSON.parse(await
|
|
3024
|
+
const parsed = JSON.parse(await readFile6(this.filePath(sessionId), "utf8"));
|
|
2861
3025
|
return Array.isArray(parsed?.paths) ? parsed.paths.filter((p) => typeof p === "string") : [];
|
|
2862
3026
|
} catch {
|
|
2863
3027
|
return [];
|
|
@@ -2866,7 +3030,7 @@ var HookStagingStore = class {
|
|
|
2866
3030
|
/** Return the staged paths and clear them. */
|
|
2867
3031
|
async drain(sessionId) {
|
|
2868
3032
|
return this.serialize(async () => {
|
|
2869
|
-
await
|
|
3033
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
2870
3034
|
return this.withFileLock(sessionId, async () => {
|
|
2871
3035
|
const path = this.filePath(sessionId);
|
|
2872
3036
|
const draining = `${path}.${process.pid}.${Date.now()}.drain`;
|
|
@@ -2877,7 +3041,7 @@ var HookStagingStore = class {
|
|
|
2877
3041
|
throw error;
|
|
2878
3042
|
}
|
|
2879
3043
|
try {
|
|
2880
|
-
const parsed = JSON.parse(await
|
|
3044
|
+
const parsed = JSON.parse(await readFile6(draining, "utf8"));
|
|
2881
3045
|
return Array.isArray(parsed?.paths) ? parsed.paths.filter((item) => typeof item === "string") : [];
|
|
2882
3046
|
} finally {
|
|
2883
3047
|
await rm3(draining, { force: true });
|
|
@@ -2887,15 +3051,15 @@ var HookStagingStore = class {
|
|
|
2887
3051
|
}
|
|
2888
3052
|
/** Claim a batch without consuming edits staged while capture runs. */
|
|
2889
3053
|
async claim(sessionId) {
|
|
2890
|
-
await
|
|
3054
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
2891
3055
|
const captureLock = `${this.filePath(sessionId)}.capture-lock`;
|
|
2892
3056
|
for (let attempt = 0; ; attempt++) {
|
|
2893
3057
|
try {
|
|
2894
|
-
await
|
|
3058
|
+
await mkdir4(captureLock);
|
|
2895
3059
|
break;
|
|
2896
3060
|
} catch (error) {
|
|
2897
3061
|
if (error.code !== "EEXIST") throw error;
|
|
2898
|
-
const age = Date.now() - (await
|
|
3062
|
+
const age = Date.now() - (await stat3(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
|
|
2899
3063
|
if (age > 6e4) {
|
|
2900
3064
|
await rm3(captureLock, { recursive: true, force: true });
|
|
2901
3065
|
continue;
|
|
@@ -2908,13 +3072,13 @@ var HookStagingStore = class {
|
|
|
2908
3072
|
return await this.withFileLock(sessionId, async () => {
|
|
2909
3073
|
const claimed = this.claimPath(sessionId);
|
|
2910
3074
|
try {
|
|
2911
|
-
return parsePaths(await
|
|
3075
|
+
return parsePaths(await readFile6(claimed, "utf8"));
|
|
2912
3076
|
} catch (error) {
|
|
2913
3077
|
if (error.code !== "ENOENT") throw error;
|
|
2914
3078
|
}
|
|
2915
3079
|
try {
|
|
2916
3080
|
await rename2(this.filePath(sessionId), claimed);
|
|
2917
|
-
return parsePaths(await
|
|
3081
|
+
return parsePaths(await readFile6(claimed, "utf8"));
|
|
2918
3082
|
} catch (error) {
|
|
2919
3083
|
if (error.code === "ENOENT") return [];
|
|
2920
3084
|
throw error;
|
|
@@ -2935,7 +3099,7 @@ var HookStagingStore = class {
|
|
|
2935
3099
|
/** Restore the claimed batch for retry, merging edits that arrived during capture. */
|
|
2936
3100
|
async rollback(sessionId) {
|
|
2937
3101
|
await this.withFileLock(sessionId, async () => {
|
|
2938
|
-
const claimed = await
|
|
3102
|
+
const claimed = await readFile6(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
|
|
2939
3103
|
const merged = [.../* @__PURE__ */ new Set([...claimed, ...await this.read(sessionId)])].sort();
|
|
2940
3104
|
if (merged.length > 0)
|
|
2941
3105
|
await writeAtomic(
|
|
@@ -2954,11 +3118,11 @@ var HookStagingStore = class {
|
|
|
2954
3118
|
const lock = `${this.filePath(sessionId)}.lock`;
|
|
2955
3119
|
for (let attempt = 0; ; attempt++) {
|
|
2956
3120
|
try {
|
|
2957
|
-
await
|
|
3121
|
+
await mkdir4(lock);
|
|
2958
3122
|
break;
|
|
2959
3123
|
} catch (error) {
|
|
2960
3124
|
if (error.code !== "EEXIST" || attempt >= 200) throw error;
|
|
2961
|
-
const age = await
|
|
3125
|
+
const age = await stat3(lock).then(
|
|
2962
3126
|
(info) => Date.now() - info.mtimeMs,
|
|
2963
3127
|
() => 0
|
|
2964
3128
|
);
|
|
@@ -2990,7 +3154,7 @@ var HookStagingStore = class {
|
|
|
2990
3154
|
}
|
|
2991
3155
|
filePath(sessionId) {
|
|
2992
3156
|
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "default";
|
|
2993
|
-
return
|
|
3157
|
+
return join6(this.directory, `${safe}.json`);
|
|
2994
3158
|
}
|
|
2995
3159
|
};
|
|
2996
3160
|
function normalize2(path) {
|
|
@@ -3003,6 +3167,7 @@ function parsePaths(raw) {
|
|
|
3003
3167
|
|
|
3004
3168
|
// src/hooks/runtime.ts
|
|
3005
3169
|
var CONTEXT_BLOCK_MAX_CHARS = 1800;
|
|
3170
|
+
var HOOK_TASK_FILES = 2;
|
|
3006
3171
|
var PROMPT_FACTS = 3;
|
|
3007
3172
|
var CONTEXT_HOOK_BUDGET_MS = 15e3;
|
|
3008
3173
|
async function resolveNamespace(repositoryRoot) {
|
|
@@ -3021,7 +3186,8 @@ async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
|
|
|
3021
3186
|
beforeEdit = await buildBeforeEditContext({
|
|
3022
3187
|
repositoryRoot,
|
|
3023
3188
|
namespace,
|
|
3024
|
-
|
|
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).
|
|
3025
3191
|
...input.prompt ? { task: input.prompt } : {},
|
|
3026
3192
|
...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
|
|
3027
3193
|
...semantics?.reranker ? { reranker: semantics.reranker } : {}
|
|
@@ -3066,16 +3232,20 @@ function recallTrace(input, beforeEdit, emitted, text, started, late) {
|
|
|
3066
3232
|
tokensOut: estimateTokens(text),
|
|
3067
3233
|
...input.session_id ? { session: input.session_id } : {},
|
|
3068
3234
|
detail: {
|
|
3069
|
-
prompt: prompt
|
|
3235
|
+
prompt: clip2(prompt, 120),
|
|
3070
3236
|
facts: facts.map((fact) => fact.summary.slice(0, 100)),
|
|
3071
3237
|
dense: facts.filter((fact) => fact.reason.includes("semantic")).length,
|
|
3072
3238
|
lexical: facts.filter((fact) => /task term|target path/.test(fact.reason)).length,
|
|
3073
3239
|
files: (beforeEdit?.targetPaths ?? []).slice(0, 3),
|
|
3074
3240
|
sections: emitted.length,
|
|
3241
|
+
...text.trim() ? { block: clip2(redactSensitive(text.trim()), 2e3) } : {},
|
|
3075
3242
|
...late ? { dropped: "built after the hook budget" } : {}
|
|
3076
3243
|
}
|
|
3077
3244
|
};
|
|
3078
3245
|
}
|
|
3246
|
+
function clip2(text, max) {
|
|
3247
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
3248
|
+
}
|
|
3079
3249
|
async function runStageHook(input) {
|
|
3080
3250
|
const repositoryRoot = hookRoot(input.cwd);
|
|
3081
3251
|
const paths = extractEditedPaths(input.tool_input).map((path) => toRepoRelative(repositoryRoot, path)).filter((path) => path !== null);
|
|
@@ -3084,7 +3254,7 @@ async function runStageHook(input) {
|
|
|
3084
3254
|
}
|
|
3085
3255
|
async function llmMemoryOptions(repositoryRoot) {
|
|
3086
3256
|
try {
|
|
3087
|
-
return await (await import("./memory-
|
|
3257
|
+
return await (await import("./memory-TGHVTBME.js")).llmCaptureOptions(repositoryRoot);
|
|
3088
3258
|
} catch {
|
|
3089
3259
|
return {};
|
|
3090
3260
|
}
|
|
@@ -3101,12 +3271,12 @@ async function runCaptureHook(input) {
|
|
|
3101
3271
|
const changedFiles = claimed;
|
|
3102
3272
|
try {
|
|
3103
3273
|
const prompts = input.transcript_path ? await turnUserPrompts(input.transcript_path) : [];
|
|
3104
|
-
const { rules, revocations } = extractUserRules(prompts);
|
|
3274
|
+
const { rules, revocations, policies } = extractUserRules(prompts);
|
|
3105
3275
|
let investigation = false;
|
|
3106
3276
|
if (changedFiles.length === 0) {
|
|
3107
3277
|
const assistant = input.transcript_path ? await lastAssistantMessage(input.transcript_path) : input.last_assistant_message?.trim() ?? "";
|
|
3108
3278
|
investigation = assistant.length >= INVESTIGATION_MIN_CHARS && NAMES_CODE.test(assistant);
|
|
3109
|
-
if (!investigation && rules.length === 0 && revocations.length === 0) {
|
|
3279
|
+
if (!investigation && rules.length === 0 && revocations.length === 0 && policies.length === 0) {
|
|
3110
3280
|
await staging.acknowledge(sessionId);
|
|
3111
3281
|
return { changedFiles, result: null };
|
|
3112
3282
|
}
|
|
@@ -3131,6 +3301,7 @@ async function runCaptureHook(input) {
|
|
|
3131
3301
|
sessionId,
|
|
3132
3302
|
...verification.length > 0 ? { verification } : {},
|
|
3133
3303
|
...rules.length > 0 ? { userRules: rules } : {},
|
|
3304
|
+
...policies.length > 0 ? { userPolicies: policies } : {},
|
|
3134
3305
|
...revocations.length > 0 ? { revokedRules: revocations } : {},
|
|
3135
3306
|
...investigation ? { investigation } : {},
|
|
3136
3307
|
// Without edits, only the user's own rules and a handoff are kept: an
|
|
@@ -3166,14 +3337,14 @@ var SYNC_BUDGET_MS = 5 * 6e4;
|
|
|
3166
3337
|
async function runSyncHook(repositoryRoot, options = {}) {
|
|
3167
3338
|
const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
|
|
3168
3339
|
if (!config.postgresUrl) return null;
|
|
3169
|
-
const lock =
|
|
3170
|
-
const dirty =
|
|
3171
|
-
await
|
|
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 });
|
|
3172
3343
|
if (!await takeLock(lock)) {
|
|
3173
3344
|
await writeFile(dirty, "").catch(() => void 0);
|
|
3174
3345
|
return null;
|
|
3175
3346
|
}
|
|
3176
|
-
const { createRuntime } = await import("./operations-
|
|
3347
|
+
const { createRuntime } = await import("./operations-7BTAAUHF.js");
|
|
3177
3348
|
const runtime = await createRuntime(config, { repositoryRoot }).catch(async (error) => {
|
|
3178
3349
|
await rm4(lock, { recursive: true, force: true });
|
|
3179
3350
|
throw error;
|
|
@@ -3205,10 +3376,23 @@ async function runSyncHook(repositoryRoot, options = {}) {
|
|
|
3205
3376
|
});
|
|
3206
3377
|
result = { reindex, drained };
|
|
3207
3378
|
round += 1;
|
|
3208
|
-
} while (round < 3 && await
|
|
3379
|
+
} while (round < 3 && await stat4(dirty).then(
|
|
3209
3380
|
() => true,
|
|
3210
3381
|
() => false
|
|
3211
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
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3212
3396
|
return result;
|
|
3213
3397
|
} finally {
|
|
3214
3398
|
await runtime.close();
|
|
@@ -3218,24 +3402,39 @@ async function runSyncHook(repositoryRoot, options = {}) {
|
|
|
3218
3402
|
var SYNC_LOCK_STALE_MS = SYNC_BUDGET_MS + 6e4;
|
|
3219
3403
|
async function takeLock(path) {
|
|
3220
3404
|
try {
|
|
3221
|
-
await
|
|
3405
|
+
await mkdir5(path);
|
|
3222
3406
|
return true;
|
|
3223
3407
|
} catch {
|
|
3224
|
-
const age = await
|
|
3408
|
+
const age = await stat4(path).then(
|
|
3225
3409
|
(info) => Date.now() - info.mtimeMs,
|
|
3226
3410
|
() => 0
|
|
3227
3411
|
);
|
|
3228
3412
|
if (age < SYNC_LOCK_STALE_MS) return false;
|
|
3229
3413
|
await rm4(path, { recursive: true, force: true });
|
|
3230
|
-
return
|
|
3414
|
+
return mkdir5(path).then(
|
|
3231
3415
|
() => true,
|
|
3232
3416
|
() => false
|
|
3233
3417
|
);
|
|
3234
3418
|
}
|
|
3235
3419
|
}
|
|
3236
|
-
async function runResetHook(input) {
|
|
3420
|
+
async function runResetHook(input, emit) {
|
|
3237
3421
|
const repositoryRoot = hookRoot(input.cwd);
|
|
3238
|
-
|
|
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;
|
|
3239
3438
|
}
|
|
3240
3439
|
function extractEditedPaths(toolInput) {
|
|
3241
3440
|
if (!toolInput) return [];
|
|
@@ -3279,7 +3478,7 @@ function isVerificationCommand(command) {
|
|
|
3279
3478
|
async function turnVerification(transcriptPath) {
|
|
3280
3479
|
let raw;
|
|
3281
3480
|
try {
|
|
3282
|
-
raw = await
|
|
3481
|
+
raw = await readFile7(transcriptPath, "utf8");
|
|
3283
3482
|
} catch {
|
|
3284
3483
|
return [];
|
|
3285
3484
|
}
|
|
@@ -3331,7 +3530,7 @@ var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
|
|
|
3331
3530
|
async function turnUserPrompts(transcriptPath) {
|
|
3332
3531
|
let raw;
|
|
3333
3532
|
try {
|
|
3334
|
-
raw = await
|
|
3533
|
+
raw = await readFile7(transcriptPath, "utf8");
|
|
3335
3534
|
} catch {
|
|
3336
3535
|
return [];
|
|
3337
3536
|
}
|
|
@@ -3362,7 +3561,7 @@ async function turnUserPrompts(transcriptPath) {
|
|
|
3362
3561
|
}
|
|
3363
3562
|
async function lastAssistantMessage(transcriptPath) {
|
|
3364
3563
|
try {
|
|
3365
|
-
const raw = await
|
|
3564
|
+
const raw = await readFile7(transcriptPath, "utf8");
|
|
3366
3565
|
let latest = "";
|
|
3367
3566
|
for (const line of raw.split("\n")) {
|
|
3368
3567
|
const trimmed = line.trim();
|
|
@@ -3506,7 +3705,7 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
|
|
|
3506
3705
|
items.push({
|
|
3507
3706
|
priority: 3,
|
|
3508
3707
|
text: bound(
|
|
3509
|
-
`${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(", ")}` : ""),
|
|
3510
3709
|
500
|
|
3511
3710
|
)
|
|
3512
3711
|
});
|
|
@@ -3555,11 +3754,6 @@ export {
|
|
|
3555
3754
|
PlanLockError,
|
|
3556
3755
|
PlanValidationError,
|
|
3557
3756
|
PlanFileStore,
|
|
3558
|
-
PlanTransitionError,
|
|
3559
|
-
PlanNotFoundError,
|
|
3560
|
-
ActivePlanExistsError,
|
|
3561
|
-
PlanLifecycle,
|
|
3562
|
-
enforceContextBudget,
|
|
3563
3757
|
RiskAssessor,
|
|
3564
3758
|
createChurnSignal,
|
|
3565
3759
|
createIncidentSignal,
|
|
@@ -3567,16 +3761,17 @@ export {
|
|
|
3567
3761
|
readPolicyFile,
|
|
3568
3762
|
assessRisk,
|
|
3569
3763
|
resolveRiskPaths,
|
|
3764
|
+
PlanTransitionError,
|
|
3765
|
+
PlanNotFoundError,
|
|
3766
|
+
ActivePlanExistsError,
|
|
3767
|
+
PlanLifecycle,
|
|
3768
|
+
enforceContextBudget,
|
|
3570
3769
|
WorklogStore,
|
|
3571
3770
|
buildBootstrapContext,
|
|
3572
3771
|
buildBeforeEditContext,
|
|
3573
3772
|
ReviewStore,
|
|
3574
3773
|
captureAfterTurn,
|
|
3575
3774
|
approveReviewItem,
|
|
3576
|
-
tracePath,
|
|
3577
|
-
recordTrace,
|
|
3578
|
-
readTraces,
|
|
3579
|
-
parseTraceLines,
|
|
3580
3775
|
configuredReranker,
|
|
3581
3776
|
openHookSemantics,
|
|
3582
3777
|
CONTEXT_HOOK_BUDGET_MS,
|
|
@@ -3594,4 +3789,4 @@ export {
|
|
|
3594
3789
|
lastAssistantMessage,
|
|
3595
3790
|
renderContextBlock
|
|
3596
3791
|
};
|
|
3597
|
-
//# sourceMappingURL=chunk-
|
|
3792
|
+
//# sourceMappingURL=chunk-YJIA2MFL.js.map
|