@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.
Files changed (115) hide show
  1. package/README.md +10 -0
  2. package/dist/adapters-S4HHTWJW.js +24 -0
  3. package/dist/{chunk-IBLJOHJL.js → chunk-3QGW53GD.js} +2 -2
  4. package/dist/{chunk-IMFUYEZA.js → chunk-6DONJW3W.js} +232 -64
  5. package/dist/chunk-6DONJW3W.js.map +1 -0
  6. package/dist/{chunk-JFDRBPYF.js → chunk-6JCKMIQY.js} +2 -2
  7. package/dist/{chunk-S6VEG4S2.js → chunk-6KGYAFG7.js} +28 -5
  8. package/dist/chunk-6KGYAFG7.js.map +1 -0
  9. package/dist/{chunk-OWWCDRXW.js → chunk-6NZEZJYD.js} +9 -7
  10. package/dist/chunk-6NZEZJYD.js.map +1 -0
  11. package/dist/{chunk-GDJH7XJC.js → chunk-AL6VBTNA.js} +77 -3
  12. package/dist/chunk-AL6VBTNA.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-QBDJXGG5.js → chunk-EJQVGFH2.js} +93 -22
  20. package/dist/chunk-EJQVGFH2.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-5FW3PMNP.js → chunk-GXOML4UZ.js} +76 -11
  25. package/dist/chunk-GXOML4UZ.js.map +1 -0
  26. package/dist/{chunk-JPHJBCZB.js → chunk-HPOR6YXA.js} +3 -2
  27. package/dist/chunk-HPOR6YXA.js.map +1 -0
  28. package/dist/chunk-ITQO632W.js +673 -0
  29. package/dist/chunk-ITQO632W.js.map +1 -0
  30. package/dist/chunk-J4T6HHI4.js +1444 -0
  31. package/dist/chunk-J4T6HHI4.js.map +1 -0
  32. package/dist/{chunk-5KZY6OTG.js → chunk-KNR2P2UY.js} +33 -83
  33. package/dist/chunk-KNR2P2UY.js.map +1 -0
  34. package/dist/{chunk-2XDPKFUW.js → chunk-NY2YXP3R.js} +46 -20
  35. package/dist/chunk-NY2YXP3R.js.map +1 -0
  36. package/dist/{chunk-RAJJSCIP.js → chunk-SFMIMLXR.js} +2 -2
  37. package/dist/chunk-TGFTKWOR.js +16 -0
  38. package/dist/chunk-TGFTKWOR.js.map +1 -0
  39. package/dist/{chunk-NERKZSSE.js → chunk-TLWGZVPC.js} +395 -686
  40. package/dist/chunk-TLWGZVPC.js.map +1 -0
  41. package/dist/{chunk-B6CO63SQ.js → chunk-UK6XTS2Y.js} +24 -10
  42. package/dist/chunk-UK6XTS2Y.js.map +1 -0
  43. package/dist/chunk-W7GAXL6C.js +104 -0
  44. package/dist/chunk-W7GAXL6C.js.map +1 -0
  45. package/dist/chunk-XL2HLACE.js +55 -0
  46. package/dist/chunk-XL2HLACE.js.map +1 -0
  47. package/dist/chunk-Y7D7PUBE.js +321 -0
  48. package/dist/chunk-Y7D7PUBE.js.map +1 -0
  49. package/dist/{chunk-R7CD6EXS.js → chunk-ZNLSCV3J.js} +578 -381
  50. package/dist/chunk-ZNLSCV3J.js.map +1 -0
  51. package/dist/cli/index.js +5 -5
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/{cloud-7WM3YLI4.js → cloud-LXZUR3U2.js} +3 -3
  54. package/dist/{codebuddy-B_pCaQrV.d.ts → codebuddy-DO7ZhoWn.d.ts} +15 -1
  55. package/dist/config-GEZFLXK6.js +12 -0
  56. package/dist/index.d.ts +46 -3
  57. package/dist/index.js +23 -19
  58. package/dist/install-BXM56QCD.js +19 -0
  59. package/dist/langgraph/index.d.ts +1 -1
  60. package/dist/lean-command-7DFW32MA.js +19 -0
  61. package/dist/{local-5MZURLCZ.js → local-ST4WMRCG.js} +3 -3
  62. package/dist/{main-MTBQZGJV.js → main-QUASSFP7.js} +987 -1741
  63. package/dist/main-QUASSFP7.js.map +1 -0
  64. package/dist/{memory-KIC5FJVW.js → memory-TGHVTBME.js} +9 -8
  65. package/dist/memory-TGHVTBME.js.map +1 -0
  66. package/dist/operations-WJR7AZZF.js +26 -0
  67. package/dist/pretool-command-JTKX6G3M.js +13 -0
  68. package/dist/{rerank-5QZ3PNUW.js → rerank-IQAEWLQR.js} +6 -5
  69. package/dist/rerank-IQAEWLQR.js.map +1 -0
  70. package/dist/{runtime-CUBOOPZO.js → runtime-OQQ22VJF.js} +15 -11
  71. package/dist/summaries-ML53R7EL.js +21 -0
  72. package/dist/summaries-ML53R7EL.js.map +1 -0
  73. package/dist/{zvec-MSRKT7IV.js → zvec-XDZNZ2OL.js} +11 -3
  74. package/dist/zvec-XDZNZ2OL.js.map +1 -0
  75. package/package.json +1 -1
  76. package/src/db/migrations/0006_fact_idempotency_key.sql +2 -0
  77. package/src/db/migrations/meta/0006_snapshot.json +1258 -0
  78. package/src/db/migrations/meta/_journal.json +7 -0
  79. package/dist/chunk-2XDPKFUW.js.map +0 -1
  80. package/dist/chunk-5FW3PMNP.js.map +0 -1
  81. package/dist/chunk-5KZY6OTG.js.map +0 -1
  82. package/dist/chunk-B6CO63SQ.js.map +0 -1
  83. package/dist/chunk-GDJH7XJC.js.map +0 -1
  84. package/dist/chunk-IMFUYEZA.js.map +0 -1
  85. package/dist/chunk-J2EAZ4MA.js.map +0 -1
  86. package/dist/chunk-JPHJBCZB.js.map +0 -1
  87. package/dist/chunk-NERKZSSE.js.map +0 -1
  88. package/dist/chunk-OWWCDRXW.js.map +0 -1
  89. package/dist/chunk-PQK3GKZH.js +0 -98
  90. package/dist/chunk-PQK3GKZH.js.map +0 -1
  91. package/dist/chunk-QBDJXGG5.js.map +0 -1
  92. package/dist/chunk-R7CD6EXS.js.map +0 -1
  93. package/dist/chunk-S6VEG4S2.js.map +0 -1
  94. package/dist/chunk-XBJQKCLU.js.map +0 -1
  95. package/dist/config-DEVHUDIU.js +0 -11
  96. package/dist/lean-command-HTQCPH76.js +0 -15
  97. package/dist/main-MTBQZGJV.js.map +0 -1
  98. package/dist/memory-KIC5FJVW.js.map +0 -1
  99. package/dist/operations-POCHY3WR.js +0 -25
  100. package/dist/pretool-command-AX6JXP25.js +0 -10
  101. package/dist/rerank-5QZ3PNUW.js.map +0 -1
  102. package/dist/summaries-SB5L2TSW.js +0 -18
  103. /package/dist/{cloud-7WM3YLI4.js.map → adapters-S4HHTWJW.js.map} +0 -0
  104. /package/dist/{chunk-IBLJOHJL.js.map → chunk-3QGW53GD.js.map} +0 -0
  105. /package/dist/{chunk-JFDRBPYF.js.map → chunk-6JCKMIQY.js.map} +0 -0
  106. /package/dist/{chunk-5MQOE4J6.js.map → chunk-ESZXCZIN.js.map} +0 -0
  107. /package/dist/{chunk-RAJJSCIP.js.map → chunk-SFMIMLXR.js.map} +0 -0
  108. /package/dist/{config-DEVHUDIU.js.map → cloud-LXZUR3U2.js.map} +0 -0
  109. /package/dist/{lean-command-HTQCPH76.js.map → config-GEZFLXK6.js.map} +0 -0
  110. /package/dist/{local-5MZURLCZ.js.map → install-BXM56QCD.js.map} +0 -0
  111. /package/dist/{operations-POCHY3WR.js.map → lean-command-7DFW32MA.js.map} +0 -0
  112. /package/dist/{pretool-command-AX6JXP25.js.map → local-ST4WMRCG.js.map} +0 -0
  113. /package/dist/{runtime-CUBOOPZO.js.map → operations-WJR7AZZF.js.map} +0 -0
  114. /package/dist/{summaries-SB5L2TSW.js.map → pretool-command-JTKX6G3M.js.map} +0 -0
  115. /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-J2EAZ4MA.js";
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-NERKZSSE.js";
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-PQK3GKZH.js";
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-GDJH7XJC.js";
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-JPHJBCZB.js";
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-5KZY6OTG.js";
70
+ } from "./chunk-CG5B3LYG.js";
56
71
 
