@orquentra/codebuddy 2.9.0 → 2.15.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 (117) hide show
  1. package/ARCHITECTURE.md +98 -0
  2. package/CLAUDE_CODEX.md +33 -0
  3. package/README.md +114 -9
  4. package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
  5. package/dist/{chunk-GXOML4UZ.js → chunk-4BSO3HST.js} +126 -18
  6. package/dist/chunk-4BSO3HST.js.map +1 -0
  7. package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
  8. package/dist/chunk-57642NGU.js.map +1 -0
  9. package/dist/{chunk-ESZXCZIN.js → chunk-6YI2D5F5.js} +33 -5
  10. package/dist/chunk-6YI2D5F5.js.map +1 -0
  11. package/dist/{chunk-GKNYMNFM.js → chunk-7IPSDDO5.js} +10 -3
  12. package/dist/chunk-7IPSDDO5.js.map +1 -0
  13. package/dist/chunk-BTTQWSDS.js +372 -0
  14. package/dist/chunk-BTTQWSDS.js.map +1 -0
  15. package/dist/{chunk-2WH2Q4TA.js → chunk-D5E7SGI5.js} +304 -77
  16. package/dist/chunk-D5E7SGI5.js.map +1 -0
  17. package/dist/chunk-EMWGLKPR.js +45 -0
  18. package/dist/chunk-EMWGLKPR.js.map +1 -0
  19. package/dist/chunk-GQKWUHMA.js +20 -0
  20. package/dist/chunk-GQKWUHMA.js.map +1 -0
  21. package/dist/chunk-ITXE27SZ.js +358 -0
  22. package/dist/chunk-ITXE27SZ.js.map +1 -0
  23. package/dist/{chunk-UK6XTS2Y.js → chunk-JJQDVGF7.js} +23 -23
  24. package/dist/chunk-JJQDVGF7.js.map +1 -0
  25. package/dist/{chunk-F4BJ3DXW.js → chunk-KQCP3ZYH.js} +1273 -331
  26. package/dist/chunk-KQCP3ZYH.js.map +1 -0
  27. package/dist/{chunk-QN76UJP7.js → chunk-KYRM3HPP.js} +89 -21
  28. package/dist/chunk-KYRM3HPP.js.map +1 -0
  29. package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
  30. package/dist/chunk-M75SN4D4.js.map +1 -0
  31. package/dist/{chunk-J4T6HHI4.js → chunk-RZ266NHE.js} +538 -317
  32. package/dist/chunk-RZ266NHE.js.map +1 -0
  33. package/dist/chunk-U7KDM62P.js +43 -0
  34. package/dist/chunk-U7KDM62P.js.map +1 -0
  35. package/dist/chunk-UATJCD2T.js +368 -0
  36. package/dist/chunk-UATJCD2T.js.map +1 -0
  37. package/dist/{chunk-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
  38. package/dist/chunk-UOTKHSQ5.js.map +1 -0
  39. package/dist/{chunk-TLWGZVPC.js → chunk-UR3DF74I.js} +1007 -132
  40. package/dist/chunk-UR3DF74I.js.map +1 -0
  41. package/dist/chunk-UTPC66MV.js +128 -0
  42. package/dist/chunk-UTPC66MV.js.map +1 -0
  43. package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
  44. package/dist/chunk-V7PT6ER5.js.map +1 -0
  45. package/dist/chunk-WHKXUXHE.js +156 -0
  46. package/dist/chunk-WHKXUXHE.js.map +1 -0
  47. package/dist/{chunk-SRA4MDVG.js → chunk-YMXFSHXT.js} +978 -950
  48. package/dist/chunk-YMXFSHXT.js.map +1 -0
  49. package/dist/cli/index.js +23 -9
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
  52. package/dist/config-TXQJCCVI.js +15 -0
  53. package/dist/expand-KWYGNCKZ.js +98 -0
  54. package/dist/expand-KWYGNCKZ.js.map +1 -0
  55. package/dist/file-vectors-MKXTSGGH.js +19 -0
  56. package/dist/index-facts-4MTEHKMN.js +38 -0
  57. package/dist/index-pass-TBDUEZCB.js +14 -0
  58. package/dist/index.d.ts +6 -4
  59. package/dist/index.js +25 -23
  60. package/dist/install-E3JWINTN.js +31 -0
  61. package/dist/langgraph/index.d.ts +1 -1
  62. package/dist/lean-MLTAFS5L.js +29 -0
  63. package/dist/local-75DJECVR.js +48 -0
  64. package/dist/local-llm-MECVJTVU.js +19 -0
  65. package/dist/{main-YJKSYFPT.js → main-RLMKQZW4.js} +999 -328
  66. package/dist/main-RLMKQZW4.js.map +1 -0
  67. package/dist/{memory-TGHVTBME.js → memory-PFFVMTCH.js} +9 -6
  68. package/dist/{memory-TGHVTBME.js.map → memory-PFFVMTCH.js.map} +1 -1
  69. package/dist/{operations-OG4ISWIM.js → operations-STR7S5UO.js} +8 -7
  70. package/dist/operations-STR7S5UO.js.map +1 -0
  71. package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
  72. package/dist/pretool-command-B5GY5T6E.js.map +1 -0
  73. package/dist/prompt-gate-TDTFFQW2.js +8 -0
  74. package/dist/prompt-gate-TDTFFQW2.js.map +1 -0
  75. package/dist/{rerank-IQAEWLQR.js → rerank-XXKWDI5Q.js} +7 -4
  76. package/dist/{rerank-IQAEWLQR.js.map → rerank-XXKWDI5Q.js.map} +1 -1
  77. package/dist/{runtime-GCSEIAVD.js → runtime-SWFWGTQ4.js} +18 -9
  78. package/dist/runtime-SWFWGTQ4.js.map +1 -0
  79. package/dist/summaries-QXWFFDUZ.js +31 -0
  80. package/dist/summaries-QXWFFDUZ.js.map +1 -0
  81. package/dist/{zvec-XDZNZ2OL.js → zvec-F73IBWO5.js} +13 -6
  82. package/dist/zvec-F73IBWO5.js.map +1 -0
  83. package/package.json +13 -3
  84. package/dist/chunk-2WH2Q4TA.js.map +0 -1
  85. package/dist/chunk-6NZEZJYD.js +0 -206
  86. package/dist/chunk-6NZEZJYD.js.map +0 -1
  87. package/dist/chunk-CAEPHLHC.js.map +0 -1
  88. package/dist/chunk-ENKDFLOO.js.map +0 -1
  89. package/dist/chunk-ESZXCZIN.js.map +0 -1
  90. package/dist/chunk-F4BJ3DXW.js.map +0 -1
  91. package/dist/chunk-GKNYMNFM.js.map +0 -1
  92. package/dist/chunk-GXOML4UZ.js.map +0 -1
  93. package/dist/chunk-ITQO632W.js +0 -673
  94. package/dist/chunk-ITQO632W.js.map +0 -1
  95. package/dist/chunk-J4T6HHI4.js.map +0 -1
  96. package/dist/chunk-KNR2P2UY.js.map +0 -1
  97. package/dist/chunk-QN76UJP7.js.map +0 -1
  98. package/dist/chunk-SFMIMLXR.js +0 -63
  99. package/dist/chunk-SFMIMLXR.js.map +0 -1
  100. package/dist/chunk-SRA4MDVG.js.map +0 -1
  101. package/dist/chunk-TLWGZVPC.js.map +0 -1
  102. package/dist/chunk-UK6XTS2Y.js.map +0 -1
  103. package/dist/chunk-W7GAXL6C.js.map +0 -1
  104. package/dist/config-GEZFLXK6.js +0 -12
  105. package/dist/install-BXM56QCD.js +0 -19
  106. package/dist/lean-command-7DFW32MA.js +0 -19
  107. package/dist/main-YJKSYFPT.js.map +0 -1
  108. package/dist/summaries-ML53R7EL.js +0 -21
  109. /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
  110. /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
  111. /package/dist/{install-BXM56QCD.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
  112. /package/dist/{lean-command-7DFW32MA.js.map → index-facts-4MTEHKMN.js.map} +0 -0
  113. /package/dist/{operations-OG4ISWIM.js.map → index-pass-TBDUEZCB.js.map} +0 -0
  114. /package/dist/{pretool-command-PRFOIHMW.js.map → install-E3JWINTN.js.map} +0 -0
  115. /package/dist/{runtime-GCSEIAVD.js.map → lean-MLTAFS5L.js.map} +0 -0
  116. /package/dist/{summaries-ML53R7EL.js.map → local-75DJECVR.js.map} +0 -0
  117. /package/dist/{zvec-XDZNZ2OL.js.map → local-llm-MECVJTVU.js.map} +0 -0
package/ARCHITECTURE.md CHANGED
@@ -113,6 +113,104 @@ Rationale:
113
113
 
114
114
  - better to return slightly stale ranking than block on Hugging Face cold starts or rate limits
115
115
 
116
+ ## Local LLM and Intent Cards (2.15)
117
+
118
+ **Runtime: node-llama-cpp.** The local small model (Qwen3-0.6B Q4_K_M by
119
+ default; Qwen3-1.7B as `better`; Qwen only) runs through
120
+ node-llama-cpp 3.22 (llama.cpp bindings). It was compared with Cactus on the
121
+ four criteria in the 2.15 brief:
122
+
123
+ | | node-llama-cpp 3.22.3 | Cactus |
124
+ |---|---|---|
125
+ | Install size | 41 MB package + one prebuilt binary (19 MB mac-arm64-metal, 33–34 MB x64) | npm ships `cactus-react-native` (152 MB, mobile) and `cactus-needle` (tool-calling model only) |
126
+ | Cold start (M-series, Qwen3-0.6B) | 441 ms load, measured | no Node server binding to measure |
127
+ | Tokens/s (M-series, Metal) | 100–200 tok/s measured; ~2 s per card incl. JSON grammar | n/a |
128
+ | Prebuilt targets | macOS arm64/x64, Linux x64/arm64, Windows x64 (+CUDA/Vulkan) | iOS/Android focus |
129
+
130
+ Cactus has no maintained Node.js binding for a desktop/server process, and its
131
+ Node package hosts only the Needle tool-calling model, which the brief excludes
132
+ (the job is plain English → code vocabulary, not tool selection). So
133
+ node-llama-cpp. It is not a package dependency: `codebuddy use` (or
134
+ `codebuddy models add local-llm`) installs it with `npm -g` and downloads the pinned GGUF into
135
+ `~/.cache/codebuddy/models/gguf/`, checking size and sha256 before moving it
136
+ into place. Forcing `gpu: false` on macOS makes node-llama-cpp compile llama.cpp
137
+ from source (34 s first load), so the backend uses `gpu: "auto"` (Metal on Apple
138
+ Silicon, CPU elsewhere) with `threads` capped at cores − 1
139
+ (`CODEBUDDY_LLM_THREADS` overrides).
140
+
141
+ **Where it runs.** Generation happens on llama.cpp's native threads inside
142
+ `codebuddy serve` (background project sync) or `codebuddy index --cards`, never
143
+ on a request path except the bounded query expansion below. A single context
144
+ sequence serialises calls. Hooks and MCP tool calls touch
145
+ `.codebuddy/cache/foreground`; the card and fact passes wait (up to 15 s) while
146
+ it is fresher than 2 s. Output is constrained by a JSON-schema grammar with
147
+ `maxItems`/`maxLength` bounds (without them the 0.6B model writes paragraphs
148
+ until the token cap cuts the JSON), then validated with zod; invalid output is
149
+ retried once, then the file is skipped.
150
+
151
+ **Lifetime.** The model is loaded on demand and unloaded `IDLE_UNLOAD_MS`
152
+ (60 s) after its last generation; measured on Qwen3-0.6B, resident memory went
153
+ from 1,274 MB to 452 MB (the rest is the llama.cpp/Metal runtime, freed when
154
+ the process exits). `serve` frees it on shutdown, and the detached
155
+ `index --cards` that setup starts exits when its pass ends. Setup skips the
156
+ local model on machines under 4 GB RAM or with one core (`machineCanCard`) and
157
+ uses cloud Qwen there when a key is set.
158
+
159
+ **Model size.** The same four-file card test with the grammar bounds:
160
+ SmolLM2-135M Q4_K_M (105 MB) wrote `...` summaries and raw code lines as
161
+ symptoms; Gemma 3 270M Q4_K_M (253 MB) wrote one-word summaries and term lists
162
+ as symptoms; Qwen3-0.6B Q4_K_M (397 MB) wrote usable summaries and symptoms.
163
+ So CodeBuddy ships Qwen only. A machine below `machineCanCard` gets Qwen from
164
+ the cloud instead (BYOK: `qwen/qwen3.8-27b` on Groq or OpenRouter, sent with
165
+ `reasoning_effort: "none"`; `<think>` blocks are stripped), configured by
166
+ setup only when a provider key is already in the environment.
167
+
168
+ **Cards** extend the existing `llm-summaries.json` store (prompt version 2), so
169
+ the BYOK and local backends fill the same cache. Each card is used three ways:
170
+ a card vector per file in `file-vectors.json` (granite, next to the chunk
171
+ vectors), one Markdown file per source file in `.codebuddy/cache/cards/`, which
172
+ is its own zvec-grep workspace (English model `local/bge-small-en-v1.5`,
173
+ `--no-ignore`, so the project's ignore rules never hide it), and card terms and
174
+ symptoms in the content index at half weight.
175
+
176
+ **Agreement rule** (`context/discovery.ts`). 2.14 dropped every vector hit
177
+ outside the keyword set unless it shared task terms with the source and cleared
178
+ `zvec.minScore`. That deleted exactly the hits a reworded task needs. In 2.15 a
179
+ candidate is admitted when at least two signals put it in their top k (default
180
+ 5): zvec-grep (code and card workspaces fused), the source-chunk vector, the
181
+ card vector, and the old term-overlap rule as one more vote. Every ranking has
182
+ a top k even for an unrelated task, so a vector vote also needs its cosine at
183
+ or above a floor (`chunkFloor` 0.80, `cardFloor` 0.75). zg prints no
184
+ similarity, so its vote is rank-only, and two rank-only votes never agree.
185
+ Abstention returns nothing only when no candidate passes the rule and keyword
186
+ evidence is below the floor. Card text alone never admits a file; card
187
+ evidence counts when another signal agrees. The floors are uncalibrated
188
+ defaults. Tune them in `.codebuddy/config.json` under `discovery.agreement`
189
+ against the unrelated-task files (the 6 in the Litestar harness plus
190
+ `evals/unrelated-2.15.json`).
191
+
192
+ **Confidence.** Discovery returns `high` (anchor, exact identifier, CLI
193
+ command, or ≥ 3 votes counting a strong keyword match as two), `medium` (two),
194
+ or `low` (dense signals present but no returned file has two). `low` returns no
195
+ paths: the hook emits nothing, and `context_pack` adds a note telling the agent
196
+ to search normally. Without any dense signal the result is 2.14's keyword path
197
+ and is never suppressed.
198
+
199
+ **Expansion.** When a task found nothing confident and names no identifier,
200
+ discovery asks the warm local model for 5–8 code terms (≤ 800 ms in
201
+ `context_pack`, ≤ 300 ms in the hook) and ranks once more. Answers are cached
202
+ per normalised task in `.codebuddy/cache/expansions.json` (LRU, 200). The hook
203
+ process never has a warm model, so it only benefits from cached expansions.
204
+
205
+ **Index facts** (`llm/index-facts.ts`) are Markdown facts with
206
+ `createdByAgent: codebuddy-index`, `paths: [source]`,
207
+ `verification: ["source:Lline"]` and an `index:` idempotency key.
208
+ Manifest and CI facts are read deterministically. Prose facts come from the
209
+ model and must share words with the cited line and read as a statement.
210
+ Sensitive candidates are dropped. Re-extraction replaces only index facts
211
+ whose source hash changed. The cap is 40 per repository, and near-duplicates
212
+ (Jaccard ≥ 0.8) of any existing fact are skipped.
213
+
116
214
  ## Sharing Model
117
215
 
118
216
  Shares are references by default.
package/CLAUDE_CODEX.md CHANGED
@@ -343,6 +343,28 @@ CODEBUDDY_PROJECT_ROOT = "/absolute/path/to/your-project"
343
343
 
344
344
  Restart Codex after installing or changing an MCP entry.
345
345
 
346
+ Native project hooks are available in Codex 0.169.0. To retain task checkpoints
347
+ with minimal prompt injection:
348
+
349
+ ```bash
350
+ codebuddy clients install codex --mode lean --capture
351
+ ```
352
+
353
+ Review the commands in Codex `/hooks` and approve them through Codex's trust UI.
354
+ CodeBuddy writes `.codex/hooks.json`, preserves unrelated hooks, and never marks
355
+ its own commands trusted. Without that review, automatic recovery is not active.
356
+ Visible prompts, edits, explicit command outcomes and final answers are journaled;
357
+ private reasoning and unstable rollout formats are not required. Compaction and
358
+ startup/resume can restore unfinished task state. Host-payload tests pass; a live
359
+ agent A/B has not yet verified correctness, token savings or cost savings.
360
+ `codebuddy clients verify codex` checks installation, not trust approval.
361
+
362
+ OpenCode and Hermes capture-enabled plugins use the same checkpoint bridge.
363
+ Reinstall existing plugins to obtain it; capability reporting distinguishes older
364
+ plugins. Disable capture with `--no-capture`; this also disables checkpoint capture
365
+ and recovery. Hermes rehydrates its ephemeral prompt context from visible events.
366
+
367
+
346
368
  ## Useful CLI Commands
347
369
 
348
370
  List namespaces:
@@ -433,3 +455,14 @@ codebuddy doctor --skip-model-check
433
455
  ```
434
456
 
435
457
  Database and MCP setup can still be valid even when live Hugging Face diagnostics are temporarily unavailable.
458
+
459
+ ## Read guard modes
460
+
461
+ Large unranged Claude `Read` calls are initially bounded to 200 lines, with an
462
+ explicit continuation hint. Any explicit offset or limit passes unchanged; request
463
+ the full line count when the whole file is needed. No permission approval is
464
+ emitted. `CODEBUDDY_TOOL_GUARD=strict` restores the legacy outline-and-deny mode;
465
+ `CODEBUDDY_TOOL_GUARD=0` disables the guard. The default leaves Bash commands
466
+ unchanged and caps content-mode `Grep` calls only. Shell `grep` output is not
467
+ silently capped because pipelines and exit status can be part of correctness.
468
+ Live-host turn and token effects remain unmeasured.
package/README.md CHANGED
@@ -1,11 +1,18 @@
1
- # CodeBuddy
1
+ # CodeBuddy by Orquentra
2
2
 
3
3
  Local-first project context and memory for AI coding agents.
4
4
 
5
+ Published by Orquentra as
6
+ [`@orquentra/codebuddy` on npm](https://www.npmjs.com/package/@orquentra/codebuddy).
7
+
5
8
  CodeBuddy helps agents find relevant code, understand project constraints, and
6
9
  remember decisions across sessions. It works through MCP with Claude Code,
7
10
  Codex, OpenCode, Hermes, and other compatible clients.
8
11
 
12
+ [npm package](https://www.npmjs.com/package/@orquentra/codebuddy) ·
13
+ [Documentation](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/guide.md) ·
14
+ [Source and issues](https://github.com/Orquentra-Inc/CodeBuddy)
15
+
9
16
  ## Install
10
17
 
11
18
  Requires Node.js 20 or later.
@@ -16,9 +23,12 @@ cd your-project
16
23
  codebuddy
17
24
  ```
18
25
 
19
- The setup wizard configures the project and agent integration. Project context
20
- works without a database; semantic memory recall optionally uses PostgreSQL
21
- with pgvector and a local Hugging Face embedding model (no token needed).
26
+ That one command sets everything up: PostgreSQL (the bundled Docker database
27
+ if none is running), the local embedding model (~90 MB), zvec-grep (installed
28
+ and indexed), the local Qwen model (~400 MB; cloud Qwen with a key you enter on
29
+ machines under 4 GB RAM), agent wiring for Claude and Codex, and a background
30
+ pass that builds file vectors, intent cards and project facts. Skip pieces with
31
+ `--no-local-llm`, `--no-graphify` or `--no-docker`.
22
32
 
23
33
  Run setup inside the project you want CodeBuddy to read. Claude Desktop
24
34
  registration saves that folder's absolute path, so restarting the desktop app
@@ -28,17 +38,111 @@ Older desktop registrations without a project path can be repaired by running
28
38
  `codebuddy claude install` from the initialized project folder, then restarting
29
39
  Claude Desktop.
30
40
 
41
+ ### Verify the package
42
+
43
+ The npm package name is **`@orquentra/codebuddy`**; the installed command is
44
+ **`codebuddy`**. Open the [npm package page](https://www.npmjs.com/package/@orquentra/codebuddy)
45
+ directly to find its published versions, or query the public registry:
46
+
47
+ ```bash
48
+ npm view @orquentra/codebuddy version --registry=https://registry.npmjs.org/
49
+ ```
50
+
51
+ Search engine results and AI summaries may lag behind npm releases. If an
52
+ installation fails, include the exact npm error and requested version in a
53
+ [GitHub issue](https://github.com/Orquentra-Inc/CodeBuddy/issues).
54
+
31
55
  ## What it does
32
56
 
33
57
  - **Task context:** relevant files, symbols, code excerpts, nearby dependencies,
34
58
  and covering tests within a token budget.
35
59
  - **Project memory:** decisions, rules, and handoffs stored as readable Markdown,
36
- recalled on every prompt by hybrid lexical + vector search
37
- ([how it works](docs/semantic-memory.md)).
60
+ recalled for relevant tasks by hybrid lexical + vector search
61
+ ([how it works](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/semantic-memory.md)).
62
+ - **Continuity:** a local journal of each session's goal, edits, checks and
63
+ findings, re-injected after compaction and offered to the next session when
64
+ a task was left unfinished. Native bridges cover Claude Code, Codex, OpenCode
65
+ and Hermes; live-host recovery and agent savings are not yet measured.
66
+ - **Capture controls:** Claude Code, Codex, OpenCode and Hermes can keep turn capture
67
+ enabled with lean prompt injection (`clients install <client> --mode lean --capture`).
68
+ Codex project hooks require review in `/hooks`; installation does not bypass trust.
38
69
  - **Change guidance:** plans, policies, incident history, and risk signals.
70
+ - **Reuse hints:** exported helpers matched lexically, with a conservative local
71
+ semantic fallback after indexing. A five-query smoke test admitted 2/5 correct
72
+ suggestions and withheld three; neither of two unrelated queries was admitted.
73
+ This is small-sample evidence, not an agent savings result.
39
74
  - **Optional retrieval:** zvec semantic search, Graphify architecture maps, and
40
75
  a bring-your-own LLM layer.
41
76
 
77
+ ## Optional local LLM (2.15)
78
+
79
+ A small local model can write a plain-English **intent card** for every source
80
+ file, so a task worded nothing like the code (for example "the browser forgets
81
+ how long to wait before reconnecting") still finds the right file. `codebuddy`
82
+ / `codebuddy use` installs it as part of setup (Qwen3-0.6B Q4_K_M, ~400 MB,
83
+ checksum-verified; skip with `--no-local-llm` or `CODEBUDDY_LOCAL_LLM=0`), and it
84
+ runs offline after that. To manage it by hand:
85
+
86
+ ```bash
87
+ codebuddy models add local-llm # Qwen3-0.6B Q4_K_M, ~400 MB, checksum-verified
88
+ codebuddy models add local-llm --size better # Qwen3-1.7B, ~1.1 GB
89
+ codebuddy models remove local-llm # back to keyword + vector discovery
90
+ ```
91
+
92
+ Setup also installs the runtime (`node-llama-cpp`, prebuilt for macOS, Linux
93
+ and Windows; `models add` asks first). CodeBuddy uses Qwen only. On a machine
94
+ under 4 GB of RAM or with one CPU core, setup uses Qwen in the cloud instead
95
+ (`qwen/qwen3.8-27b` on Groq, free-tier key; or OpenRouter). It uses
96
+ `GROQ_API_KEY` / `OPENROUTER_API_KEY` from the environment, or asks for a Groq
97
+ key (free at console.groq.com/keys) and saves it in `~/.codebuddy/global.json`
98
+ (mode 0600, never in the project). Bounded, redacted file heads and
99
+ signatures are then sent to that provider ([details](docs/llm-byok.md)). Skip
100
+ the prompt and keyword + vector discovery keep working.
101
+ The model runs inside `codebuddy serve` (not in Docker; Docker only hosts
102
+ PostgreSQL). It is unloaded 60 s after its last card and freed when the agent
103
+ closes `serve`, so it never keeps running in the background.
104
+
105
+ Qwen3-0.6B is the smallest model that wrote usable cards in our checks:
106
+ SmolLM2-135M (105 MB) returned placeholders and raw code lines, and Gemma 3
107
+ 270M returned keyword lists instead of descriptions, so neither ships.
108
+ Nothing leaves your machine, and without the model every feature falls back to
109
+ the 2.14 behaviour.
110
+
111
+ - **Intent cards:** a summary, 3–6 "symptoms" in bug-report wording, the code
112
+ terms to search for, and a line per main symbol. Written in the background by
113
+ `codebuddy serve` (or `codebuddy index --cards`) for new and changed source
114
+ files only; tests, docs, generated, vendored and secret-bearing files are
115
+ skipped. Cards feed a second vector per file, a zvec-grep card workspace, and
116
+ (down-weighted) the keyword index. Progress: `codebuddy models`, `codebuddy
117
+ storage`.
118
+ - **Agreement rule:** a file the keywords missed is admitted when two
119
+ independent signals agree (zvec-grep, source-chunk vectors, card vectors,
120
+ term overlap). When no returned file has two, CodeBuddy says so instead of
121
+ guessing: `context_pack` returns `confidence: "low"` with a note to search
122
+ normally, and the prompt hook stays silent.
123
+ - **Index facts:** up to 40 project facts drawn from `package.json`,
124
+ `pyproject.toml`, CI workflows, README / CONTRIBUTING / AGENTS.md /
125
+ CLAUDE.md and deprecation or release policies, each with its source line.
126
+ They are ordinary Markdown facts; `codebuddy memory list --origin index`
127
+ shows them and `codebuddy memory prune --origin index` removes them. Facts a
128
+ person or agent wrote are never replaced.
129
+
130
+ `codebuddy use` also builds the zvec-grep index and enables it when `zg` 0.2.x
131
+ is installed, and `codebuddy serve` keeps a `zg server` running so the prompt
132
+ hook gets vector candidates within its deadline.
133
+
134
+ ## What it costs
135
+
136
+ Context is not free: tool schemas, the rules block and the injected context are
137
+ re-read on every turn. A 2026-11 A/B on 23 real Litestar bug fixes (Claude Code,
138
+ Sonnet) found CodeBuddy 2.9 cost a median 22% more per task than stock Claude
139
+ Code with no change in pass rate or exploration. 2.10 cuts the fixed overhead
140
+ from about 4,250 to about 400 tokens per session; that it now breaks even or
141
+ saves is not yet measured. On small, well-specified tasks expect near-zero
142
+ overhead, not savings; savings need vague tasks, large repositories or
143
+ cross-session memory. Method and pre-registered criteria:
144
+ [docs/benchmarks](docs/benchmarks/).
145
+
42
146
  ## See what it does
43
147
 
44
148
  ```bash
@@ -68,13 +172,14 @@ codebuddy init --non-interactive
68
172
  codebuddy context preview "fix the login flow"
69
173
  ```
70
174
 
71
- Agents use `context_pack` to request task evidence and `context_after_turn` to
72
- capture useful knowledge. Request `evidence: "source"` when a short excerpt
175
+ By default the MCP server lists three tools: `context_pack` for task evidence,
176
+ and `recall` / `remember` for project memory (`CODEBUDDY_TOOL_PROFILE=full`
177
+ lists all of them). Request `evidence: "source"` when a short excerpt
73
178
  needs more detail. Inspect missing or truncated evidence before editing.
74
179
 
75
180
  ## Documentation
76
181
 
77
- - [Full guide and examples](https://github.com/Orquentra-Inc/CodeBuddy/blob/348bb7c/README.md)
182
+ - [Full guide and examples](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/guide.md)
78
183
  - [Telemetry and the admin panel](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/telemetry.md)
79
184
  - [Token budgets](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/token-budgets.md)
80
185
  - [Optional LLM setup](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/llm-byok.md)
@@ -8,7 +8,7 @@ import {
8
8
  parseMode,
9
9
  sharedRulesReaders,
10
10
  supportedModes
11
- } from "./chunk-W7GAXL6C.js";
11
+ } from "./chunk-V7PT6ER5.js";
12
12
  import "./chunk-PZ5AY32C.js";
13
13
  export {
14
14
  AUTOMATION_MODES,
@@ -21,4 +21,4 @@ export {
21
21
  sharedRulesReaders,
22
22
  supportedModes
23
23
  };
24
- //# sourceMappingURL=adapters-S4HHTWJW.js.map
24
+ //# sourceMappingURL=adapters-OJRSVFV3.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  asksForChangelog
3
- } from "./chunk-TLWGZVPC.js";
3
+ } from "./chunk-UR3DF74I.js";
4
4
  import {
5
5
  readConfigFile
6
- } from "./chunk-KNR2P2UY.js";
6
+ } from "./chunk-M75SN4D4.js";
7
7
  import {
8
8
  writeAtomic
9
9
  } from "./chunk-CG5B3LYG.js";
@@ -52,10 +52,10 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
52
52
  const config = await zvecConfig(root);
53
53
  if (!config.enabled) return none("disabled", "zvec-grep not enabled");
54
54
  const { deadlineAt } = options;
55
- const run = (args) => {
55
+ const run = (args, cwd = root) => {
56
56
  const ms = deadlineAt === void 0 ? config.timeoutMs : Math.min(config.timeoutMs, deadlineAt - Date.now());
57
- if (ms <= 0) return Promise.resolve({ stdout: "", error: deadlineError() });
58
- return runZg(config.binary, args, root, ms);
57
+ if (ms <= 0) return Promise.resolve({ stdout: "", stderr: "", error: deadlineError() });
58
+ return runZg(config.binary, args, cwd, ms);
59
59
  };
60
60
  const missedDeadline = (status) => deadlineAt !== void 0 && status === "timeout" && Date.now() >= deadlineAt - 5;
61
61
  const cached = await readCachedReadiness(root, config.binary);
@@ -73,7 +73,7 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
73
73
  const limit = Math.min(20, Math.max(1, Math.floor(options.limit ?? 10)));
74
74
  const chunkLimit = limit * 8;
75
75
  const globs = zvecExcludeGlobs(task);
76
- const result = await run([
76
+ const query = [
77
77
  "query",
78
78
  "--vector",
79
79
  task,
@@ -84,13 +84,20 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
84
84
  "--limit",
85
85
  String(chunkLimit),
86
86
  ...globs.flatMap((glob) => ["-g", glob])
87
- ]);
87
+ ];
88
+ const cardQuery = queryCards(root, task, limit, (args, cwd) => run(args, cwd));
89
+ let result = await run(query);
90
+ if (result.error && isBusy(result) && (deadlineAt === void 0 || deadlineAt - Date.now() > 400)) {
91
+ await new Promise((resolveWait) => setTimeout(resolveWait, BUSY_RETRY_MS));
92
+ result = await run(query);
93
+ }
88
94
  if (result.error) {
89
- const failure = fromError(result.error, "zvec-grep query");
90
- if (!missedDeadline(failure.status)) await resetZvecCache(root);
95
+ const failure = fromError(result.error, "zvec-grep query", result.stderr);
96
+ if (!missedDeadline(failure.status) && failure.status !== "busy") await resetZvecCache(root);
91
97
  return failure;
92
98
  }
93
99
  const hits = parseHits(result.stdout).slice(0, chunkLimit);
100
+ const cardPaths = await cardQuery;
94
101
  if (hits.length === 0 && /\bzg index\b/.test(result.stdout)) {
95
102
  const notReady = none("index-not-ready", NOT_READY);
96
103
  await markReadiness(root, config.binary, notReady);
@@ -133,7 +140,8 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
133
140
  stale,
134
141
  changed,
135
142
  minScore: config.minScore,
136
- reason: `zvec-grep vector search: ${paths.length} file(s)` + (globs.length > 0 ? "; non-source paths excluded" : "") + (stale > 0 ? `, ${stale} stale hit(s) dropped` : "") + (changed > 0 ? `, ${changed} hit(s) changed since the zg index dropped` : "") + (indexedAt === null ? "; index age unknown, hits unverified (run `codebuddy zvec index`)" : "")
143
+ ...cardPaths.length > 0 ? { cardPaths } : {},
144
+ reason: `zvec-grep vector search: ${paths.length} file(s)` + (globs.length > 0 ? "; non-source paths excluded" : "") + (stale > 0 ? `, ${stale} stale hit(s) dropped` : "") + (changed > 0 ? `, ${changed} hit(s) changed since the zg index dropped` : "") + (cardPaths.length > 0 ? `; ${cardPaths.length} intent card match(es)` : "") + (indexedAt === null ? "; index age unknown, hits unverified (run `codebuddy zvec index`)" : "")
137
145
  };
138
146
  }
139
147
  async function refreshZvecIndex(repositoryRoot, options = {}) {
@@ -229,6 +237,90 @@ async function buildZvecIndex(repositoryRoot, options = {}) {
229
237
  return code;
230
238
  }
231
239
  var NOT_READY = "zvec-grep index missing or not ready; run `codebuddy zvec index`";
240
+ var CARDS_DIR = join(".codebuddy", "cache", "cards");
241
+ var DEFAULT_CARD_MODEL = "local/bge-small-en-v1.5";
242
+ async function queryCards(root, task, limit, run) {
243
+ const cards = join(root, CARDS_DIR);
244
+ if (!await stat(join(cards, ".zvec-grep")).catch(() => null)) return [];
245
+ const result = await run(
246
+ ["query", "--vector", task, "--mode", "auto", "--refresh", "off", "--limit", String(limit)],
247
+ cards
248
+ );
249
+ if (result.error) return [];
250
+ const paths = [];
251
+ for (const hit of parseHits(result.stdout)) {
252
+ const card = toRepoRelative(cards, hit.path);
253
+ if (!card?.endsWith(".md")) continue;
254
+ const path = card.slice(0, -3);
255
+ if (paths.includes(path)) continue;
256
+ if (!(await stat(resolve(root, path)).catch(() => null))?.isFile()) continue;
257
+ paths.push(path);
258
+ }
259
+ return paths.slice(0, limit);
260
+ }
261
+ async function refreshZvecCardIndex(repositoryRoot, options = {}) {
262
+ const root = resolve(repositoryRoot);
263
+ const config = await zvecConfig(root);
264
+ if (!config.enabled) return { status: "disabled", reason: "zvec-grep not enabled" };
265
+ const cards = join(root, CARDS_DIR);
266
+ if (!await stat(cards).catch(() => null))
267
+ return { status: "ok", reason: "no intent cards yet" };
268
+ const exists = await stat(join(cards, ".zvec-grep")).catch(() => null);
269
+ const result = await runZg(
270
+ config.binary,
271
+ [
272
+ "index",
273
+ cards,
274
+ ...exists ? [] : ["--embedding", config.cardModel],
275
+ "--no-ignore",
276
+ "--mode",
277
+ "auto"
278
+ ],
279
+ cards,
280
+ REFRESH_TIMEOUT_MS,
281
+ options.signal
282
+ );
283
+ if (result.error) {
284
+ const failure = fromError(result.error, "zvec-grep card index", result.stderr);
285
+ return { status: failure.status, reason: failure.reason };
286
+ }
287
+ await writeFile(join(cards, ".zvec-grep", ".gitignore"), "*\n", { flag: "wx" }).catch(() => {
288
+ });
289
+ return { status: "ok", reason: "zvec-grep card index refreshed" };
290
+ }
291
+ async function ensureZvecServer(repositoryRoot) {
292
+ const root = resolve(repositoryRoot);
293
+ const config = await zvecConfig(root);
294
+ const noop = async () => {
295
+ };
296
+ if (!config.enabled) return { started: false, reason: "zvec-grep not enabled", stop: noop };
297
+ const run = (args) => runZg(config.binary, args, root, Math.max(config.timeoutMs, 5e3));
298
+ const readiness = await checkReadiness(run);
299
+ if (readiness) return { started: false, reason: readiness.reason, stop: noop };
300
+ const status = await run(["server", "status"]);
301
+ if (!status.error && /^Server: ready\b/m.test(status.stdout)) {
302
+ return { started: false, reason: "zg server already running (reused)", stop: noop };
303
+ }
304
+ const on = await run(["server", "on"]);
305
+ await rm(serverPath(root), { force: true });
306
+ if (on.error) {
307
+ return {
308
+ started: false,
309
+ reason: fromError(on.error, "zg server on", on.stderr).reason,
310
+ stop: noop
311
+ };
312
+ }
313
+ return {
314
+ started: true,
315
+ reason: "zg server started",
316
+ // ponytail: the daemon is shared; a second `codebuddy serve` that reused it
317
+ // loses vectors when this one exits. Refcount via ZVEC_GREP_HOME if that matters.
318
+ stop: async () => {
319
+ await runZg(config.binary, ["server", "off"], root, 5e3);
320
+ await rm(serverPath(root), { force: true });
321
+ }
322
+ };
323
+ }
232
324
  async function checkReadiness(run) {
233
325
  const version = await run(["version"]);
234
326
  if (version.error) return fromError(version.error, "zvec-grep version check");
@@ -306,7 +398,8 @@ async function zvecConfig(root) {
306
398
  enabled: env !== void 0 ? env === "1" || env === "true" : file.zvec?.enabled ?? false,
307
399
  binary: file.zvec?.binary ?? "zg",
308
400
  timeoutMs: file.zvec?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
309
- minScore: file.zvec?.minScore ?? DEFAULT_ZVEC_MIN_SCORE
401
+ minScore: file.zvec?.minScore ?? DEFAULT_ZVEC_MIN_SCORE,
402
+ cardModel: file.zvec?.cardModel ?? DEFAULT_CARD_MODEL
310
403
  };
311
404
  }
312
405
  function runZg(binary, args, cwd, timeoutMs, signal) {
@@ -321,12 +414,16 @@ function runZg(binary, args, cwd, timeoutMs, signal) {
321
414
  killSignal: "SIGKILL",
322
415
  ...signal ? { signal } : {}
323
416
  },
324
- (error, stdout) => {
417
+ (error, stdout, stderr) => {
325
418
  if (error && error.killed) {
326
- resolveRun({ stdout: "", error: Object.assign(error, { code: "ETIMEDOUT" }) });
419
+ resolveRun({
420
+ stdout: "",
421
+ stderr: String(stderr),
422
+ error: Object.assign(error, { code: "ETIMEDOUT" })
423
+ });
327
424
  return;
328
425
  }
329
- resolveRun({ stdout: String(stdout), error });
426
+ resolveRun({ stdout: String(stdout), stderr: String(stderr), error });
330
427
  }
331
428
  );
332
429
  });
@@ -360,10 +457,17 @@ async function serverRunning(root, binary, run) {
360
457
  }
361
458
  return running;
362
459
  }
363
- function fromError(error, what) {
460
+ function fromError(error, what, stderr = "") {
364
461
  if (error.code === "ENOENT") return none("not-installed", "zvec-grep (`zg`) not found on PATH");
365
462
  if (error.code === "ETIMEDOUT") return none("timeout", `${what} timed out`);
366
- return none("failed", `${what} failed`);
463
+ if (BUSY.test(stderr)) return none("busy", `${what}: index busy (a zg index refresh is running)`);
464
+ const code = stderr.match(/\bCode:\s*(\S+)/)?.[1];
465
+ return none("failed", `${what} failed${code ? ` (${code})` : ""}`);
466
+ }
467
+ var BUSY = /ZVEC_GREP\.ENGINE\.LOCK\.BUSY/;
468
+ var BUSY_RETRY_MS = 150;
469
+ function isBusy(result) {
470
+ return BUSY.test(result.stderr);
367
471
  }
368
472
  function none(status, reason) {
369
473
  return { status, paths: [], hits: [], reason, stale: 0, changed: 0 };
@@ -384,6 +488,10 @@ export {
384
488
  parseHits,
385
489
  resetZvecCache,
386
490
  zvecStatus,
387
- buildZvecIndex
491
+ buildZvecIndex,
492
+ CARDS_DIR,
493
+ DEFAULT_CARD_MODEL,
494
+ refreshZvecCardIndex,
495
+ ensureZvecServer
388
496
  };
389
- //# sourceMappingURL=chunk-GXOML4UZ.js.map
497
+ //# sourceMappingURL=chunk-4BSO3HST.js.map