@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.
Files changed (104) hide show
  1. package/dist/{chunk-IBLJOHJL.js → chunk-3QGW53GD.js} +2 -2
  2. package/dist/{chunk-NERKZSSE.js → chunk-5EDGAZQN.js} +438 -68
  3. package/dist/chunk-5EDGAZQN.js.map +1 -0
  4. package/dist/{chunk-JFDRBPYF.js → chunk-6JCKMIQY.js} +2 -2
  5. package/dist/{chunk-2XDPKFUW.js → chunk-AK2UAE32.js} +46 -20
  6. package/dist/chunk-AK2UAE32.js.map +1 -0
  7. package/dist/{chunk-GDJH7XJC.js → chunk-AL6VBTNA.js} +77 -3
  8. package/dist/chunk-AL6VBTNA.js.map +1 -0
  9. package/dist/{chunk-QBDJXGG5.js → chunk-AQHPEJR6.js} +93 -22
  10. package/dist/chunk-AQHPEJR6.js.map +1 -0
  11. package/dist/{chunk-OWWCDRXW.js → chunk-BQM7FJ7D.js} +6 -6
  12. package/dist/chunk-BQM7FJ7D.js.map +1 -0
  13. package/dist/{chunk-XBJQKCLU.js → chunk-CAEPHLHC.js} +13 -9
  14. package/dist/chunk-CAEPHLHC.js.map +1 -0
  15. package/dist/chunk-CG5B3LYG.js +66 -0
  16. package/dist/chunk-CG5B3LYG.js.map +1 -0
  17. package/dist/{chunk-IHQM2ZDV.js → chunk-DFV2SK6C.js} +1 -1
  18. package/dist/{chunk-IHQM2ZDV.js.map → chunk-DFV2SK6C.js.map} +1 -1
  19. package/dist/{chunk-IMFUYEZA.js → chunk-EEUFU7KV.js} +81 -54
  20. package/dist/chunk-EEUFU7KV.js.map +1 -0
  21. package/dist/{chunk-5MQOE4J6.js → chunk-ESZXCZIN.js} +3 -3
  22. package/dist/{chunk-J2EAZ4MA.js → chunk-GKNYMNFM.js} +22 -25
  23. package/dist/chunk-GKNYMNFM.js.map +1 -0
  24. package/dist/{chunk-JPHJBCZB.js → chunk-HPOR6YXA.js} +3 -2
  25. package/dist/chunk-HPOR6YXA.js.map +1 -0
  26. package/dist/{chunk-S6VEG4S2.js → chunk-JUAFCLRB.js} +28 -5
  27. package/dist/chunk-JUAFCLRB.js.map +1 -0
  28. package/dist/{chunk-5KZY6OTG.js → chunk-KNR2P2UY.js} +33 -83
  29. package/dist/chunk-KNR2P2UY.js.map +1 -0
  30. package/dist/{chunk-B6CO63SQ.js → chunk-LCSBL5TT.js} +21 -9
  31. package/dist/chunk-LCSBL5TT.js.map +1 -0
  32. package/dist/{chunk-5FW3PMNP.js → chunk-MSUAVR3R.js} +76 -11
  33. package/dist/chunk-MSUAVR3R.js.map +1 -0
  34. package/dist/{chunk-RAJJSCIP.js → chunk-SFMIMLXR.js} +2 -2
  35. package/dist/chunk-TGFTKWOR.js +16 -0
  36. package/dist/chunk-TGFTKWOR.js.map +1 -0
  37. package/dist/chunk-TUR6YKEE.js +55 -0
  38. package/dist/chunk-TUR6YKEE.js.map +1 -0
  39. package/dist/chunk-Y7D7PUBE.js +321 -0
  40. package/dist/chunk-Y7D7PUBE.js.map +1 -0
  41. package/dist/{chunk-R7CD6EXS.js → chunk-YJIA2MFL.js} +572 -377
  42. package/dist/chunk-YJIA2MFL.js.map +1 -0
  43. package/dist/cli/index.js +5 -5
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/{cloud-7WM3YLI4.js → cloud-LXZUR3U2.js} +3 -3
  46. package/dist/{codebuddy-B_pCaQrV.d.ts → codebuddy-DO7ZhoWn.d.ts} +15 -1
  47. package/dist/config-GEZFLXK6.js +12 -0
  48. package/dist/index.d.ts +46 -3
  49. package/dist/index.js +22 -19
  50. package/dist/langgraph/index.d.ts +1 -1
  51. package/dist/lean-command-CWJAVQ5G.js +18 -0
  52. package/dist/{local-5MZURLCZ.js → local-ST4WMRCG.js} +3 -3
  53. package/dist/{main-MTBQZGJV.js → main-AHNGHC4L.js} +213 -183
  54. package/dist/main-AHNGHC4L.js.map +1 -0
  55. package/dist/{memory-KIC5FJVW.js → memory-TGHVTBME.js} +9 -8
  56. package/dist/memory-TGHVTBME.js.map +1 -0
  57. package/dist/operations-7BTAAUHF.js +26 -0
  58. package/dist/pretool-command-3SXQSMAO.js +13 -0
  59. package/dist/{rerank-5QZ3PNUW.js → rerank-IQAEWLQR.js} +6 -5
  60. package/dist/rerank-IQAEWLQR.js.map +1 -0
  61. package/dist/{runtime-CUBOOPZO.js → runtime-E7JQZ2BZ.js} +14 -11
  62. package/dist/summaries-XV3JGGCC.js +20 -0
  63. package/dist/{zvec-MSRKT7IV.js → zvec-KP4KNB7Y.js} +10 -3
  64. package/package.json +1 -1
  65. package/src/db/migrations/0006_fact_idempotency_key.sql +2 -0
  66. package/src/db/migrations/meta/0006_snapshot.json +1258 -0
  67. package/src/db/migrations/meta/_journal.json +7 -0
  68. package/dist/chunk-2XDPKFUW.js.map +0 -1
  69. package/dist/chunk-5FW3PMNP.js.map +0 -1
  70. package/dist/chunk-5KZY6OTG.js.map +0 -1
  71. package/dist/chunk-B6CO63SQ.js.map +0 -1
  72. package/dist/chunk-GDJH7XJC.js.map +0 -1
  73. package/dist/chunk-IMFUYEZA.js.map +0 -1
  74. package/dist/chunk-J2EAZ4MA.js.map +0 -1
  75. package/dist/chunk-JPHJBCZB.js.map +0 -1
  76. package/dist/chunk-NERKZSSE.js.map +0 -1
  77. package/dist/chunk-OWWCDRXW.js.map +0 -1
  78. package/dist/chunk-PQK3GKZH.js +0 -98
  79. package/dist/chunk-PQK3GKZH.js.map +0 -1
  80. package/dist/chunk-QBDJXGG5.js.map +0 -1
  81. package/dist/chunk-R7CD6EXS.js.map +0 -1
  82. package/dist/chunk-S6VEG4S2.js.map +0 -1
  83. package/dist/chunk-XBJQKCLU.js.map +0 -1
  84. package/dist/config-DEVHUDIU.js +0 -11
  85. package/dist/lean-command-HTQCPH76.js +0 -15
  86. package/dist/main-MTBQZGJV.js.map +0 -1
  87. package/dist/memory-KIC5FJVW.js.map +0 -1
  88. package/dist/operations-POCHY3WR.js +0 -25
  89. package/dist/pretool-command-AX6JXP25.js +0 -10
  90. package/dist/rerank-5QZ3PNUW.js.map +0 -1
  91. package/dist/summaries-SB5L2TSW.js +0 -18
  92. /package/dist/{chunk-IBLJOHJL.js.map → chunk-3QGW53GD.js.map} +0 -0
  93. /package/dist/{chunk-JFDRBPYF.js.map → chunk-6JCKMIQY.js.map} +0 -0
  94. /package/dist/{chunk-5MQOE4J6.js.map → chunk-ESZXCZIN.js.map} +0 -0
  95. /package/dist/{chunk-RAJJSCIP.js.map → chunk-SFMIMLXR.js.map} +0 -0
  96. /package/dist/{cloud-7WM3YLI4.js.map → cloud-LXZUR3U2.js.map} +0 -0
  97. /package/dist/{config-DEVHUDIU.js.map → config-GEZFLXK6.js.map} +0 -0
  98. /package/dist/{lean-command-HTQCPH76.js.map → lean-command-CWJAVQ5G.js.map} +0 -0
  99. /package/dist/{local-5MZURLCZ.js.map → local-ST4WMRCG.js.map} +0 -0
  100. /package/dist/{operations-POCHY3WR.js.map → operations-7BTAAUHF.js.map} +0 -0
  101. /package/dist/{pretool-command-AX6JXP25.js.map → pretool-command-3SXQSMAO.js.map} +0 -0
  102. /package/dist/{runtime-CUBOOPZO.js.map → runtime-E7JQZ2BZ.js.map} +0 -0
  103. /package/dist/{summaries-SB5L2TSW.js.map → summaries-XV3JGGCC.js.map} +0 -0
  104. /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-IHQM2ZDV.js";
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-J2EAZ4MA.js";
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-NERKZSSE.js";
39
+ } from "./chunk-5EDGAZQN.js";
23
40
  import {
24
41
  extractSymbolTable,
25
42
  supportsSymbols
26
- } from "./chunk-PQK3GKZH.js";
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-GDJH7XJC.js";
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-JPHJBCZB.js";
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-5KZY6OTG.js";
68
+ } from "./chunk-CG5B3LYG.js";
56
69
 