57
72
  // 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";
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 stat6 = perSignal.get(signal.id) ?? {
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
- stat6.contributions += 1;
529
- stat6.totalWeight += contribution.weight;
359
+ stat5.contributions += 1;
360
+ stat5.totalWeight += contribution.weight;
530
361
  }
531
- perSignal.set(signal.id, stat6);
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 since = options.since ?? "30 days ago";
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, since))
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.slice(0, 5).map((commit) => ({
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
- async function gitLogForPath(repositoryRoot, path, since) {
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
- `--since=${since}`,
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 readFile2 } from "fs/promises";
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 readFile2(path, "utf8");
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 ? [] : [createChurnSignal({ repositoryRoot })]
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(async (path) => {
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
- runGit2(repositoryRoot, ["diff", "--name-only", "HEAD"]),
882
- runGit2(repositoryRoot, ["ls-files", "--others", "--exclude-standard"])
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("\n"), ...untracked.stdout.split("\n")].map((path) => path.trim()).filter((path) => path && !path.startsWith(".codebuddy/"));
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 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";
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, dirname2(path));
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 readFile3(path, "utf8"));
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 targetPaths = hasExplicitTargets ? resolvedPaths : taskPaths.length > 0 ? taskPaths : resolvedPaths;
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(({ symbol }) => symbol.signature);
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
- ["diff", "HEAD", "--no-ext-diff", "--unified=3", "--", ...paths],
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
- ["diff", "--cached", "--no-ext-diff", "--unified=3", "--", ...paths],
1655
- ["diff", "--no-ext-diff", "--unified=3", "--", ...paths]
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 parseGitDiff(diff) {
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.match(/^diff --git a\/(.+?) b\/(.+?)\n/m);
1837
+ const header = diffHeaderPaths(section);
1680
1838
  if (!header?.[2]) return [];
1681
- const rawPatch = section.trim();
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(/\r?\n/).map((path) => path.trim()).filter(Boolean);
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 readFile4(join4(repositoryRoot, path), "utf8");
1707
- if (content.includes("\0")) return null;
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 candidates = eligible.filter((fact) => fact.category !== "incident").filter((fact) => !incidentText.has(normalizeText(fact.content || fact.object))).map((fact) => {
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 taskScore = relevanceScore(text, task);
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
- return { fact, text, taskScore, pathScore, lexical: taskScore + pathScore };
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?)\b/i
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 rule = doubt || SPECULATIVE.test(sentence) && !EXPLICIT_DECISION.test(sentence) && !EXPLICIT_RESOLUTION.test(sentence) ? {
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
- if (!rule) {
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 dirname3, join as join5, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
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, dirname3(path));
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, dirname3(this.reviewDirectory));
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+\S+\s+\S+|^(?:we|you)\s+(?:always|never|must)\b|^every\s+(?!time\b)\S+.*\bmust\b/i;
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
- } else if (isRule(sentence) && !SPECULATIVE.test(sentence)) {
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 { rules: [...rules.values()], revocations: [...revocations.values()] };
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: STATED_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-5MZURLCZ.js");
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-7WM3YLI4.js");
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-POCHY3WR.js");
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 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";
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 = join7(resolve8(repositoryRoot), ".codebuddy", "cache", "hooks");
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 mkdir5(this.directory, { recursive: true, mode: 448 });
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 readFile7(this.filePath(sessionId), "utf8"));
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 mkdir5(this.directory, { recursive: true, mode: 448 });
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 readFile7(draining, "utf8"));
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 mkdir5(this.directory, { recursive: true, mode: 448 });
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 mkdir5(captureLock);
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 stat4(captureLock).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
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 readFile7(claimed, "utf8"));
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 readFile7(claimed, "utf8"));
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 readFile7(this.claimPath(sessionId), "utf8").then(parsePaths).catch(() => []);
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 mkdir5(lock);
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 stat4(lock).then(
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 join7(this.directory, `${safe}.json`);
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
- useGit: true,
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.length > 120 ? `${prompt.slice(0, 119)}\u2026` : 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-KIC5FJVW.js")).llmCaptureOptions(repositoryRoot);
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 = join8(repositoryRoot, ".codebuddy", "cache", "sync.lock");
3170
- const dirty = join8(repositoryRoot, ".codebuddy", "cache", "sync.dirty");
3171
- await mkdir6(dirname5(lock), { recursive: true });
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-POCHY3WR.js");
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 stat5(dirty).then(
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 mkdir6(path);
3407
+ await mkdir5(path);
3222
3408
  return true;
3223
3409
  } catch {
3224
- const age = await stat5(path).then(
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 mkdir6(path).then(
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
- await new SessionCapsuleLedger(repositoryRoot).reset(input.session_id ?? "default");
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 readFile8(transcriptPath, "utf8");
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 readFile8(transcriptPath, "utf8");
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 readFile8(transcriptPath, "utf8");
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-R7CD6EXS.js.map
3794
+ //# sourceMappingURL=chunk-ZNLSCV3J.js.map