@orquentra/codebuddy 2.6.2 → 2.8.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/README.md +10 -0
- package/dist/adapters-S4HHTWJW.js +24 -0
- package/dist/{chunk-IBLJOHJL.js → chunk-3QGW53GD.js} +2 -2
- package/dist/{chunk-IMFUYEZA.js → chunk-6DONJW3W.js} +232 -64
- package/dist/chunk-6DONJW3W.js.map +1 -0
- package/dist/{chunk-JFDRBPYF.js → chunk-6JCKMIQY.js} +2 -2
- package/dist/{chunk-S6VEG4S2.js → chunk-6KGYAFG7.js} +28 -5
- package/dist/chunk-6KGYAFG7.js.map +1 -0
- package/dist/{chunk-OWWCDRXW.js → chunk-6NZEZJYD.js} +9 -7
- package/dist/chunk-6NZEZJYD.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-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-QBDJXGG5.js → chunk-EJQVGFH2.js} +93 -22
- package/dist/chunk-EJQVGFH2.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-5FW3PMNP.js → chunk-GXOML4UZ.js} +76 -11
- package/dist/chunk-GXOML4UZ.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-ITQO632W.js +673 -0
- package/dist/chunk-ITQO632W.js.map +1 -0
- package/dist/chunk-J4T6HHI4.js +1444 -0
- package/dist/chunk-J4T6HHI4.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-2XDPKFUW.js → chunk-NY2YXP3R.js} +46 -20
- package/dist/chunk-NY2YXP3R.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-NERKZSSE.js → chunk-TLWGZVPC.js} +395 -686
- package/dist/chunk-TLWGZVPC.js.map +1 -0
- package/dist/{chunk-B6CO63SQ.js → chunk-UK6XTS2Y.js} +24 -10
- package/dist/chunk-UK6XTS2Y.js.map +1 -0
- package/dist/chunk-W7GAXL6C.js +104 -0
- package/dist/chunk-W7GAXL6C.js.map +1 -0
- package/dist/chunk-XL2HLACE.js +55 -0
- package/dist/chunk-XL2HLACE.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-ZNLSCV3J.js} +578 -381
- package/dist/chunk-ZNLSCV3J.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 +23 -19
- package/dist/install-BXM56QCD.js +19 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-command-7DFW32MA.js +19 -0
- package/dist/{local-5MZURLCZ.js → local-ST4WMRCG.js} +3 -3
- package/dist/{main-MTBQZGJV.js → main-QUASSFP7.js} +987 -1741
- package/dist/main-QUASSFP7.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-WJR7AZZF.js +26 -0
- package/dist/pretool-command-JTKX6G3M.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-OQQ22VJF.js} +15 -11
- package/dist/summaries-ML53R7EL.js +21 -0
- package/dist/summaries-ML53R7EL.js.map +1 -0
- package/dist/{zvec-MSRKT7IV.js → zvec-XDZNZ2OL.js} +11 -3
- package/dist/zvec-XDZNZ2OL.js.map +1 -0
- 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/{cloud-7WM3YLI4.js.map → adapters-S4HHTWJW.js.map} +0 -0
- /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/{config-DEVHUDIU.js.map → cloud-LXZUR3U2.js.map} +0 -0
- /package/dist/{lean-command-HTQCPH76.js.map → config-GEZFLXK6.js.map} +0 -0
- /package/dist/{local-5MZURLCZ.js.map → install-BXM56QCD.js.map} +0 -0
- /package/dist/{operations-POCHY3WR.js.map → lean-command-7DFW32MA.js.map} +0 -0
- /package/dist/{pretool-command-AX6JXP25.js.map → local-ST4WMRCG.js.map} +0 -0
- /package/dist/{runtime-CUBOOPZO.js.map → operations-WJR7AZZF.js.map} +0 -0
- /package/dist/{summaries-SB5L2TSW.js.map → pretool-command-JTKX6G3M.js.map} +0 -0
- /package/dist/{zvec-MSRKT7IV.js.map → runtime-OQQ22VJF.js.map} +0 -0
|
@@ -1,67 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MEMORY_CLASSES,
|
|
3
|
-
memoryCandidateSchema
|
|
4
|
-
} from "./chunk-IHQM2ZDV.js";
|
|
5
1
|
import {
|
|
6
2
|
SessionCapsuleLedger,
|
|
7
3
|
detectTestCommand,
|
|
8
4
|
estimateTokens,
|
|
9
|
-
findCoveringTest
|
|
5
|
+
findCoveringTest
|
|
6
|
+
} from "./chunk-GKNYMNFM.js";
|
|
7
|
+
import {
|
|
8
|
+
recordTrace
|
|
9
|
+
} from "./chunk-XL2HLACE.js";
|
|
10
|
+
import {
|
|
10
11
|
hookRoot
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TGFTKWOR.js";
|
|
13
|
+
import {
|
|
14
|
+
MEMORY_CLASSES,
|
|
15
|
+
memoryCandidateSchema
|
|
16
|
+
} from "./chunk-DFV2SK6C.js";
|
|
17
|
+
import {
|
|
18
|
+
INCIDENT_SEVERITIES,
|
|
19
|
+
MemoryFileStore,
|
|
20
|
+
admitsLexically,
|
|
21
|
+
isEligibleMemory,
|
|
22
|
+
lexicalTerms,
|
|
23
|
+
termOverlap
|
|
24
|
+
} from "./chunk-6KGYAFG7.js";
|
|
25
|
+
import {
|
|
26
|
+
generateEntityId
|
|
27
|
+
} from "./chunk-CAEPHLHC.js";
|
|
12
28
|
import {
|
|
13
29
|
TASK_PATH_LIMIT,
|
|
14
30
|
countTerms,
|
|
15
31
|
discoverTaskPaths,
|
|
16
32
|
identifierTokens,
|
|
17
|
-
loadFreshIndex,
|
|
18
33
|
loadProjectArchitectureMap,
|
|
19
34
|
namedSegments,
|
|
20
35
|
readBoundedSource,
|
|
36
|
+
syncFileVectors,
|
|
21
37
|
taskTerms
|
|
22
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-TLWGZVPC.js";
|
|
39
|
+
import {
|
|
40
|
+
loadFreshIndex
|
|
41
|
+
} from "./chunk-ITQO632W.js";
|
|
23
42
|
import {
|
|
24
43
|
extractSymbolTable,
|
|
25
44
|
supportsSymbols
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
import {
|
|
28
|
-
INCIDENT_SEVERITIES,
|
|
29
|
-
MemoryFileStore,
|
|
30
|
-
isEligibleMemory,
|
|
31
|
-
lexicalTerms,
|
|
32
|
-
termOverlap
|
|
33
|
-
} from "./chunk-S6VEG4S2.js";
|
|
45
|
+
} from "./chunk-Y7D7PUBE.js";
|
|
34
46
|
import {
|
|
47
|
+
REDACTED_LINE,
|
|
35
48
|
claimText,
|
|
49
|
+
isCredentialPath,
|
|
50
|
+
loadKnownSecretValues,
|
|
36
51
|
redactPersisted,
|
|
37
52
|
redactSensitive,
|
|
53
|
+
redactSourceText,
|
|
38
54
|
scanFields,
|
|
39
55
|
scanSensitive
|
|
40
|
-
} from "./chunk-
|
|
41
|
-
import {
|
|
42
|
-
generateEntityId
|
|
43
|
-
} from "./chunk-XBJQKCLU.js";
|
|
56
|
+
} from "./chunk-AL6VBTNA.js";
|
|
44
57
|
import {
|
|
45
58
|
DEFAULT_RERANK_MODELS
|
|
46
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-HPOR6YXA.js";
|
|
47
60
|
import {
|
|
48
|
-
assertNoSymlinkBetween,
|
|
49
|
-
composeMarkdown,
|
|
50
61
|
loadPlanPolicy,
|
|
51
62
|
loadRuntimeConfig,
|
|
63
|
+
readConfigFile
|
|
64
|
+
} from "./chunk-KNR2P2UY.js";
|
|
65
|
+
import {
|
|
66
|
+
assertNoSymlinkBetween,
|
|
67
|
+
composeMarkdown,
|
|
52
68
|
parseFrontMatter,
|
|
53
|
-
readConfigFile,
|
|
54
69
|
writeAtomic
|
|
55
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-CG5B3LYG.js";
|
|
56
71
|
|
|
57
72
|
// src/hooks/runtime.ts
|
|
58
|
-
import { mkdir as
|
|
59
|
-
import { basename, dirname as dirname5, isAbsolute, join as
|
|
73
|
+
import { mkdir as mkdir5, readFile as readFile7, rm as rm4, stat as stat4, writeFile } from "fs/promises";
|
|
74
|
+
import { basename, dirname as dirname5, isAbsolute, join as join7, relative as relative5, resolve as resolve9, sep as sep5 } from "path";
|
|
60
75
|
import pTimeout2 from "p-timeout";
|
|
61
76
|
|
|
62
77
|
// src/context/engine.ts
|
|
63
78
|
import { execFile } from "child_process";
|
|
64
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
65
79
|
import { join as join4, resolve as resolve5 } from "path";
|
|
66
80
|
import { promisify } from "util";
|
|
67
81
|
|
|
@@ -307,193 +321,10 @@ async function releaseOwnedLock(lockPath) {
|
|
|
307
321
|
}
|
|
308
322
|
}
|
|
309
323
|
|
|
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
324
|
// src/risk/service.ts
|
|
494
325
|
import { spawn as spawn2 } from "child_process";
|
|
495
326
|
import { realpath as realpath2 } from "fs/promises";
|
|
496
|
-
import { relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
327
|
+
import { dirname as dirname2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
497
328
|
|
|
498
329
|
// src/risk/assessor.ts
|
|
499
330
|
var RiskAssessor = class {
|
|
@@ -514,7 +345,7 @@ var RiskAssessor = class {
|
|
|
514
345
|
const byPath = /* @__PURE__ */ new Map();
|
|
515
346
|
const perSignal = /* @__PURE__ */ new Map();
|
|
516
347
|
for (const { signal, contributions } of settled) {
|
|
517
|
-
const
|
|
348
|
+
const stat5 = perSignal.get(signal.id) ?? {
|
|
518
349
|
id: signal.id,
|
|
519
350
|
category: signal.category,
|
|
520
351
|
contributions: 0,
|
|
@@ -525,10 +356,10 @@ var RiskAssessor = class {
|
|
|
525
356
|
const list = byPath.get(contribution.path) ?? [];
|
|
526
357
|
list.push({ signal, contribution });
|
|
527
358
|
byPath.set(contribution.path, list);
|
|
528
|
-
|
|
529
|
-
|
|
359
|
+
stat5.contributions += 1;
|
|
360
|
+
stat5.totalWeight += contribution.weight;
|
|
530
361
|
}
|
|
531
|
-
perSignal.set(signal.id,
|
|
362
|
+
perSignal.set(signal.id, stat5);
|
|
532
363
|
}
|
|
533
364
|
const items = [];
|
|
534
365
|
for (const [path, contributions] of byPath) {
|
|
@@ -594,9 +425,10 @@ function round2(value) {
|
|
|
594
425
|
|
|
595
426
|
// src/risk/signals/churn.ts
|
|
596
427
|
import { spawn } from "child_process";
|
|
597
|
-
import { realpath } from "fs/promises";
|
|
428
|
+
import { readFile as readFile2, realpath } from "fs/promises";
|
|
598
429
|
import { resolve as resolve2 } from "path";
|
|
599
430
|
var GIT_LOG_CONCURRENCY = 4;
|
|
431
|
+
var DAY_MS = 864e5;
|
|
600
432
|
function createChurnSignal(options) {
|
|
601
433
|
return {
|
|
602
434
|
id: "git-churn",
|
|
@@ -611,20 +443,56 @@ function createChurnSignal(options) {
|
|
|
611
443
|
if (root.code !== 0 || resolve2(root.stdout.trim()) !== expected) {
|
|
612
444
|
return [];
|
|
613
445
|
}
|
|
614
|
-
const
|
|
446
|
+
const lifetime = options.mode === "lifetime-size";
|
|
447
|
+
const asOf = options.asOf === void 0 ? void 0 : new Date(options.asOf);
|
|
448
|
+
const end = asOf ?? /* @__PURE__ */ new Date();
|
|
449
|
+
const range = [
|
|
450
|
+
...lifetime ? [] : [
|
|
451
|
+
`--since=${options.since ?? new Date(end.getTime() - (options.windowDays ?? 30) * DAY_MS).toISOString()}`
|
|
452
|
+
],
|
|
453
|
+
...asOf ? [`--until=${asOf.toISOString()}`] : []
|
|
454
|
+
];
|
|
455
|
+
const revision = lifetime && asOf ? (await runGit(options.repositoryRoot, [
|
|
456
|
+
"rev-list",
|
|
457
|
+
"-1",
|
|
458
|
+
`--before=${asOf.toISOString()}`,
|
|
459
|
+
"HEAD"
|
|
460
|
+
])).stdout.trim() : null;
|
|
615
461
|
const logs = [];
|
|
462
|
+
const sizes = [];
|
|
616
463
|
for (let start = 0; start < input.paths.length; start += GIT_LOG_CONCURRENCY) {
|
|
617
464
|
const batch = input.paths.slice(start, start + GIT_LOG_CONCURRENCY);
|
|
618
465
|
logs.push(
|
|
619
466
|
...await Promise.all(
|
|
620
|
-
batch.map((path) => gitLogForPath(options.repositoryRoot, path,
|
|
467
|
+
batch.map((path) => gitLogForPath(options.repositoryRoot, path, range))
|
|
621
468
|
)
|
|
622
469
|
);
|
|
470
|
+
if (lifetime) {
|
|
471
|
+
sizes.push(
|
|
472
|
+
...await Promise.all(
|
|
473
|
+
batch.map((path) => lineCount(options.repositoryRoot, path, revision))
|
|
474
|
+
)
|
|
475
|
+
);
|
|
476
|
+
}
|
|
623
477
|
}
|
|
624
478
|
const contributions = [];
|
|
625
479
|
for (const [index, path] of input.paths.entries()) {
|
|
626
480
|
const commits = logs[index] ?? [];
|
|
627
481
|
if (commits.length === 0) continue;
|
|
482
|
+
if (lifetime) {
|
|
483
|
+
const lines = sizes[index] ?? 0;
|
|
484
|
+
contributions.push({
|
|
485
|
+
path,
|
|
486
|
+
// ponytail: hand-picked coefficients; fit them on the time-split eval before defaulting on.
|
|
487
|
+
weight: Math.min(
|
|
488
|
+
100,
|
|
489
|
+
Math.round(8 * Math.log2(1 + commits.length) + 4 * Math.log2(1 + lines))
|
|
490
|
+
),
|
|
491
|
+
reason: `${path} has ${commits.length} commit${commits.length === 1 ? "" : "s"}, ${lines} line${lines === 1 ? "" : "s"} (lifetime churn and size).`,
|
|
492
|
+
evidence: evidenceOf(commits)
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
628
496
|
const authors = new Set(commits.map((commit) => commit.author));
|
|
629
497
|
const reverts = commits.filter((commit) => /revert|rollback|backout/i.test(commit.message));
|
|
630
498
|
const weight = Math.min(100, commits.length * 8 + authors.size * 12 + reverts.length * 20);
|
|
@@ -632,23 +500,38 @@ function createChurnSignal(options) {
|
|
|
632
500
|
path,
|
|
633
501
|
weight,
|
|
634
502
|
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
|
-
}))
|
|
503
|
+
evidence: evidenceOf(commits)
|
|
641
504
|
});
|
|
642
505
|
}
|
|
643
506
|
return contributions;
|
|
644
507
|
}
|
|
645
508
|
};
|
|
646
509
|
}
|
|
647
|
-
|
|
510
|
+
function evidenceOf(commits) {
|
|
511
|
+
return commits.slice(0, 5).map((commit) => ({
|
|
512
|
+
kind: "git_commit",
|
|
513
|
+
sha: commit.sha,
|
|
514
|
+
message: commit.message,
|
|
515
|
+
ts: commit.ts
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
async function lineCount(repositoryRoot, path, revision) {
|
|
519
|
+
let text = "";
|
|
520
|
+
if (revision === null) {
|
|
521
|
+
text = await readFile2(resolve2(repositoryRoot, path), "utf8").catch(() => "");
|
|
522
|
+
} else if (revision) {
|
|
523
|
+
const shown = await runGit(repositoryRoot, ["show", `${revision}:${path}`]);
|
|
524
|
+
if (shown.code === 0) text = shown.stdout;
|
|
525
|
+
}
|
|
526
|
+
if (text.length === 0) return 0;
|
|
527
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
528
|
+
}
|
|
529
|
+
async function gitLogForPath(repositoryRoot, path, range) {
|
|
648
530
|
const result = await runGit(repositoryRoot, [
|
|
649
531
|
"log",
|
|
650
532
|
"--follow",
|
|
651
|
-
|
|
533
|
+
"--max-count=1000",
|
|
534
|
+
...range,
|
|
652
535
|
"--format=%H%x1f%an%x1f%cI%x1f%s%x1e",
|
|
653
536
|
"--",
|
|
654
537
|
path
|
|
@@ -734,7 +617,7 @@ function severityWeight(severity) {
|
|
|
734
617
|
}
|
|
735
618
|
|
|
736
619
|
// src/risk/signals/policy.ts
|
|
737
|
-
import { readFile as
|
|
620
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
738
621
|
import { join as join2 } from "path";
|
|
739
622
|
import { parse as parseYaml2 } from "yaml";
|
|
740
623
|
import { z as z2 } from "zod";
|
|
@@ -786,7 +669,7 @@ function createPolicySignal(options) {
|
|
|
786
669
|
}
|
|
787
670
|
async function readPolicyFile(path) {
|
|
788
671
|
try {
|
|
789
|
-
const raw = await
|
|
672
|
+
const raw = await readFile3(path, "utf8");
|
|
790
673
|
return policyFileSchema.parse(parseYaml2(raw) ?? {});
|
|
791
674
|
} catch (error) {
|
|
792
675
|
if (error.code === "ENOENT") return { rules: [] };
|
|
@@ -822,10 +705,17 @@ async function assessRisk(input) {
|
|
|
822
705
|
};
|
|
823
706
|
}
|
|
824
707
|
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
708
|
+
const churn = (await readConfigFile(repositoryRoot).catch(() => ({ risk: void 0 }))).risk;
|
|
825
709
|
const signals = input.signals ?? [
|
|
826
710
|
createIncidentSignal({ store: memoryStore }),
|
|
827
711
|
createPolicySignal({ repositoryRoot }),
|
|
828
|
-
...input.useGit === false ? [] : [
|
|
712
|
+
...input.useGit === false ? [] : [
|
|
713
|
+
createChurnSignal({
|
|
714
|
+
repositoryRoot,
|
|
715
|
+
...churn?.churnWindowDays !== void 0 ? { windowDays: churn.churnWindowDays } : {},
|
|
716
|
+
...churn?.churnMode !== void 0 ? { mode: churn.churnMode } : {}
|
|
717
|
+
})
|
|
718
|
+
]
|
|
829
719
|
];
|
|
830
720
|
const assessment = await new RiskAssessor(signals).assess({
|
|
831
721
|
paths,
|
|
@@ -859,15 +749,25 @@ async function resolveRiskPaths(input) {
|
|
|
859
749
|
}
|
|
860
750
|
const realRoot = await realpath2(input.repositoryRoot).catch(() => resolve3(input.repositoryRoot));
|
|
861
751
|
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
|
-
})
|
|
752
|
+
[...paths].map((path) => realPathInside(realRoot, resolve3(input.repositoryRoot, path)))
|
|
866
753
|
);
|
|
867
754
|
const resolved = [...paths].filter((_, index) => inside[index]).sort();
|
|
868
755
|
return resolved.length > MAX_RESOLVED_PATHS ? resolved.slice(0, MAX_RESOLVED_PATHS) : resolved;
|
|
869
756
|
}
|
|
870
757
|
var MAX_RESOLVED_PATHS = 200;
|
|
758
|
+
async function containedRepoPath(root, path) {
|
|
759
|
+
const contained = containedRelativePath(root, path);
|
|
760
|
+
if (contained === null) return null;
|
|
761
|
+
const realRoot = await realpath2(root).catch(() => resolve3(root));
|
|
762
|
+
return await realPathInside(realRoot, resolve3(root, contained)) ? contained : null;
|
|
763
|
+
}
|
|
764
|
+
async function realPathInside(realRoot, absolute) {
|
|
765
|
+
for (let current = absolute; ; current = dirname2(current)) {
|
|
766
|
+
const real = await realpath2(current).catch(() => null);
|
|
767
|
+
if (real !== null) return real === realRoot || real.startsWith(`${realRoot}${sep2}`);
|
|
768
|
+
if (dirname2(current) === current) return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
871
771
|
function containedRelativePath(root, path) {
|
|
872
772
|
const trimmed = normalizePath2(path);
|
|
873
773
|
if (!trimmed || trimmed.includes("\0")) return null;
|
|
@@ -878,16 +778,18 @@ function containedRelativePath(root, path) {
|
|
|
878
778
|
}
|
|
879
779
|
async function changedGitPaths(repositoryRoot) {
|
|
880
780
|
const [tracked, untracked] = await Promise.all([
|
|
881
|
-
|
|
882
|
-
runGit2(repositoryRoot, ["
|
|
781
|
+
// `-z`: NUL-separated, unquoted names (non-ASCII, quotes, tabs, newlines).
|
|
782
|
+
runGit2(repositoryRoot, ["diff", "--name-only", "-z", "HEAD"]),
|
|
783
|
+
runGit2(repositoryRoot, ["ls-files", "-z", "--others", "--exclude-standard"])
|
|
883
784
|
]);
|
|
884
|
-
return [...tracked.stdout.split("\
|
|
785
|
+
return [...tracked.stdout.split("\0"), ...untracked.stdout.split("\0")].filter((path) => path && !path.split("/").some((segment) => segment.startsWith(".")));
|
|
885
786
|
}
|
|
886
787
|
function runGit2(cwd, args) {
|
|
887
788
|
return new Promise((resolveResult, reject) => {
|
|
888
789
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
889
790
|
let stdout = "";
|
|
890
791
|
let stderr = "";
|
|
792
|
+
child.stdout.setEncoding("utf8");
|
|
891
793
|
child.stdout.on("data", (chunk) => {
|
|
892
794
|
stdout += String(chunk);
|
|
893
795
|
});
|
|
@@ -899,13 +801,206 @@ function runGit2(cwd, args) {
|
|
|
899
801
|
});
|
|
900
802
|
}
|
|
901
803
|
function normalizePath2(path) {
|
|
902
|
-
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
804
|
+
return (sep2 === "\\" ? path.replace(/\\/g, "/") : path).replace(/^\.\//, "");
|
|
903
805
|
}
|
|
904
806
|
|
|
807
|
+
// src/core/plan-lifecycle.ts
|
|
808
|
+
var TRANSITIONS = {
|
|
809
|
+
draft: ["approved", "abandoned"],
|
|
810
|
+
approved: ["executing", "abandoned"],
|
|
811
|
+
executing: ["complete", "abandoned"],
|
|
812
|
+
complete: [],
|
|
813
|
+
abandoned: []
|
|
814
|
+
};
|
|
815
|
+
var AMENDABLE = ["draft", "executing"];
|
|
816
|
+
var PlanTransitionError = class extends Error {
|
|
817
|
+
constructor(from, to) {
|
|
818
|
+
super(`Illegal plan transition: ${from} \u2192 ${to}.`);
|
|
819
|
+
this.name = "PlanTransitionError";
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
var PlanNotFoundError = class extends Error {
|
|
823
|
+
constructor(id) {
|
|
824
|
+
super(`Plan ${id} not found.`);
|
|
825
|
+
this.name = "PlanNotFoundError";
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var ActivePlanExistsError = class extends Error {
|
|
829
|
+
activePlanId;
|
|
830
|
+
constructor(namespace, activePlanId) {
|
|
831
|
+
super(
|
|
832
|
+
`Namespace "${namespace}" already has an active plan (${activePlanId}). Complete or abandon it before approving another.`
|
|
833
|
+
);
|
|
834
|
+
this.name = "ActivePlanExistsError";
|
|
835
|
+
this.activePlanId = activePlanId;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
var DEFAULT_BODY = [
|
|
839
|
+
"# Goal",
|
|
840
|
+
"",
|
|
841
|
+
"(what this plan achieves)",
|
|
842
|
+
"",
|
|
843
|
+
"# Approach",
|
|
844
|
+
"",
|
|
845
|
+
"(ordered steps)",
|
|
846
|
+
"",
|
|
847
|
+
"# Notes",
|
|
848
|
+
"",
|
|
849
|
+
"(freeform; ignored by tooling)"
|
|
850
|
+
].join("\n");
|
|
851
|
+
var PlanLifecycle = class {
|
|
852
|
+
store;
|
|
853
|
+
constructor(store) {
|
|
854
|
+
this.store = store;
|
|
855
|
+
}
|
|
856
|
+
async create(input) {
|
|
857
|
+
await this.assertPathsInside(input);
|
|
858
|
+
const id = generateEntityId("pln");
|
|
859
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
860
|
+
await this.store.writePlan({
|
|
861
|
+
id,
|
|
862
|
+
namespace: input.namespace,
|
|
863
|
+
title: input.title,
|
|
864
|
+
status: "draft",
|
|
865
|
+
brief: input.brief,
|
|
866
|
+
filesToTouch: input.filesToTouch ?? [],
|
|
867
|
+
testsToAdd: input.testsToAdd ?? [],
|
|
868
|
+
outOfScope: input.outOfScope ?? [],
|
|
869
|
+
risks: input.risks ?? [],
|
|
870
|
+
createdAt: now,
|
|
871
|
+
createdByAgent: input.createdByAgent ?? null,
|
|
872
|
+
approvedAt: null,
|
|
873
|
+
executingAt: null,
|
|
874
|
+
completedAt: null,
|
|
875
|
+
abandonedAt: null,
|
|
876
|
+
commitSha: null,
|
|
877
|
+
abandonReason: null,
|
|
878
|
+
body: input.body ?? DEFAULT_BODY
|
|
879
|
+
});
|
|
880
|
+
return this.store.readPlan(id);
|
|
881
|
+
}
|
|
882
|
+
async approve(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
883
|
+
const current = await this.require(id);
|
|
884
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
885
|
+
const fresh = await this.require(id);
|
|
886
|
+
this.assertTransition(fresh.status, "approved");
|
|
887
|
+
const active = await this.store.findActivePlan(fresh.namespace);
|
|
888
|
+
if (active && active.id !== id) {
|
|
889
|
+
throw new ActivePlanExistsError(fresh.namespace, active.id);
|
|
890
|
+
}
|
|
891
|
+
return this.rewrite(fresh, { status: "approved", approvedAt: now });
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
async start(id, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
895
|
+
const current = await this.require(id);
|
|
896
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
897
|
+
const fresh = await this.require(id);
|
|
898
|
+
this.assertTransition(fresh.status, "executing");
|
|
899
|
+
return this.rewrite(fresh, { status: "executing", executingAt: now });
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
async complete(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
903
|
+
const current = await this.require(id);
|
|
904
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
905
|
+
const fresh = await this.require(id);
|
|
906
|
+
this.assertTransition(fresh.status, "complete");
|
|
907
|
+
return this.rewrite(fresh, {
|
|
908
|
+
status: "complete",
|
|
909
|
+
completedAt: now,
|
|
910
|
+
...options.commitSha !== void 0 ? { commitSha: options.commitSha } : {}
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
async abandon(id, options = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
915
|
+
const current = await this.require(id);
|
|
916
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
917
|
+
const fresh = await this.require(id);
|
|
918
|
+
this.assertTransition(fresh.status, "abandoned");
|
|
919
|
+
return this.rewrite(fresh, {
|
|
920
|
+
status: "abandoned",
|
|
921
|
+
abandonedAt: now,
|
|
922
|
+
...options.reason !== void 0 ? { abandonReason: options.reason } : {}
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async amend(id, patch) {
|
|
927
|
+
await this.assertPathsInside(patch);
|
|
928
|
+
const current = await this.require(id);
|
|
929
|
+
return this.store.withNamespaceLock(current.namespace, async () => {
|
|
930
|
+
const fresh = await this.require(id);
|
|
931
|
+
if (!AMENDABLE.includes(fresh.status)) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`Plan ${id} is ${fresh.status}; only draft or executing plans can be amended.`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return this.rewrite(fresh, {
|
|
937
|
+
...patch.title !== void 0 ? { title: patch.title } : {},
|
|
938
|
+
...patch.brief !== void 0 ? { brief: patch.brief } : {},
|
|
939
|
+
...patch.filesToTouch !== void 0 ? { filesToTouch: patch.filesToTouch } : {},
|
|
940
|
+
...patch.testsToAdd !== void 0 ? { testsToAdd: patch.testsToAdd } : {},
|
|
941
|
+
...patch.outOfScope !== void 0 ? { outOfScope: patch.outOfScope } : {},
|
|
942
|
+
...patch.risks !== void 0 ? { risks: patch.risks } : {},
|
|
943
|
+
...patch.body !== void 0 ? { body: patch.body } : {}
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
/** Plan paths flow into risk, context and file reads; none may leave the repository. */
|
|
948
|
+
async assertPathsInside(input) {
|
|
949
|
+
for (const { path } of [...input.filesToTouch ?? [], ...input.testsToAdd ?? []]) {
|
|
950
|
+
if (await containedRepoPath(this.store.repositoryRoot, path) === null) {
|
|
951
|
+
throw new Error(`Plan path is outside the repository: ${path}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async current(namespace) {
|
|
956
|
+
return this.store.findActivePlan(namespace);
|
|
957
|
+
}
|
|
958
|
+
assertTransition(from, to) {
|
|
959
|
+
if (!TRANSITIONS[from].includes(to)) {
|
|
960
|
+
throw new PlanTransitionError(from, to);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async require(id) {
|
|
964
|
+
try {
|
|
965
|
+
return await this.store.readPlan(id);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
if (error.code === "ENOENT") {
|
|
968
|
+
throw new PlanNotFoundError(id);
|
|
969
|
+
}
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/** Rewrite the plan file with a partial set of changed fields. */
|
|
974
|
+
async rewrite(plan, changes) {
|
|
975
|
+
const next = { ...plan, ...changes };
|
|
976
|
+
await this.store.writePlan({
|
|
977
|
+
id: next.id,
|
|
978
|
+
namespace: next.namespace,
|
|
979
|
+
title: next.title,
|
|
980
|
+
status: next.status,
|
|
981
|
+
brief: next.brief,
|
|
982
|
+
filesToTouch: next.filesToTouch,
|
|
983
|
+
testsToAdd: next.testsToAdd,
|
|
984
|
+
outOfScope: next.outOfScope,
|
|
985
|
+
risks: next.risks,
|
|
986
|
+
createdAt: next.createdAt,
|
|
987
|
+
createdByAgent: next.createdByAgent,
|
|
988
|
+
approvedAt: next.approvedAt,
|
|
989
|
+
executingAt: next.executingAt,
|
|
990
|
+
completedAt: next.completedAt,
|
|
991
|
+
abandonedAt: next.abandonedAt,
|
|
992
|
+
commitSha: next.commitSha,
|
|
993
|
+
abandonReason: next.abandonReason,
|
|
994
|
+
body: next.body
|
|
995
|
+
});
|
|
996
|
+
return this.store.readPlan(next.id);
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
905
1000
|
// src/worklog/store.ts
|
|
906
1001
|
import { createHash } from "crypto";
|
|
907
|
-
import { mkdir as mkdir2, readdir as readdir2, readFile as
|
|
908
|
-
import { dirname as
|
|
1002
|
+
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1003
|
+
import { dirname as dirname3, join as join3, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
|
|
909
1004
|
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
910
1005
|
import { z as z3 } from "zod";
|
|
911
1006
|
var LIMITS = {
|
|
@@ -958,7 +1053,7 @@ var WorklogStore = class {
|
|
|
958
1053
|
if (existing) return existing.id;
|
|
959
1054
|
const id = generateEntityId("wlg", now.getTime());
|
|
960
1055
|
const path = this.entryPath(id);
|
|
961
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
1056
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname3(path));
|
|
962
1057
|
await mkdir2(this.directory, { recursive: true, mode: 448 });
|
|
963
1058
|
const frontMatter = stringifyYaml2({
|
|
964
1059
|
schemaVersion: 1,
|
|
@@ -981,7 +1076,7 @@ var WorklogStore = class {
|
|
|
981
1076
|
async read(id) {
|
|
982
1077
|
const path = this.entryPath(id);
|
|
983
1078
|
await assertNoSymlinkBetween(this.repositoryRoot, path);
|
|
984
|
-
const parsed = parseFrontMatter(await
|
|
1079
|
+
const parsed = parseFrontMatter(await readFile4(path, "utf8"));
|
|
985
1080
|
return {
|
|
986
1081
|
...entrySchema.parse(parseYaml3(parsed.frontMatter)),
|
|
987
1082
|
summary: parsed.content.trim(),
|
|
@@ -1319,11 +1414,13 @@ async function buildBeforeEditContext(input) {
|
|
|
1319
1414
|
const mapPromise = loadProjectArchitectureMap(repositoryRoot, manifest);
|
|
1320
1415
|
const discovery = !hasExplicitTargets && input.task ? await discoverTaskPaths(repositoryRoot, input.task, manifest, mapPromise) : { paths: [], hits: [], explain: [] };
|
|
1321
1416
|
const taskPaths = discovery.paths;
|
|
1322
|
-
const
|
|
1417
|
+
const askedTask = Boolean(input.task?.trim());
|
|
1418
|
+
const targetPaths = hasExplicitTargets ? resolvedPaths : taskPaths.length > 0 ? taskPaths : askedTask && input.useGit !== true ? [] : resolvedPaths;
|
|
1323
1419
|
const discoveredFromTask = taskPaths.length > 0;
|
|
1324
1420
|
const pathSource = classifyPathSource(input, targetPaths.length, discoveredFromTask);
|
|
1325
1421
|
const planStore = new PlanFileStore(repositoryRoot);
|
|
1326
1422
|
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
1423
|
+
const knownSecrets = await loadKnownSecretValues(repositoryRoot);
|
|
1327
1424
|
const [referencedPlan, policy, policyFile, incidents, facts, risks, map, diffs, handoffs] = await Promise.all([
|
|
1328
1425
|
resolveRelevantPlan(planStore, namespace, input.planId),
|
|
1329
1426
|
loadPlanPolicy(repositoryRoot),
|
|
@@ -1338,7 +1435,7 @@ async function buildBeforeEditContext(input) {
|
|
|
1338
1435
|
useGit: !hasExplicitTargets && input.useGit !== false
|
|
1339
1436
|
}),
|
|
1340
1437
|
mapPromise,
|
|
1341
|
-
input.useGit === false ? Promise.resolve([]) : readGitDiffs(repositoryRoot, targetPaths),
|
|
1438
|
+
input.useGit === false ? Promise.resolve([]) : readGitDiffs(repositoryRoot, targetPaths, knownSecrets),
|
|
1342
1439
|
recallHandoffs(repositoryRoot, {
|
|
1343
1440
|
namespace,
|
|
1344
1441
|
paths: targetPaths,
|
|
@@ -1354,6 +1451,11 @@ async function buildBeforeEditContext(input) {
|
|
|
1354
1451
|
);
|
|
1355
1452
|
}
|
|
1356
1453
|
explain.push(...discovery.explain);
|
|
1454
|
+
if (askedTask && !hasExplicitTargets && taskPaths.length === 0) {
|
|
1455
|
+
explain.push(
|
|
1456
|
+
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"
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1357
1459
|
const plan = summarizePlan(referencedPlan);
|
|
1358
1460
|
if (plan) explain.push(`plan: ${plan.id} (${plan.status})`);
|
|
1359
1461
|
const policyRules = summarizeMatchingPolicyRules(policyFile.rules, targetPaths);
|
|
@@ -1376,7 +1478,8 @@ async function buildBeforeEditContext(input) {
|
|
|
1376
1478
|
input.task ?? "",
|
|
1377
1479
|
discovery.hits,
|
|
1378
1480
|
"symbols" in discovery ? discovery.symbols ?? {} : {},
|
|
1379
|
-
input.evidence ?? "overview"
|
|
1481
|
+
input.evidence ?? "overview",
|
|
1482
|
+
knownSecrets
|
|
1380
1483
|
);
|
|
1381
1484
|
if (symbols.length > 0) {
|
|
1382
1485
|
const spans = symbols.reduce((total, file) => total + (file.spans?.length ?? 0), 0);
|
|
@@ -1543,7 +1646,7 @@ function computeNeighbours(map, paths) {
|
|
|
1543
1646
|
}
|
|
1544
1647
|
return result;
|
|
1545
1648
|
}
|
|
1546
|
-
async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, picked = {}, evidence = "overview") {
|
|
1649
|
+
async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, picked = {}, evidence = "overview", knownSecrets = []) {
|
|
1547
1650
|
const terms = taskTerms(task);
|
|
1548
1651
|
const tokens = identifierTokens(task);
|
|
1549
1652
|
const result = [];
|
|
@@ -1552,9 +1655,9 @@ async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, pic
|
|
|
1552
1655
|
for (const path of targetPaths.slice(0, LIMITS2.symbolFiles)) {
|
|
1553
1656
|
const source = await readBoundedSource(repositoryRoot, path);
|
|
1554
1657
|
if (source === null) continue;
|
|
1555
|
-
const lines = source.split(/\r?\n/);
|
|
1658
|
+
const lines = redactSourceText(source, knownSecrets).split(/\r?\n/);
|
|
1556
1659
|
const fileHits = hits.filter((hit2) => hit2.path === path && hit2.endLine <= lines.length);
|
|
1557
|
-
const table = supportsSymbols(path) ? extractSymbolTable(source) : [];
|
|
1660
|
+
const table = supportsSymbols(path) ? extractSymbolTable(source, path) : [];
|
|
1558
1661
|
const credited = /* @__PURE__ */ new Map();
|
|
1559
1662
|
for (const hit2 of fileHits) {
|
|
1560
1663
|
const owner = table.filter((symbol) => hit2.startLine <= symbol.endLine && hit2.endLine >= symbol.startLine).sort(
|
|
@@ -1570,7 +1673,9 @@ async function computeTargetSymbols(repositoryRoot, targetPaths, task, hits, pic
|
|
|
1570
1673
|
}).sort(
|
|
1571
1674
|
(left, right) => right.score - left.score || left.symbol.startLine - right.symbol.startLine
|
|
1572
1675
|
);
|
|
1573
|
-
const signatures = ranked.filter(({ symbol, score }) => symbol.kind !== "method" || score > 0).slice(0, LIMITS2.symbolsPerFile).map(
|
|
1676
|
+
const signatures = ranked.filter(({ symbol, score }) => symbol.kind !== "method" || score > 0).slice(0, LIMITS2.symbolsPerFile).map(
|
|
1677
|
+
({ symbol }) => lines[symbol.startLine - 1] === REDACTED_LINE ? REDACTED_LINE : redactSourceText(symbol.signature, knownSecrets)
|
|
1678
|
+
);
|
|
1574
1679
|
const best = ranked[0]?.score ? ranked[0] : void 0;
|
|
1575
1680
|
const hit = best ? best.hit : fileHits[0];
|
|
1576
1681
|
const range = best?.symbol ?? hit;
|
|
@@ -1639,20 +1744,38 @@ function headInSignature(lines, symbol) {
|
|
|
1639
1744
|
const line = (lines[symbol.startLine - 1] ?? "").trim();
|
|
1640
1745
|
return !head.endsWith("\u2026") && line.startsWith(head) && /^[\s{=>(]*$/.test(line.slice(head.length));
|
|
1641
1746
|
}
|
|
1642
|
-
async function readGitDiffs(repositoryRoot, paths) {
|
|
1747
|
+
async function readGitDiffs(repositoryRoot, paths, knownSecrets = []) {
|
|
1643
1748
|
if (paths.length === 0) return [];
|
|
1644
1749
|
const sections = [];
|
|
1645
1750
|
try {
|
|
1646
1751
|
const { stdout } = await execFileAsync(
|
|
1647
1752
|
"git",
|
|
1648
|
-
[
|
|
1753
|
+
[
|
|
1754
|
+
"-c",
|
|
1755
|
+
"core.quotePath=false",
|
|
1756
|
+
"diff",
|
|
1757
|
+
"HEAD",
|
|
1758
|
+
"--no-ext-diff",
|
|
1759
|
+
"--unified=3",
|
|
1760
|
+
"--",
|
|
1761
|
+
...paths
|
|
1762
|
+
],
|
|
1649
1763
|
{ cwd: repositoryRoot, maxBuffer: 2e6 }
|
|
1650
1764
|
);
|
|
1651
1765
|
sections.push(stdout);
|
|
1652
1766
|
} catch {
|
|
1653
1767
|
for (const args of [
|
|
1654
|
-
[
|
|
1655
|
-
|
|
1768
|
+
[
|
|
1769
|
+
"-c",
|
|
1770
|
+
"core.quotePath=false",
|
|
1771
|
+
"diff",
|
|
1772
|
+
"--cached",
|
|
1773
|
+
"--no-ext-diff",
|
|
1774
|
+
"--unified=3",
|
|
1775
|
+
"--",
|
|
1776
|
+
...paths
|
|
1777
|
+
],
|
|
1778
|
+
["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--unified=3", "--", ...paths]
|
|
1656
1779
|
]) {
|
|
1657
1780
|
try {
|
|
1658
1781
|
const { stdout } = await execFileAsync("git", args, {
|
|
@@ -1664,21 +1787,59 @@ async function readGitDiffs(repositoryRoot, paths) {
|
|
|
1664
1787
|
}
|
|
1665
1788
|
}
|
|
1666
1789
|
}
|
|
1667
|
-
const parsed = parseGitDiff(sections.join("\n"));
|
|
1790
|
+
const parsed = parseGitDiff(sections.join("\n"), knownSecrets);
|
|
1668
1791
|
const known = new Set(parsed.map((diff) => diff.path));
|
|
1669
1792
|
const untracked = await listUntrackedPaths(repositoryRoot, paths);
|
|
1670
1793
|
for (const path of untracked) {
|
|
1671
1794
|
if (known.has(path)) continue;
|
|
1672
|
-
const patch = await buildUntrackedPatch(repositoryRoot, path);
|
|
1795
|
+
const patch = isCredentialPath(path) ? withheldPatch(path) : await buildUntrackedPatch(repositoryRoot, path, knownSecrets);
|
|
1673
1796
|
if (patch) parsed.push(patch);
|
|
1674
1797
|
}
|
|
1675
1798
|
return parsed.slice(0, MAX_DIFF_FILES);
|
|
1676
1799
|
}
|
|
1677
|
-
function
|
|
1800
|
+
function diffHeaderPaths(section) {
|
|
1801
|
+
const quoted = '"(?:[^"\\\\]|\\\\.)*"';
|
|
1802
|
+
const header = new RegExp(`^diff --git (${quoted}|a/.+?) (${quoted}|b/.+?)\\n`, "m").exec(
|
|
1803
|
+
section
|
|
1804
|
+
);
|
|
1805
|
+
if (!header?.[1] || !header[2]) return null;
|
|
1806
|
+
const path = (raw) => (raw.startsWith('"') ? unquoteGitPath(raw) : raw).slice(2);
|
|
1807
|
+
return [header[0], path(header[1]), path(header[2])];
|
|
1808
|
+
}
|
|
1809
|
+
var C_ESCAPES = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 };
|
|
1810
|
+
function unquoteGitPath(raw) {
|
|
1811
|
+
const bytes = [];
|
|
1812
|
+
const body = raw.slice(1, -1);
|
|
1813
|
+
for (let at = 0; at < body.length; at++) {
|
|
1814
|
+
const char = body[at];
|
|
1815
|
+
if (char !== "\\") {
|
|
1816
|
+
bytes.push(...Buffer.from(char, "utf8"));
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const next = body[++at] ?? "";
|
|
1820
|
+
const octal = /^[0-7]{3}/.exec(body.slice(at))?.[0];
|
|
1821
|
+
if (octal) {
|
|
1822
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
1823
|
+
at += 2;
|
|
1824
|
+
} else bytes.push(C_ESCAPES[next] ?? next.charCodeAt(0));
|
|
1825
|
+
}
|
|
1826
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1827
|
+
}
|
|
1828
|
+
function withheldPatch(path) {
|
|
1829
|
+
return {
|
|
1830
|
+
path,
|
|
1831
|
+
patch: `diff --git a/${path} b/${path}
|
|
1832
|
+
(contents withheld: credential file)`
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function parseGitDiff(diff, knownSecrets = []) {
|
|
1678
1836
|
return diff.split(/(?=^diff --git )/m).filter((section) => section.startsWith("diff --git ")).flatMap((section) => {
|
|
1679
|
-
const header = section
|
|
1837
|
+
const header = diffHeaderPaths(section);
|
|
1680
1838
|
if (!header?.[2]) return [];
|
|
1681
|
-
|
|
1839
|
+
if (isCredentialPath(header[1] ?? "") || isCredentialPath(header[2])) {
|
|
1840
|
+
return [withheldPatch(header[2])];
|
|
1841
|
+
}
|
|
1842
|
+
const rawPatch = redactSourceText(section.trim(), knownSecrets, true);
|
|
1682
1843
|
const patch = rawPatch.slice(0, MAX_DIFF_CHARS_PER_FILE).trim();
|
|
1683
1844
|
return patch ? [
|
|
1684
1845
|
{
|
|
@@ -1693,20 +1854,20 @@ async function listUntrackedPaths(repositoryRoot, paths) {
|
|
|
1693
1854
|
try {
|
|
1694
1855
|
const { stdout } = await execFileAsync(
|
|
1695
1856
|
"git",
|
|
1696
|
-
["ls-files", "--others", "--exclude-standard", "--", ...paths],
|
|
1857
|
+
["ls-files", "-z", "--others", "--exclude-standard", "--", ...paths],
|
|
1697
1858
|
{ cwd: repositoryRoot, maxBuffer: 1e6 }
|
|
1698
1859
|
);
|
|
1699
|
-
return stdout.split(
|
|
1860
|
+
return stdout.split("\0").filter(Boolean);
|
|
1700
1861
|
} catch {
|
|
1701
1862
|
return [];
|
|
1702
1863
|
}
|
|
1703
1864
|
}
|
|
1704
|
-
async function buildUntrackedPatch(repositoryRoot, path) {
|
|
1865
|
+
async function buildUntrackedPatch(repositoryRoot, path, knownSecrets) {
|
|
1705
1866
|
try {
|
|
1706
|
-
const content = await
|
|
1707
|
-
if (content
|
|
1867
|
+
const content = await readBoundedSource(repositoryRoot, path);
|
|
1868
|
+
if (content === null) return null;
|
|
1708
1869
|
const lines = content.split(/\r?\n/);
|
|
1709
|
-
const body = lines.map((line) => `+${line}`).join("\n");
|
|
1870
|
+
const body = redactSourceText(lines.map((line) => `+${line}`).join("\n"), knownSecrets, true);
|
|
1710
1871
|
const rawPatch = [
|
|
1711
1872
|
`diff --git a/${path} b/${path}`,
|
|
1712
1873
|
"new file mode 100644",
|
|
@@ -1750,11 +1911,27 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
1750
1911
|
const incidentText = new Set(
|
|
1751
1912
|
eligible.filter((fact) => fact.category === "incident").map((fact) => normalizeText(fact.content || fact.object))
|
|
1752
1913
|
);
|
|
1753
|
-
const
|
|
1914
|
+
const terms = lexicalTerms(task).filter((term) => term.length > 2);
|
|
1915
|
+
const withTerms = eligible.filter((fact) => fact.category !== "incident").filter((fact) => !incidentText.has(normalizeText(fact.content || fact.object))).map((fact) => {
|
|
1754
1916
|
const text = `${fact.subject} ${fact.predicate} ${fact.object} ${fact.content}`;
|
|
1755
|
-
const
|
|
1917
|
+
const words2 = new Set(text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []);
|
|
1918
|
+
return { fact, text, shared: terms.filter((term) => words2.has(term)) };
|
|
1919
|
+
});
|
|
1920
|
+
const documentFrequency = new Map(
|
|
1921
|
+
terms.map((term) => [term, withTerms.filter(({ shared }) => shared.includes(term)).length])
|
|
1922
|
+
);
|
|
1923
|
+
const candidates = withTerms.map(({ fact, text, shared }) => {
|
|
1924
|
+
const taskScore = shared.length * 3;
|
|
1756
1925
|
const pathScore = fact.paths.some((path) => targetPaths.includes(path)) ? 5 : 0;
|
|
1757
|
-
|
|
1926
|
+
const admitted = admitsLexically(shared, documentFrequency, withTerms.length);
|
|
1927
|
+
return {
|
|
1928
|
+
fact,
|
|
1929
|
+
text,
|
|
1930
|
+
taskScore,
|
|
1931
|
+
pathScore,
|
|
1932
|
+
admitted,
|
|
1933
|
+
lexical: (admitted ? taskScore : 0) + pathScore
|
|
1934
|
+
};
|
|
1758
1935
|
});
|
|
1759
1936
|
const summarize = (fact, relevance, reason) => ({
|
|
1760
1937
|
id: fact.id,
|
|
@@ -1830,10 +2007,10 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
1830
2007
|
}
|
|
1831
2008
|
selected.push(candidate);
|
|
1832
2009
|
}
|
|
1833
|
-
return selected.map(({ fact, taskScore, pathScore, score }) => {
|
|
2010
|
+
return selected.map(({ fact, taskScore, pathScore, score, admitted }) => {
|
|
1834
2011
|
const evidence = [
|
|
1835
2012
|
pathScore > 0 ? "matches a target path" : "",
|
|
1836
|
-
taskScore > 0 ? `shares ${taskScore / 3} task term(s)` : "",
|
|
2013
|
+
admitted && taskScore > 0 ? `shares ${taskScore / 3} task term(s)` : "",
|
|
1837
2014
|
cosine(fact.id) !== void 0 ? `semantic ${cosine(fact.id)?.toFixed(2)}` : ""
|
|
1838
2015
|
].filter(Boolean);
|
|
1839
2016
|
return summarize(
|
|
@@ -1844,12 +2021,6 @@ async function rankRelevantFacts(facts, namespace, task, targetPaths, semanticRa
|
|
|
1844
2021
|
);
|
|
1845
2022
|
});
|
|
1846
2023
|
}
|
|
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
2024
|
function normalizeText(value) {
|
|
1854
2025
|
return value.toLowerCase().replace(/[^a-z0-9_./-]+/g, " ").trim();
|
|
1855
2026
|
}
|
|
@@ -1896,6 +2067,21 @@ var RULES = [
|
|
|
1896
2067
|
confidence: 0.8,
|
|
1897
2068
|
regex: EXPLICIT_RESOLUTION
|
|
1898
2069
|
},
|
|
2070
|
+
{
|
|
2071
|
+
// An explicit label ("Decision: …") is the agent saying this is durable.
|
|
2072
|
+
class: "decision",
|
|
2073
|
+
subject: "decision",
|
|
2074
|
+
predicate: "made",
|
|
2075
|
+
confidence: 0.8,
|
|
2076
|
+
regex: /^(?:decision|decided)\s*:/i
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
class: "fact",
|
|
2080
|
+
subject: "project",
|
|
2081
|
+
predicate: "fact",
|
|
2082
|
+
confidence: 0.75,
|
|
2083
|
+
regex: /^(?:convention|fact|rule|policy)\s*:/i
|
|
2084
|
+
},
|
|
1899
2085
|
{
|
|
1900
2086
|
class: "decision",
|
|
1901
2087
|
subject: "decision",
|
|
@@ -1915,11 +2101,20 @@ var RULES = [
|
|
|
1915
2101
|
subject: "project",
|
|
1916
2102
|
predicate: "fact",
|
|
1917
2103
|
confidence: 0.55,
|
|
1918
|
-
regex: /\b(uses?|is|are|requires?|depends on|stores?|returns?|implemented|added|supports?|configured|lives in|located|handles
|
|
2104
|
+
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
2105
|
}
|
|
1920
2106
|
];
|
|
1921
2107
|
var INCIDENT_REGEX = RULES.find((rule) => rule.predicate === "occurred").regex;
|
|
1922
2108
|
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;
|
|
2109
|
+
var HEDGED = /\b(might|maybe|perhaps|possibly|probably|could|consider(?:ing)?|planned|planning|would|whether|suppose|hypothetical(?:ly)?|in theory)\b/i;
|
|
2110
|
+
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;
|
|
2111
|
+
var DURABLE_LABEL = /^(?:decision|decided|rule|policy|convention|fact)\s*:/i;
|
|
2112
|
+
function stripDecoration(sentence) {
|
|
2113
|
+
return sentence.replace(/^(?:[-*•]\s+|\d+[.)]\s+)/, "").replace(
|
|
2114
|
+
/^(?:\*{1,2}|_{1,2})?(decision|decided|rule|policy|convention|fact)(?:\*{1,2}|_{1,2})?\s*:\s*(?:\*{1,2}|_{1,2})?\s*/i,
|
|
2115
|
+
"$1: "
|
|
2116
|
+
).trim();
|
|
2117
|
+
}
|
|
1923
2118
|
var NEGATION = /\b(no|not|never|none|nothing|nobody|without|zero|neither|nor|cannot)\b(?!\s+longer)|\w+n['’]t\b/i;
|
|
1924
2119
|
var EXAMPLE = /\b(?:e\.g|i\.e|for example|for instance|such as|example|sample|hypothetical)\b/i;
|
|
1925
2120
|
var QUOTED = /"[^"]*"|\u201c[^\u201d]*\u201d|`[^`]*`/g;
|
|
@@ -1946,17 +2141,19 @@ var SEVERITY_MARKERS = [
|
|
|
1946
2141
|
function deterministicExtract(input) {
|
|
1947
2142
|
const changedFiles = (input.changedFiles ?? []).filter(Boolean);
|
|
1948
2143
|
const candidates = [];
|
|
1949
|
-
for (const sentence of splitSentences(input.summary)) {
|
|
2144
|
+
for (const sentence of splitSentences(input.summary).map(stripDecoration)) {
|
|
1950
2145
|
const classified = classify(sentence);
|
|
1951
2146
|
const doubt = classified?.class === "incident" ? incidentDoubt(sentence) : null;
|
|
1952
|
-
const
|
|
2147
|
+
const speculative = DURABLE_LABEL.test(sentence) ? HEDGED.test(sentence) : SPECULATIVE.test(sentence) && !EXPLICIT_DECISION.test(sentence) && !EXPLICIT_RESOLUTION.test(sentence);
|
|
2148
|
+
const rule = doubt || speculative ? {
|
|
1953
2149
|
class: "note",
|
|
1954
2150
|
subject: "speculation",
|
|
1955
2151
|
predicate: "consider",
|
|
1956
2152
|
confidence: 0.3,
|
|
1957
2153
|
regex: SPECULATIVE
|
|
1958
2154
|
} : classified;
|
|
1959
|
-
|
|
2155
|
+
const passing = (rule?.class === "decision" || rule?.class === "fact") && NOT_DURABLE.test(sentence);
|
|
2156
|
+
if (!rule || passing) {
|
|
1960
2157
|
candidates.push(ignoredCandidate(sentence));
|
|
1961
2158
|
continue;
|
|
1962
2159
|
}
|
|
@@ -2094,7 +2291,7 @@ function matchResolution(fixText, pathCandidates, unresolved) {
|
|
|
2094
2291
|
|
|
2095
2292
|
// src/memory-extract/review-store.ts
|
|
2096
2293
|
import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, rename, rm as rm2 } from "fs/promises";
|
|
2097
|
-
import { dirname as
|
|
2294
|
+
import { dirname as dirname4, join as join5, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
|
|
2098
2295
|
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
2099
2296
|
import { z as z5 } from "zod";
|
|
2100
2297
|
var reviewSchema = z5.object({
|
|
@@ -2137,7 +2334,7 @@ var ReviewStore = class {
|
|
|
2137
2334
|
const input = redactPersisted(raw);
|
|
2138
2335
|
const path = this.itemPath(input.id);
|
|
2139
2336
|
await this.prepareDirectory();
|
|
2140
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
2337
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname4(path));
|
|
2141
2338
|
const frontMatter = stringifyYaml3({
|
|
2142
2339
|
schemaVersion: 1,
|
|
2143
2340
|
id: input.id,
|
|
@@ -2232,7 +2429,7 @@ var ReviewStore = class {
|
|
|
2232
2429
|
return absolute;
|
|
2233
2430
|
}
|
|
2234
2431
|
async prepareDirectory() {
|
|
2235
|
-
await assertNoSymlinkBetween(this.repositoryRoot,
|
|
2432
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname4(this.reviewDirectory));
|
|
2236
2433
|
await mkdir3(this.reviewDirectory, { recursive: true, mode: 448 });
|
|
2237
2434
|
}
|
|
2238
2435
|
};
|
|
@@ -2243,7 +2440,8 @@ var INJECTED_BLOCK = /^##\s*CodeBuddy\b/m;
|
|
|
2243
2440
|
var FENCED_CODE = /```[\s\S]*?(?:```|$)/g;
|
|
2244
2441
|
var QUOTE_LINE = /^\s*>.*$/gm;
|
|
2245
2442
|
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
|
|
2443
|
+
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;
|
|
2444
|
+
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
2445
|
var QUESTION_START = /^(?:why|how|what|when|where|which|who|can|could|would|will|do|does|did|is|are|should)\b/i;
|
|
2248
2446
|
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
2447
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
@@ -2271,21 +2469,31 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
2271
2469
|
function extractUserRules(prompts) {
|
|
2272
2470
|
const rules = /* @__PURE__ */ new Map();
|
|
2273
2471
|
const revocations = /* @__PURE__ */ new Map();
|
|
2472
|
+
const policies = /* @__PURE__ */ new Map();
|
|
2274
2473
|
for (const prompt of prompts) {
|
|
2275
2474
|
const own = (prompt.split(INJECTED_BLOCK)[0] ?? "").replace(FENCED_CODE, "\n").replace(QUOTE_LINE, "");
|
|
2276
2475
|
for (const raw of own.split(/(?<=[.!?])\s+|\n+/)) {
|
|
2277
|
-
const sentence = raw.trim();
|
|
2476
|
+
const sentence = stripDecoration(raw.trim());
|
|
2278
2477
|
if (!sentence || sentence.length > MAX_RULE_CHARS || sentence.endsWith("?")) continue;
|
|
2279
2478
|
if (QUESTION_START.test(sentence)) continue;
|
|
2280
2479
|
const key = sentence.toLowerCase().replace(/\s+/g, " ").replace(/[.!]+$/, "");
|
|
2281
2480
|
if (REVOCATION_MARKER.test(sentence)) {
|
|
2282
2481
|
if (!revocations.has(key)) revocations.set(key, sentence);
|
|
2283
|
-
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
if (HEDGED.test(sentence) || NOT_DURABLE.test(sentence)) continue;
|
|
2485
|
+
if (isRule(sentence)) {
|
|
2284
2486
|
if (!rules.has(key)) rules.set(key, sentence);
|
|
2487
|
+
} else if (POLICY.test(sentence) && !policies.has(key)) {
|
|
2488
|
+
policies.set(key, sentence);
|
|
2285
2489
|
}
|
|
2286
2490
|
}
|
|
2287
2491
|
}
|
|
2288
|
-
return {
|
|
2492
|
+
return {
|
|
2493
|
+
rules: [...rules.values()],
|
|
2494
|
+
revocations: [...revocations.values()],
|
|
2495
|
+
policies: [...policies.values()]
|
|
2496
|
+
};
|
|
2289
2497
|
}
|
|
2290
2498
|
function isRule(sentence) {
|
|
2291
2499
|
const rest = sentence.replace(RULE_PREFIX, "");
|
|
@@ -2307,6 +2515,7 @@ function revokes(revocation, rule) {
|
|
|
2307
2515
|
// src/memory-extract/engine.ts
|
|
2308
2516
|
var AUTO_SAVE_THRESHOLD = 0.7;
|
|
2309
2517
|
var STATED_CONFIDENCE = 0.9;
|
|
2518
|
+
var POLICY_CONFIDENCE = 0.6;
|
|
2310
2519
|
async function captureAfterTurn(input, options = {}) {
|
|
2311
2520
|
const repositoryRoot = resolve7(options.repositoryRoot ?? process.cwd());
|
|
2312
2521
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -2316,7 +2525,8 @@ async function captureAfterTurn(input, options = {}) {
|
|
|
2316
2525
|
const source = extracted.source;
|
|
2317
2526
|
const candidates = [
|
|
2318
2527
|
...extracted.candidates,
|
|
2319
|
-
...(input.userRules ?? []).map(ruleCandidate),
|
|
2528
|
+
...(input.userRules ?? []).map((rule) => ruleCandidate(rule)),
|
|
2529
|
+
...(input.userPolicies ?? []).map((policy) => ruleCandidate(policy, POLICY_CONFIDENCE)),
|
|
2320
2530
|
...await procedureCandidates(input, repositoryRoot)
|
|
2321
2531
|
];
|
|
2322
2532
|
const saved = [];
|
|
@@ -2391,14 +2601,14 @@ async function recordWorklog(input, saved, queued, repositoryRoot, now, reasons)
|
|
|
2391
2601
|
return null;
|
|
2392
2602
|
}
|
|
2393
2603
|
}
|
|
2394
|
-
function ruleCandidate(rule) {
|
|
2604
|
+
function ruleCandidate(rule, confidence = STATED_CONFIDENCE) {
|
|
2395
2605
|
return {
|
|
2396
2606
|
class: "decision",
|
|
2397
2607
|
content: rule,
|
|
2398
2608
|
subject: "rule",
|
|
2399
2609
|
predicate: "rule",
|
|
2400
2610
|
object: rule,
|
|
2401
|
-
confidence
|
|
2611
|
+
confidence,
|
|
2402
2612
|
paths: [],
|
|
2403
2613
|
severity: null
|
|
2404
2614
|
};
|
|
@@ -2720,50 +2930,6 @@ async function approveReviewItem(id, options = {}) {
|
|
|
2720
2930
|
return factId;
|
|
2721
2931
|
}
|
|
2722
2932
|
|
|
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
2933
|
// src/hooks/semantic.ts
|
|
2768
2934
|
import pTimeout from "p-timeout";
|
|
2769
2935
|
var CONNECT_BUDGET_MS = 2500;
|
|
@@ -2779,7 +2945,7 @@ async function configuredReranker(repositoryRoot) {
|
|
|
2779
2945
|
if (provider === "local") {
|
|
2780
2946
|
const revision = model === DEFAULT_RERANK_MODELS.local.id ? DEFAULT_RERANK_MODELS.local.revision : void 0;
|
|
2781
2947
|
return async (task, documents) => {
|
|
2782
|
-
const { rerankLocally } = await import("./local-
|
|
2948
|
+
const { rerankLocally } = await import("./local-ST4WMRCG.js");
|
|
2783
2949
|
return bounded(
|
|
2784
2950
|
rerankLocally({ id: model, ...revision ? { revision } : {} }, task, documents)
|
|
2785
2951
|
);
|
|
@@ -2787,7 +2953,7 @@ async function configuredReranker(repositoryRoot) {
|
|
|
2787
2953
|
}
|
|
2788
2954
|
const apiKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY : process.env.HF_TOKEN ?? file.provider?.apiKey;
|
|
2789
2955
|
if (!apiKey) return void 0;
|
|
2790
|
-
const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-
|
|
2956
|
+
const { rerankHuggingFace, rerankOpenRouter } = await import("./cloud-LXZUR3U2.js");
|
|
2791
2957
|
const call = provider === "openrouter" ? rerankOpenRouter : rerankHuggingFace;
|
|
2792
2958
|
return (task, documents) => call({ apiKey, model, timeoutMs: RERANK_BUDGET_MS }, task, documents);
|
|
2793
2959
|
}
|
|
@@ -2796,7 +2962,7 @@ async function openHookSemantics(repositoryRoot) {
|
|
|
2796
2962
|
const connect = async () => {
|
|
2797
2963
|
const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
|
|
2798
2964
|
if (!config.postgresUrl) return null;
|
|
2799
|
-
const { createRuntime } = await import("./operations-
|
|
2965
|
+
const { createRuntime } = await import("./operations-WJR7AZZF.js");
|
|
2800
2966
|
const opening = createRuntime(config, {
|
|
2801
2967
|
repositoryRoot,
|
|
2802
2968
|
startWorker: false,
|
|
@@ -2831,20 +2997,20 @@ async function openHookSemantics(repositoryRoot) {
|
|
|
2831
2997
|
}
|
|
2832
2998
|
|
|
2833
2999
|
// src/hooks/staging.ts
|
|
2834
|
-
import { mkdir as
|
|
2835
|
-
import { join as
|
|
3000
|
+
import { mkdir as mkdir4, readFile as readFile6, rename as rename2, rm as rm3, stat as stat3 } from "fs/promises";
|
|
3001
|
+
import { join as join6, resolve as resolve8 } from "path";
|
|
2836
3002
|
var STAGING_LOCK_STALE_MS = 1e4;
|
|
2837
3003
|
var HookStagingStore = class {
|
|
2838
3004
|
directory;
|
|
2839
3005
|
operations = Promise.resolve();
|
|
2840
3006
|
constructor(repositoryRoot = process.cwd()) {
|
|
2841
|
-
this.directory =
|
|
3007
|
+
this.directory = join6(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
|
|
2842
3008
|
}
|
|
2843
3009
|
async add(sessionId, paths) {
|
|
2844
3010
|
await this.serialize(async () => {
|
|
2845
3011
|
const clean = paths.map(normalize2).filter(Boolean);
|
|
2846
3012
|
if (clean.length === 0) return;
|
|
2847
|
-
await
|
|
3013
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
2848
3014
|
await this.withFileLock(sessionId, async () => {
|
|
2849
3015
|
const merged = [.../* @__PURE__ */ new Set([...await this.read(sessionId), ...clean])].sort();
|
|
2850
3016
|
await writeAtomic(
|
|
@@ -2857,7 +3023,7 @@ var HookStagingStore = class {
|
|
|
2857
3023
|
}
|
|
2858
3024
|
async read(sessionId) {
|
|
2859
3025
|
try {
|
|
2860
|
-
const parsed = JSON.parse(await
|
|
3026
|
+
const parsed = JSON.parse(await readFile6(this.filePath(sessionId), "utf8"));
|
|
2861
3027
|
return Array.isArray(parsed?.paths) ? parsed.paths.filter((p) => typeof p === "string") : [];
|
|
2862
3028
|
} catch {
|
|
2863
3029
|
return [];
|
|
@@ -2866,7 +3032,7 @@ var HookStagingStore = class {
|
|
|
2866
3032
|
/** Return the staged paths and clear them. */
|
|
2867
3033
|
async drain(sessionId) {
|
|
2868
3034
|
return this.serialize(async () => {
|
|
2869
|
-
await
|
|
3035
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
2870
3036
|
return this.withFileLock(sessionId, async () => {
|
|
2871
3037
|
const path = this.filePath(sessionId);
|
|
2872
3038
|
const draining = `${path}.${process.pid}.${Date.now()}.drain`;
|
|
@@ -2877,7 +3043,7 @@ var HookStagingStore = class {
|
|
|
2877
3043
|
throw error;
|
|
2878
3044
|
}
|
|
2879
3045
|
try {
|
|
2880
|
-
const parsed = JSON.parse(await
|
|
3046
|
+
const parsed = JSON.parse(await readFile6(draining, "utf8"));
|
|
2881
3047
|
return Array.isArray(parsed?.paths) ? parsed.paths.filter((item) => typeof item === "string") : [];
|
|
2882
3048
|
} finally {
|
|
2883
3049
|
await rm3(draining, { force: true });
|
|
@@ -2887,15 +3053,15 @@ var HookStagingStore = class {
|
|
|
2887
3053
|
}
|
|
2888
3054
|
/** Claim a batch without consuming edits staged while capture runs. */
|
|
2889
3055
|
async claim(sessionId) {
|
|
2890
|
-
await
|
|
3056
|
+
await mkdir4(this.directory, { recursive: true, mode: 448 });
|
|
2891
3057
|
const captureLock = `${this.filePath(sessionId)}.capture-lock`;
|
|
2892
3058
|
for (let attempt = 0; ; attempt++) {
|
|
2893
3059
|
try {
|
|
2894
|
-
await
|
|
3060
|
+
await mkdir4(captureLock);
|
|
2895
3061
|
break;
|
|
2896
3062
|
} catch (error) {
|
|
2897
3063
|
if (error.code !== "EEXIST") throw error;
|
|
2898
|
-
const age = Date.now() - (await
|
|
3064
|
+
const age = Date.now() - (await stat3(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
|
|
2899
3065
|
if (age > 6e4) {
|
|
2900
3066
|
await rm3(captureLock, { recursive: true, force: true });
|
|
2901
3067
|
continue;
|
|
@@ -2908,13 +3074,13 @@ var HookStagingStore = class {
|
|
|
2908
3074
|
return await this.withFileLock(sessionId, async () => {
|
|
2909
3075
|
const claimed = this.claimPath(sessionId);
|
|
2910
3076
|
try {
|
|
2911
|
-
return parsePaths(await
|
|
3077
|
+
return parsePaths(await readFile6(claimed, "utf8"));
|
|
2912
3078
|
} catch (error) {
|
|
2913
3079
|
if (error.code !== "ENOENT") throw error;
|
|
2914
3080
|
}
|
|
2915
3081
|
try {
|
|
2916
3082
|
await rename2(this.filePath(sessionId), claimed);
|
|
2917
|
-
return parsePaths(await
|
|
3083
|
+
return parsePaths(await readFile6(claimed, "utf8"));
|
|
2918
3084
|
} catch (error) {
|
|
2919
3085
|
if (error.code === "ENOENT") return [];
|
|
2920
3086
|
throw error;
|
|
@@ -2935,7 +3101,7 @@ var HookStagingStore = class {
|
|
|
2935
3101
|
/** Restore the claimed batch for retry, merging edits that arrived during capture. */
|
|
2936
3102
|
async rollback(sessionId) {
|
|
2937
3103
|
await this.withFileLock(sessionId, async () => {
|
|
2938
|
-
const claimed = await
|
|
3104
|
+
const claimed = await readFile6(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
|
|
2939
3105
|
const merged = [.../* @__PURE__ */ new Set([...claimed, ...await this.read(sessionId)])].sort();
|
|
2940
3106
|
if (merged.length > 0)
|
|
2941
3107
|
await writeAtomic(
|
|
@@ -2954,11 +3120,11 @@ var HookStagingStore = class {
|
|
|
2954
3120
|
const lock = `${this.filePath(sessionId)}.lock`;
|
|
2955
3121
|
for (let attempt = 0; ; attempt++) {
|
|
2956
3122
|
try {
|
|
2957
|
-
await
|
|
3123
|
+
await mkdir4(lock);
|
|
2958
3124
|
break;
|
|
2959
3125
|
} catch (error) {
|
|
2960
3126
|
if (error.code !== "EEXIST" || attempt >= 200) throw error;
|
|
2961
|
-
const age = await
|
|
3127
|
+
const age = await stat3(lock).then(
|
|
2962
3128
|
(info) => Date.now() - info.mtimeMs,
|
|
2963
3129
|
() => 0
|
|
2964
3130
|
);
|
|
@@ -2990,7 +3156,7 @@ var HookStagingStore = class {
|
|
|
2990
3156
|
}
|
|
2991
3157
|
filePath(sessionId) {
|
|
2992
3158
|
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "default";
|
|
2993
|
-
return
|
|
3159
|
+
return join6(this.directory, `${safe}.json`);
|
|
2994
3160
|
}
|
|
2995
3161
|
};
|
|
2996
3162
|
function normalize2(path) {
|
|
@@ -3003,6 +3169,7 @@ function parsePaths(raw) {
|
|
|
3003
3169
|
|
|
3004
3170
|
// src/hooks/runtime.ts
|
|
3005
3171
|
var CONTEXT_BLOCK_MAX_CHARS = 1800;
|
|
3172
|
+
var HOOK_TASK_FILES = 2;
|
|
3006
3173
|
var PROMPT_FACTS = 3;
|
|
3007
3174
|
var CONTEXT_HOOK_BUDGET_MS = 15e3;
|
|
3008
3175
|
async function resolveNamespace(repositoryRoot) {
|
|
@@ -3021,7 +3188,8 @@ async function runContextHook(input, emit, budgetMs = CONTEXT_HOOK_BUDGET_MS) {
|
|
|
3021
3188
|
beforeEdit = await buildBeforeEditContext({
|
|
3022
3189
|
repositoryRoot,
|
|
3023
3190
|
namespace,
|
|
3024
|
-
|
|
3191
|
+
// Git changes are the targets only without a prompt; a prompt that
|
|
3192
|
+
// matches no file gets none (an explicit useGit would force them back).
|
|
3025
3193
|
...input.prompt ? { task: input.prompt } : {},
|
|
3026
3194
|
...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
|
|
3027
3195
|
...semantics?.reranker ? { reranker: semantics.reranker } : {}
|
|
@@ -3066,16 +3234,20 @@ function recallTrace(input, beforeEdit, emitted, text, started, late) {
|
|
|
3066
3234
|
tokensOut: estimateTokens(text),
|
|
3067
3235
|
...input.session_id ? { session: input.session_id } : {},
|
|
3068
3236
|
detail: {
|
|
3069
|
-
prompt: prompt
|
|
3237
|
+
prompt: clip2(prompt, 120),
|
|
3070
3238
|
facts: facts.map((fact) => fact.summary.slice(0, 100)),
|
|
3071
3239
|
dense: facts.filter((fact) => fact.reason.includes("semantic")).length,
|
|
3072
3240
|
lexical: facts.filter((fact) => /task term|target path/.test(fact.reason)).length,
|
|
3073
3241
|
files: (beforeEdit?.targetPaths ?? []).slice(0, 3),
|
|
3074
3242
|
sections: emitted.length,
|
|
3243
|
+
...text.trim() ? { block: clip2(redactSensitive(text.trim()), 2e3) } : {},
|
|
3075
3244
|
...late ? { dropped: "built after the hook budget" } : {}
|
|
3076
3245
|
}
|
|
3077
3246
|
};
|
|
3078
3247
|
}
|
|
3248
|
+
function clip2(text, max) {
|
|
3249
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
3250
|
+
}
|
|
3079
3251
|
async function runStageHook(input) {
|
|
3080
3252
|
const repositoryRoot = hookRoot(input.cwd);
|
|
3081
3253
|
const paths = extractEditedPaths(input.tool_input).map((path) => toRepoRelative(repositoryRoot, path)).filter((path) => path !== null);
|
|
@@ -3084,7 +3256,7 @@ async function runStageHook(input) {
|
|
|
3084
3256
|
}
|
|
3085
3257
|
async function llmMemoryOptions(repositoryRoot) {
|
|
3086
3258
|
try {
|
|
3087
|
-
return await (await import("./memory-
|
|
3259
|
+
return await (await import("./memory-TGHVTBME.js")).llmCaptureOptions(repositoryRoot);
|
|
3088
3260
|
} catch {
|
|
3089
3261
|
return {};
|
|
3090
3262
|
}
|
|
@@ -3101,12 +3273,12 @@ async function runCaptureHook(input) {
|
|
|
3101
3273
|
const changedFiles = claimed;
|
|
3102
3274
|
try {
|
|
3103
3275
|
const prompts = input.transcript_path ? await turnUserPrompts(input.transcript_path) : [];
|
|
3104
|
-
const { rules, revocations } = extractUserRules(prompts);
|
|
3276
|
+
const { rules, revocations, policies } = extractUserRules(prompts);
|
|
3105
3277
|
let investigation = false;
|
|
3106
3278
|
if (changedFiles.length === 0) {
|
|
3107
3279
|
const assistant = input.transcript_path ? await lastAssistantMessage(input.transcript_path) : input.last_assistant_message?.trim() ?? "";
|
|
3108
3280
|
investigation = assistant.length >= INVESTIGATION_MIN_CHARS && NAMES_CODE.test(assistant);
|
|
3109
|
-
if (!investigation && rules.length === 0 && revocations.length === 0) {
|
|
3281
|
+
if (!investigation && rules.length === 0 && revocations.length === 0 && policies.length === 0) {
|
|
3110
3282
|
await staging.acknowledge(sessionId);
|
|
3111
3283
|
return { changedFiles, result: null };
|
|
3112
3284
|
}
|
|
@@ -3131,6 +3303,7 @@ async function runCaptureHook(input) {
|
|
|
3131
3303
|
sessionId,
|
|
3132
3304
|
...verification.length > 0 ? { verification } : {},
|
|
3133
3305
|
...rules.length > 0 ? { userRules: rules } : {},
|
|
3306
|
+
...policies.length > 0 ? { userPolicies: policies } : {},
|
|
3134
3307
|
...revocations.length > 0 ? { revokedRules: revocations } : {},
|
|
3135
3308
|
...investigation ? { investigation } : {},
|
|
3136
3309
|
// Without edits, only the user's own rules and a handoff are kept: an
|
|
@@ -3166,14 +3339,14 @@ var SYNC_BUDGET_MS = 5 * 6e4;
|
|
|
3166
3339
|
async function runSyncHook(repositoryRoot, options = {}) {
|
|
3167
3340
|
const config = await loadRuntimeConfig(repositoryRoot, { requireDatabase: false });
|
|
3168
3341
|
if (!config.postgresUrl) return null;
|
|
3169
|
-
const lock =
|
|
3170
|
-
const dirty =
|
|
3171
|
-
await
|
|
3342
|
+
const lock = join7(repositoryRoot, ".codebuddy", "cache", "sync.lock");
|
|
3343
|
+
const dirty = join7(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
|
|
3344
|
+
await mkdir5(dirname5(lock), { recursive: true });
|
|
3172
3345
|
if (!await takeLock(lock)) {
|
|
3173
3346
|
await writeFile(dirty, "").catch(() => void 0);
|
|
3174
3347
|
return null;
|
|
3175
3348
|
}
|
|
3176
|
-
const { createRuntime } = await import("./operations-
|
|
3349
|
+
const { createRuntime } = await import("./operations-WJR7AZZF.js");
|
|
3177
3350
|
const runtime = await createRuntime(config, { repositoryRoot }).catch(async (error) => {
|
|
3178
3351
|
await rm4(lock, { recursive: true, force: true });
|
|
3179
3352
|
throw error;
|
|
@@ -3205,10 +3378,23 @@ async function runSyncHook(repositoryRoot, options = {}) {
|
|
|
3205
3378
|
});
|
|
3206
3379
|
result = { reindex, drained };
|
|
3207
3380
|
round += 1;
|
|
3208
|
-
} while (round < 3 && await
|
|
3381
|
+
} while (round < 3 && await stat4(dirty).then(
|
|
3209
3382
|
() => true,
|
|
3210
3383
|
() => false
|
|
3211
3384
|
));
|
|
3385
|
+
if (config.provider.type === "local") {
|
|
3386
|
+
const started = Date.now();
|
|
3387
|
+
const vectors = await syncFileVectors(repositoryRoot, SYNC_BUDGET_MS);
|
|
3388
|
+
if (vectors) {
|
|
3389
|
+
await recordTrace(repositoryRoot, {
|
|
3390
|
+
kind: "sync",
|
|
3391
|
+
name: "file-vectors",
|
|
3392
|
+
ms: Date.now() - started,
|
|
3393
|
+
ok: true,
|
|
3394
|
+
detail: { ...vectors }
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3212
3398
|
return result;
|
|
3213
3399
|
} finally {
|
|
3214
3400
|
await runtime.close();
|
|
@@ -3218,24 +3404,39 @@ async function runSyncHook(repositoryRoot, options = {}) {
|
|
|
3218
3404
|
var SYNC_LOCK_STALE_MS = SYNC_BUDGET_MS + 6e4;
|
|
3219
3405
|
async function takeLock(path) {
|
|
3220
3406
|
try {
|
|
3221
|
-
await
|
|
3407
|
+
await mkdir5(path);
|
|
3222
3408
|
return true;
|
|
3223
3409
|
} catch {
|
|
3224
|
-
const age = await
|
|
3410
|
+
const age = await stat4(path).then(
|
|
3225
3411
|
(info) => Date.now() - info.mtimeMs,
|
|
3226
3412
|
() => 0
|
|
3227
3413
|
);
|
|
3228
3414
|
if (age < SYNC_LOCK_STALE_MS) return false;
|
|
3229
3415
|
await rm4(path, { recursive: true, force: true });
|
|
3230
|
-
return
|
|
3416
|
+
return mkdir5(path).then(
|
|
3231
3417
|
() => true,
|
|
3232
3418
|
() => false
|
|
3233
3419
|
);
|
|
3234
3420
|
}
|
|
3235
3421
|
}
|
|
3236
|
-
async function runResetHook(input) {
|
|
3422
|
+
async function runResetHook(input, emit) {
|
|
3237
3423
|
const repositoryRoot = hookRoot(input.cwd);
|
|
3238
|
-
|
|
3424
|
+
const sessionId = input.session_id ?? "default";
|
|
3425
|
+
const ledger = new SessionCapsuleLedger(repositoryRoot);
|
|
3426
|
+
await ledger.reset(sessionId);
|
|
3427
|
+
if (input.hook_event_name !== "SessionStart") return "";
|
|
3428
|
+
const namespace = await resolveNamespace(repositoryRoot);
|
|
3429
|
+
const bootstrap = await buildBootstrapContext({ repositoryRoot, namespace });
|
|
3430
|
+
const { text, emitted } = await renderContextBlock(
|
|
3431
|
+
bootstrap,
|
|
3432
|
+
null,
|
|
3433
|
+
ledger,
|
|
3434
|
+
sessionId,
|
|
3435
|
+
input.session_id !== void 0
|
|
3436
|
+
);
|
|
3437
|
+
if (text.trim()) await emit?.(text);
|
|
3438
|
+
await ledger.markSent(sessionId, emitted);
|
|
3439
|
+
return text;
|
|
3239
3440
|
}
|
|
3240
3441
|
function extractEditedPaths(toolInput) {
|
|
3241
3442
|
if (!toolInput) return [];
|
|
@@ -3279,7 +3480,7 @@ function isVerificationCommand(command) {
|
|
|
3279
3480
|
async function turnVerification(transcriptPath) {
|
|
3280
3481
|
let raw;
|
|
3281
3482
|
try {
|
|
3282
|
-
raw = await
|
|
3483
|
+
raw = await readFile7(transcriptPath, "utf8");
|
|
3283
3484
|
} catch {
|
|
3284
3485
|
return [];
|
|
3285
3486
|
}
|
|
@@ -3331,7 +3532,7 @@ var TAGGED_BLOCK = /<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi;
|
|
|
3331
3532
|
async function turnUserPrompts(transcriptPath) {
|
|
3332
3533
|
let raw;
|
|
3333
3534
|
try {
|
|
3334
|
-
raw = await
|
|
3535
|
+
raw = await readFile7(transcriptPath, "utf8");
|
|
3335
3536
|
} catch {
|
|
3336
3537
|
return [];
|
|
3337
3538
|
}
|
|
@@ -3362,7 +3563,7 @@ async function turnUserPrompts(transcriptPath) {
|
|
|
3362
3563
|
}
|
|
3363
3564
|
async function lastAssistantMessage(transcriptPath) {
|
|
3364
3565
|
try {
|
|
3365
|
-
const raw = await
|
|
3566
|
+
const raw = await readFile7(transcriptPath, "utf8");
|
|
3366
3567
|
let latest = "";
|
|
3367
3568
|
for (const line of raw.split("\n")) {
|
|
3368
3569
|
const trimmed = line.trim();
|
|
@@ -3506,7 +3707,7 @@ async function renderContextBlock(bootstrap, beforeEdit, ledger, sessionId, anno
|
|
|
3506
3707
|
items.push({
|
|
3507
3708
|
priority: 3,
|
|
3508
3709
|
text: bound(
|
|
3509
|
-
`${targetLabel}: ${beforeEdit.targetPaths.slice(0, 8).join(", ")}` + (tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""),
|
|
3710
|
+
`${targetLabel}: ${beforeEdit.targetPaths.slice(0, beforeEdit.pathSource === "task" ? HOOK_TASK_FILES : 8).join(", ")}` + (tests.length > 0 ? `; tests: ${tests.join(", ")}` : ""),
|
|
3510
3711
|
500
|
|
3511
3712
|
)
|
|
3512
3713
|
});
|
|
@@ -3555,11 +3756,6 @@ export {
|
|
|
3555
3756
|
PlanLockError,
|
|
3556
3757
|
PlanValidationError,
|
|
3557
3758
|
PlanFileStore,
|
|
3558
|
-
PlanTransitionError,
|
|
3559
|
-
PlanNotFoundError,
|
|
3560
|
-
ActivePlanExistsError,
|
|
3561
|
-
PlanLifecycle,
|
|
3562
|
-
enforceContextBudget,
|
|
3563
3759
|
RiskAssessor,
|
|
3564
3760
|
createChurnSignal,
|
|
3565
3761
|
createIncidentSignal,
|
|
@@ -3567,16 +3763,17 @@ export {
|
|
|
3567
3763
|
readPolicyFile,
|
|
3568
3764
|
assessRisk,
|
|
3569
3765
|
resolveRiskPaths,
|
|
3766
|
+
PlanTransitionError,
|
|
3767
|
+
PlanNotFoundError,
|
|
3768
|
+
ActivePlanExistsError,
|
|
3769
|
+
PlanLifecycle,
|
|
3770
|
+
enforceContextBudget,
|
|
3570
3771
|
WorklogStore,
|
|
3571
3772
|
buildBootstrapContext,
|
|
3572
3773
|
buildBeforeEditContext,
|
|
3573
3774
|
ReviewStore,
|
|
3574
3775
|
captureAfterTurn,
|
|
3575
3776
|
approveReviewItem,
|
|
3576
|
-
tracePath,
|
|
3577
|
-
recordTrace,
|
|
3578
|
-
readTraces,
|
|
3579
|
-
parseTraceLines,
|
|
3580
3777
|
configuredReranker,
|
|
3581
3778
|
openHookSemantics,
|
|
3582
3779
|
CONTEXT_HOOK_BUDGET_MS,
|
|
@@ -3594,4 +3791,4 @@ export {
|
|
|
3594
3791
|
lastAssistantMessage,
|
|
3595
3792
|
renderContextBlock
|
|
3596
3793
|
};
|
|
3597
|
-
//# sourceMappingURL=chunk-
|
|
3794
|
+
//# sourceMappingURL=chunk-ZNLSCV3J.js.map
|