57
70
  // src/hooks/runtime.ts
58
- import { mkdir as mkdir6, readFile as readFile8, rm as rm4, stat as stat5, writeFile } from "fs/promises";
59
- import { basename, dirname as dirname5, isAbsolute, join as join8, relative as relative5, resolve as resolve9, sep as sep5 } from "path";
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 stat6 = perSignal.get(signal.id) ?? {
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
- stat6.contributions += 1;
529
- stat6.totalWeight += contribution.weight;
357
+ stat5.contributions += 1;
358
+ stat5.totalWeight += contribution.weight;
530
359
  }
531
- perSignal.set(signal.id, stat6);
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 since = options.since ?? "30 days ago";
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, since))
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.slice(0, 5).map((commit) => ({
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
- async function gitLogForPath(repositoryRoot, path, since) {
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
- `--since=${since}`,
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 readFile2 } from "fs/promises";
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 readFile2(path, "utf8");
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 ? [] : [createChurnSignal({ repositoryRoot })]
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(async (path) => {
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
- runGit2(repositoryRoot, ["diff", "--name-only", "HEAD"]),
882
- runGit2(repositoryRoot, ["ls-files", "--others", "--exclude-standard"])
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("\n"), ...untracked.stdout.split("\n")].map((path) => path.trim()).filter((path) => path && !path.startsWith(".codebuddy/"));
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 readFile3, stat as stat2 } from "fs/promises";
908
- import { dirname as dirname2, join as join3, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
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, dirname2(path));
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 readFile3(path, "utf8"));
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 targetPaths = hasExplicitTargets ? resolvedPaths : taskPaths.length > 0 ? taskPaths : resolvedPaths;
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(({ symbol }) => symbol.signature);
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
- ["diff", "HEAD", "--no-ext-diff", "--unified=3", "--", ...paths],
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
- ["diff", "--cached", "--no-ext-diff", "--unified=3", "--", ...paths],
1655
- ["diff", "--no-ext-diff", "--unified=3", "--", ...paths]
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 parseGitDiff(diff) {
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.match(/^diff --git a\/(.+?) b\/(.+?)\n/m);
1835
+ const header = diffHeaderPaths(section);
1680
1836
  if (!header?.[2]) return [];
1681
- const rawPatch = section.trim();
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(/\r?\n/).map((path) => path.trim()).filter(Boolean);
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 readFile4(join4(repositoryRoot, path), "utf8");
1707
- if (content.includes("\0")) return null;
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 candidates = eligible.filter((fact) => fact.category !== "incident").filter((fact) => !incidentText.has(normalizeText(fact.content || fact.object))).map((fact) => {
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 taskScore = relevanceScore(text, task);
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
- return { fact, text, taskScore, pathScore, lexical: taskScore + pathScore };
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?)\b/i
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 rule = doubt || SPECULATIVE.test(sentence) && !EXPLICIT_DECISION.test(sentence) && !EXPLICIT_RESOLUTION.test(sentence) ? {
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
- if (!rule) {
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 dirname3, join as join5, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
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, dirname3(path));
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, dirname3(this.reviewDirectory));
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+\S+\s+\S+|^(?:we|you)\s+(?:always|never|must)\b|^every\s+(?!time\b)\S+.*\bmust\b/i;
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
- } else if (isRule(sentence) && !SPECULATIVE.test(sentence)) {
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 { rules: [...rules.values()], revocations: [...revocations.values()] };
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: STATED_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-5MZURLCZ.js");
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-7WM3YLI4.js");
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-POCHY3WR.js");
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 mkdir5, readFile as readFile7, rename as rename2, rm as rm3, stat as stat4 } from "fs/promises";
2835
- import { join as join7, resolve as resolve8 } from "path";
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 = join7(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
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 mkdir5(this.directory, { recursive: true, mode: 448 });
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 readFile7(this.filePath(sessionId), "utf8"));
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 mkdir5(this.directory, { recursive: true, mode: 448 });
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 readFile7(draining, "utf8"));
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 mkdir5(this.directory, { recursive: true, mode: 448 });
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 mkdir5(captureLock);
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 stat4(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
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 readFile7(claimed, "utf8"));
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 readFile7(claimed, "utf8"));
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 readFile7(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
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 mkdir5(lock);
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 stat4(lock).then(
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 join7(this.directory, `${safe}.json`);
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
- useGit: true,
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.length > 120 ? `${prompt.slice(0, 119)}\u2026` : 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-KIC5FJVW.js")).llmCaptureOptions(repositoryRoot);
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 = join8(repositoryRoot, ".codebuddy", "cache", "sync.lock");
3170
- const dirty = join8(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
3171
- await mkdir6(dirname5(lock), { recursive: true });
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-POCHY3WR.js");
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 stat5(dirty).then(
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 mkdir6(path);
3405
+ await mkdir5(path);
3222
3406
  return true;
3223
3407
  } catch {
3224
- const age = await stat5(path).then(
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 mkdir6(path).then(
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
- await new SessionCapsuleLedger(repositoryRoot).reset(input.session_id ?? "default");
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 readFile8(transcriptPath, "utf8");
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 readFile8(transcriptPath, "utf8");
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 readFile8(transcriptPath, "utf8");
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-R7CD6EXS.js.map
3792
+ //# sourceMappingURL=chunk-YJIA2MFL.js.map