@7n/llm-lib 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0] - 2026-07-11
4
+
5
+ ### Added
6
+
7
+ - anchored-edit (Фаза A2): строгі hash-anchored read_anchored/edit_anchored tools, opts.anchoredEdits у runAgentFix (toolset-профіль без built-in read/edit), edit_anchored під write-guard veto/snapshot
8
+
3
9
  ## [2.1.1] - 2026-07-11
4
10
 
5
11
  ### Changed
package/lib/agent-fix.mjs CHANGED
@@ -32,6 +32,7 @@
32
32
 
33
33
  import { env } from 'node:process'
34
34
  import { homedir } from 'node:os'
35
+ import { createAnchoredTools } from './anchored-edit.mjs'
35
36
  import { getRegistry, resolveModelSpec } from './internal/registry.mjs'
36
37
  import { isLocalModel, thinkingLevelForTier } from './model-tiers.mjs'
37
38
  import { createWriteGuard, gitRoot } from './write-guard.mjs'
@@ -152,7 +153,7 @@ async function runVerifyLoop({ session, verify, verifyMax, timeoutMs, startedAt,
152
153
  * єдині наявні файли, які дозволено редагувати (порожньо/відсутнє — без переліку).
153
154
  * @returns {string} промпт
154
155
  */
155
- export function buildFixPrompt({ ruleId, violation, ruleText, feedback, targetFiles }) {
156
+ export function buildFixPrompt({ ruleId, violation, ruleText, feedback, targetFiles, anchoredEdits = false }) {
156
157
  const parts = [`Виправ порушення правила "${ruleId}" у цьому проєкті.`]
157
158
  if (ruleText) parts.push(`## Правило\n${ruleText}`)
158
159
  parts.push(`## Порушення\n${violation}`)
@@ -176,6 +177,13 @@ export function buildFixPrompt({ ruleId, violation, ruleText, feedback, targetFi
176
177
  'Після правок виклич `self_check`, щоб підтвердити, що порушення зникло. ' +
177
178
  'Редагуй лише потрібне, не чіпай стороннє.'
178
179
  )
180
+ if (anchoredEdits) {
181
+ parts.push(
182
+ 'Наявні файли читай і редагуй ЛИШЕ через `read_anchored` → `edit_anchored` ' +
183
+ '(кожна правка = {anchor, line, newText} з якорями від read_anchored; ' +
184
+ 'stale anchor = перечитай файл і повтори). `write` — лише для НОВИХ файлів.'
185
+ )
186
+ }
179
187
  return parts.join('\n\n')
180
188
  }
181
189
 
@@ -187,7 +195,17 @@ export function buildFixPrompt({ ruleId, violation, ruleText, feedback, targetFi
187
195
  * selfCheck: (files: string[]) => Promise<{ ok: boolean, output: string }> | { ok: boolean, output: string } }} args параметри сесії.
188
196
  * @returns {Promise<object>} pi AgentSession
189
197
  */
190
- async function defaultCreateSession({ registry, model, thinkingLevel, cwd, factory, astContext, selfCheck, chain }) {
198
+ async function defaultCreateSession({
199
+ registry,
200
+ model,
201
+ thinkingLevel,
202
+ cwd,
203
+ factory,
204
+ astContext,
205
+ selfCheck,
206
+ chain,
207
+ anchoredEdits = false
208
+ }) {
191
209
  const { createAgentSession, SessionManager, DefaultResourceLoader, SettingsManager, defineTool } =
192
210
  await import('@earendil-works/pi-coding-agent')
193
211
  const agentDir = `${homedir()}/.pi/agent`
@@ -226,13 +244,23 @@ async function defaultCreateSession({ registry, model, thinkingLevel, cwd, facto
226
244
  })
227
245
  })
228
246
 
247
+ // A2: anchored-профіль заміняє built-in read/edit на строгі read_anchored/edit_anchored
248
+ // (write лишається для НОВИХ файлів, під тим самим write-guard).
249
+ let tools = ['read', 'grep', 'find', 'edit', 'write', 'ls', 'ast_facts', 'self_check']
250
+ const customTools = [astTool, checkTool]
251
+ if (anchoredEdits) {
252
+ const { readTool, editTool } = createAnchoredTools({ cwd, defineTool })
253
+ tools = ['grep', 'find', 'write', 'ls', 'ast_facts', 'self_check', 'read_anchored', 'edit_anchored']
254
+ customTools.push(readTool, editTool)
255
+ }
256
+
229
257
  const { session } = await createAgentSession({
230
258
  modelRegistry: registry,
231
259
  model,
232
260
  thinkingLevel,
233
261
  cwd,
234
- tools: ['read', 'grep', 'find', 'edit', 'write', 'ls', 'ast_facts', 'self_check'],
235
- customTools: [astTool, checkTool],
262
+ tools,
263
+ customTools,
236
264
  sessionManager: SessionManager.inMemory(),
237
265
  resourceLoader: loader
238
266
  })
@@ -250,6 +278,7 @@ async function defaultCreateSession({ registry, model, thinkingLevel, cwd, facto
250
278
  * targetFiles?: string[],
251
279
  * verify?: (args: { touchedFiles: string[] }) => Promise<{ ok: boolean, output?: string }> | { ok: boolean, output?: string },
252
280
  * verifyMax?: number,
281
+ * anchoredEdits?: boolean,
253
282
  * deps?: { createSession?: (args: object) => Promise<object>, getRegistry?: () => Promise<object>,
254
283
  * registry?: object, root?: string|null,
255
284
  * astContext?: (path: string) => object,
@@ -270,6 +299,7 @@ export async function runAgentFix(ruleId, violation, cwd, opts = {}) {
270
299
  targetFiles,
271
300
  verify = null,
272
301
  verifyMax = VERIFY_MAX_DEFAULT,
302
+ anchoredEdits = false,
273
303
  deps = {}
274
304
  } = opts
275
305
  const createSession = deps.createSession ?? defaultCreateSession
@@ -328,6 +358,7 @@ export async function runAgentFix(ruleId, violation, cwd, opts = {}) {
328
358
  factory: guard.factory,
329
359
  astContext,
330
360
  selfCheck,
361
+ anchoredEdits,
331
362
  // Заголовки кореляції — лише локальним моделям (myllm стоїть тільки перед ними).
332
363
  chain: modelSpec && isLocalModel(modelSpec) ? chain : null
333
364
  })
@@ -376,7 +407,7 @@ export async function runAgentFix(ruleId, violation, cwd, opts = {}) {
376
407
  }
377
408
  })
378
409
 
379
- const fixPrompt = buildFixPrompt({ ruleId, violation, ruleText, feedback, targetFiles })
410
+ const fixPrompt = buildFixPrompt({ ruleId, violation, ruleText, feedback, targetFiles, anchoredEdits })
380
411
  const pHash = promptHash(fixPrompt)
381
412
  const startedAt = clock()
382
413
  let error = null
@@ -439,6 +470,7 @@ export async function runAgentFix(ruleId, violation, cwd, opts = {}) {
439
470
  backstopHit,
440
471
  verifyAttempts: verifyAttempts.length,
441
472
  verifyOk: verifyAttempts.length > 0 ? verifyAttempts.at(-1).ok : null,
473
+ anchoredEdits,
442
474
  wallMs: clock() - startedAt,
443
475
  error,
444
476
  ...chain?.traceFields()
@@ -0,0 +1,198 @@
1
+ /** @see ./docs/anchored-edit.md */
2
+
3
+ /**
4
+ * Hash-anchored line-editing (Фаза A2 run-harness, дизайн 2026-07-11).
5
+ *
6
+ * Строга заміна built-in `read`/`edit` для fix-профілів: кожен рядок файлу має
7
+ * 3-символьний base36-якір від хешу вмісту; правка застосовується ЛИШЕ якщо якір
8
+ * збігається з поточним вмістом рядка. Жодного fuzzy-match, мовчазного переміщення
9
+ * чи автокорекції (клас collateral слабких моделей: переписаний файл, літеральний
10
+ * `\n\n`). Будь-який mismatch → структурована відмова `stale anchor` БЕЗ часткового
11
+ * застосування (атомарно на файл) — агенту лишається перечитати файл і повторити.
12
+ *
13
+ * Референс патерну — pi-hashline-edit-pro (вендоримо ідею, не пакет: рішення Б спеки
14
+ * pi-harness-mt-fix-graph). Хеш — `node:crypto` sha256-префікс (нуль нових залежностей;
15
+ * контекст не sensitive — якір лише детектує розбіжність вмісту рядка).
16
+ *
17
+ * Модуль pi-free: чисті функції над рядками + фабрика tool-дефініцій, якій caller
18
+ * (agent-fix) передає pi `defineTool` через lazy import. Запис на диск робить
19
+ * tool `edit_anchored` — він у WRITE_TOOLS write-guard, тож підлягає тому самому
20
+ * scope/denylist veto, pre-image snapshot і rollback, що й built-in `edit`/`write`.
21
+ */
22
+
23
+ import { createHash } from 'node:crypto'
24
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
25
+ import { isAbsolute, resolve } from 'node:path'
26
+
27
+ /** Довжина якоря у base36-символах (36³ = 46656 значень — на рядок, не на файл). */
28
+ const ANCHOR_LEN = 3
29
+
30
+ /**
31
+ * Якір рядка: перші 3 base36-символи sha256 від вмісту рядка.
32
+ * Навмисно БЕЗ номера рядка у хеші: якір «прив'язаний до вмісту», а номер іде
33
+ * окремим полем правки — розбіжність будь-якого з двох означає stale-стан.
34
+ * @param {string} text вміст рядка (без завершального \n)
35
+ * @returns {string} 3-символьний base36-якір
36
+ */
37
+ export function lineAnchor(text) {
38
+ const h = createHash('sha256').update(text, 'utf8').digest()
39
+ return (h.readUInt32BE(0) % 36 ** ANCHOR_LEN).toString(36).padStart(ANCHOR_LEN, '0')
40
+ }
41
+
42
+ /**
43
+ * Рендерить вміст файлу у anchored-форматі: `якір|номер|текст`, нумерація з 1.
44
+ * @param {string} content повний вміст файлу
45
+ * @param {{ from?: number, to?: number }} [range] діапазон рядків (включно, 1-based)
46
+ * @returns {string} anchored-подання (по рядку на кожен рядок діапазону)
47
+ */
48
+ export function renderAnchored(content, { from = 1, to = Infinity } = {}) {
49
+ const lines = content.split('\n')
50
+ const out = []
51
+ for (let i = from; i <= Math.min(to, lines.length); i++) {
52
+ const text = lines[i - 1]
53
+ out.push(`${lineAnchor(text)}|${i}|${text}`)
54
+ }
55
+ return out.join('\n')
56
+ }
57
+
58
+ /**
59
+ * Атомарно застосовує anchored-правки до вмісту файлу.
60
+ *
61
+ * Валідація ПЕРЕД застосуванням: кожна правка перевіряється на (а) існування рядка,
62
+ * (б) збіг якоря з поточним вмістом. Хоч одна stale → нічого не застосовується.
63
+ * Семантика правки: `newText` — заміна рядка (може бути багаторядковим), `null` —
64
+ * видалення рядка. Дублікати номера рядка у списку правок — теж відмова (двозначно).
65
+ * @param {string} content поточний вміст файлу
66
+ * @param {Array<{ anchor: string, line: number, newText: string|null }>} edits правки
67
+ * @returns {{ ok: true, content: string } | { ok: false, stale: Array<{ line: number, reason: string }> }}
68
+ * новий вміст або перелік розбіжностей (для структурованої відповіді агенту)
69
+ */
70
+ export function applyAnchoredEdits(content, edits) {
71
+ const lines = content.split('\n')
72
+ const stale = []
73
+ const seen = new Set()
74
+ for (const e of edits) {
75
+ if (!Number.isSafeInteger(e.line) || e.line < 1 || e.line > lines.length) {
76
+ stale.push({ line: e.line, reason: `рядка ${e.line} не існує (у файлі ${lines.length})` })
77
+ continue
78
+ }
79
+ if (seen.has(e.line)) {
80
+ stale.push({ line: e.line, reason: `рядок ${e.line} правиться двічі в одному виклику` })
81
+ continue
82
+ }
83
+ seen.add(e.line)
84
+ const actual = lineAnchor(lines[e.line - 1])
85
+ if (e.anchor !== actual) {
86
+ stale.push({ line: e.line, reason: `stale anchor ${e.anchor} (актуальний ${actual}) — перечитай файл` })
87
+ }
88
+ }
89
+ if (stale.length > 0) return { ok: false, stale }
90
+
91
+ // Застосування знизу вгору — номери рядків вище не зсуваються.
92
+ const sorted = edits.toSorted((a, b) => b.line - a.line)
93
+ for (const e of sorted) {
94
+ if (e.newText === null) lines.splice(e.line - 1, 1)
95
+ else lines.splice(e.line - 1, 1, ...e.newText.split('\n'))
96
+ }
97
+ return { ok: true, content: lines.join('\n') }
98
+ }
99
+
100
+ /**
101
+ * Резолвить шлях tool-виклику відносно cwd сесії.
102
+ * @param {string} cwd робоча директорія сесії
103
+ * @param {string} raw шлях із tool-input
104
+ * @returns {string} абсолютний шлях
105
+ */
106
+ function resolvePath(cwd, raw) {
107
+ return isAbsolute(raw) ? raw : resolve(cwd, raw)
108
+ }
109
+
110
+ /**
111
+ * Структурована текстова відмова tool-виклику (JSON-текст — слабкі моделі бачать
112
+ * точну причину і наступний крок).
113
+ * @param {object} payload обʼєкт з `error` (+ опційні деталі)
114
+ * @returns {{ content: Array<{ type: string, text: string }>, details: object }} tool-результат
115
+ */
116
+ function toolFail(payload) {
117
+ return { content: [{ type: 'text', text: JSON.stringify(payload) }], details: {} }
118
+ }
119
+
120
+ /**
121
+ * Фабрика пари pi-tools `read_anchored`/`edit_anchored`.
122
+ *
123
+ * `defineTool` передається caller-ом (lazy pi-import в agent-fix) — модуль лишається
124
+ * pi-free. Обидва tools повертають результат текстом (JSON для помилок), щоб слабкі
125
+ * моделі бачили точну причину відмови і мали інструкцію наступного кроку.
126
+ * @param {{ cwd: string, defineTool: (def: object) => object,
127
+ * fs?: { existsSync?: typeof existsSync, readFileSync?: typeof readFileSync, writeFileSync?: typeof writeFileSync } }} args
128
+ * контекст: cwd сесії, pi defineTool, опційні fs-інжекції для тестів
129
+ * @returns {{ readTool: object, editTool: object }} tool-дефініції для customTools
130
+ */
131
+ export function createAnchoredTools({ cwd, defineTool, fs = {} }) {
132
+ const exists = fs.existsSync ?? existsSync
133
+ const read = fs.readFileSync ?? readFileSync
134
+ const write = fs.writeFileSync ?? writeFileSync
135
+
136
+ const readTool = defineTool({
137
+ name: 'read_anchored',
138
+ label: 'Read (anchored)',
139
+ description:
140
+ 'Read a file as anchored lines "anchor|lineNo|text". To edit an existing file you MUST read it with this tool first and use the returned anchors in edit_anchored.',
141
+ parameters: {
142
+ type: 'object',
143
+ properties: {
144
+ path: { type: 'string', description: 'file path' },
145
+ from: { type: 'number', description: 'first line (1-based, optional)' },
146
+ to: { type: 'number', description: 'last line (inclusive, optional)' }
147
+ },
148
+ required: ['path']
149
+ },
150
+ execute: (_id, { path, from, to }) => {
151
+ const abs = resolvePath(cwd, path)
152
+ if (!exists(abs)) return toolFail({ error: `файл не існує: ${path}` })
153
+ const text = renderAnchored(read(abs, 'utf8'), { from: from ?? 1, to: to ?? Infinity })
154
+ return { content: [{ type: 'text', text }], details: {} }
155
+ }
156
+ })
157
+
158
+ const editTool = defineTool({
159
+ name: 'edit_anchored',
160
+ label: 'Edit (anchored)',
161
+ description:
162
+ 'Strictly edit a file by anchored lines. Each edit = {anchor, line, newText}; newText replaces the whole line (may be multiline), null deletes the line. Anchors come from read_anchored. Any stale anchor rejects the WHOLE call — re-read the file and retry. No fuzzy matching.',
163
+ parameters: {
164
+ type: 'object',
165
+ properties: {
166
+ path: { type: 'string', description: 'file path' },
167
+ edits: {
168
+ type: 'array',
169
+ items: {
170
+ type: 'object',
171
+ properties: {
172
+ anchor: { type: 'string', description: '3-char anchor from read_anchored' },
173
+ line: { type: 'number', description: '1-based line number' },
174
+ newText: { type: ['string', 'null'], description: 'replacement text (null = delete line)' }
175
+ },
176
+ required: ['anchor', 'line']
177
+ }
178
+ }
179
+ },
180
+ required: ['path', 'edits']
181
+ },
182
+ execute: (_id, { path, edits }) => {
183
+ const abs = resolvePath(cwd, path)
184
+ if (!exists(abs)) return toolFail({ error: `файл не існує: ${path} — для нового файлу використовуй write` })
185
+ if (!Array.isArray(edits) || edits.length === 0) return toolFail({ error: 'edits порожній' })
186
+ const normalized = edits.map(e => ({ anchor: e.anchor, line: e.line, newText: e.newText ?? null }))
187
+ const res = applyAnchoredEdits(read(abs, 'utf8'), normalized)
188
+ if (!res.ok) return toolFail({ error: 'stale anchors — нічого не застосовано', stale: res.stale })
189
+ write(abs, res.content)
190
+ return {
191
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, applied: normalized.length }) }],
192
+ details: {}
193
+ }
194
+ }
195
+ })
196
+
197
+ return { readTool, editTool }
198
+ }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: agent-fix.mjs
4
4
  resource: llm-lib/lib/agent-fix.mjs
5
5
  docgen:
6
- crc: 0f2be7f8
6
+ crc: 730809af
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -18,6 +18,8 @@ buildFixPrompt готує текстовий промпт, що містить
18
18
  runAgentFix виконує повний агентний цикл для спроби виправлення порушення правила, включаючи взаємодію з інструментами, застосування патча та фіксацію телеметрії.
19
19
  Виклик сесії огорнутий timeout-гонкою: `opts.timeoutMs` (дефолт 300s, коли consumer не передав значення) на спрацюванні abort-ить сесію і повертає помилку `fix timeout …` — зависла LLM-сесія (напр. мертва SSE) не блокує виклик назавжди.
20
20
 
21
+ Anchored-профіль (опційний `opts.anchoredEdits`, Фаза A2): toolset сесії заміняє built-in `read`/`edit` на строгі `read_anchored`/`edit_anchored` (див. anchored-edit.md; `write` лишається для нових файлів, під тим самим write-guard), а промпт отримує інструкцію anchored-циклу. Прапорець фіксується у трейсі (`anchoredEdits`) для A/B-аналізу; дефолт вимкнено.
22
+
21
23
  Evidence-гейт (опційний `opts.verify`, Фаза A1 run-harness): після prompt-у модуль сам запускає canonical-перевірку consumer-а; провал інʼєктиться фідбеком у ту саму сесію — до `opts.verifyMax` додаткових ітерацій (дефолт 2), у межах того самого `timeoutMs` (залишок бюджету < 5s — чесна зупинка без ітерації). Гейт структурний: заяви агента про успіх не важать, джерелом правди лишається зовнішня перевірка. Помилка самої перевірки — інфраструктурна: ітерації не витрачаються, повертається `error` з префіксом `verify:`. Без `verify` — поведінка попередня (один прохід). Спроби фіксуються у `telemetry.verifyAttempts` і у trace (`verifyAttempts`, `verifyOk`).
22
24
 
23
25
  ## Публічний API
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: JS Module
3
+ title: anchored-edit.mjs
4
+ resource: llm-lib/lib/anchored-edit.mjs
5
+ docgen:
6
+ crc: 6ac40e43
7
+ ---
8
+
9
+ ## Огляд
10
+
11
+ Строге hash-anchored редагування рядків (Фаза A2 run-harness): заміна built-in `read`/`edit` для fix-профілів. Кожен рядок файлу отримує 3-символьний base36-якір від sha256 вмісту; правка застосовується лише при точному збігу якоря з поточним станом рядка. Мета — прибрати collateral слабких моделей (переписаний файл, літеральний `\n\n`): жодного fuzzy-match, мовчазного переміщення чи автокорекції. Референс патерну — pi-hashline-edit-pro (вендориться ідея, не пакет).
12
+
13
+ ## Поведінка
14
+
15
+ Рендер (`renderAnchored`) віддає рядки у форматі `якір|номер|текст` (нумерація з 1, опційний діапазон включно). Застосування (`applyAnchoredEdits`) атомарне на файл: усі правки спершу валідуються (існування рядка, збіг якоря, відсутність дублю номера) — хоч одна розбіжність означає повну відмову з переліком `stale`-причин, файл не змінюється. Валідні правки застосовуються знизу вгору, тож номери рядків у пакеті правок не зсуваються; `newText` заміняє рядок (може бути багаторядковим), `null` — видаляє.
16
+
17
+ Tool-фабрика (`createAnchoredTools`) створює пару pi-tools: `read_anchored` (файл або діапазон в anchored-форматі; неіснуючий файл — структурована помилка) і `edit_anchored` (правки `{anchor, line, newText}`; stale → JSON-відмова з інструкцією перечитати файл; для нових файлів чесно відсилає до `write`). `defineTool` передається caller-ом — модуль лишається pi-free; fs-операції інжектовані для тестів.
18
+
19
+ ## Публічний API
20
+
21
+ lineAnchor — 3-символьний base36-якір від sha256 вмісту рядка (детермінований, чутливий до будь-якої зміни тексту).
22
+ renderAnchored — anchored-подання вмісту файлу для читання агентом.
23
+ applyAnchoredEdits — атомарне застосування пакету правок або структурована відмова `{ok:false, stale:[…]}`.
24
+ createAnchoredTools — фабрика tool-дефініцій `read_anchored`/`edit_anchored` для customTools pi-сесії.
25
+
26
+ ## Де використовується
27
+
28
+ `agent-fix.mjs`: `opts.anchoredEdits` перемикає toolset сесії (built-in `read`/`edit` прибираються, `write` лишається для нових файлів). `edit_anchored` внесений у WRITE_TOOLS write-guard — той самий scope/denylist veto, pre-image snapshot і rollback, що й у built-in write-tools. Opt-in у cursor: env `N_LLM_FIX_ANCHORED` (default-worker).
29
+
30
+ ## Гарантії поведінки
31
+
32
+ - Атомарність на файл: часткове застосування пакету правок неможливе.
33
+ - Якір прив'язаний лише до вмісту рядка; номер рядка звіряється окремо — розбіжність будь-якого з двох дає відмову.
34
+ - Чисті функції не торкаються диска; запис робить лише `edit_anchored` (під write-guard).
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: write-guard.mjs
4
4
  resource: llm-lib/lib/write-guard.mjs
5
5
  docgen:
6
- crc: b95012fc
6
+ crc: b5110048
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -16,6 +16,7 @@ docgen:
16
16
  NEW_FILE позначає файл, якого до запису не існувало.
17
17
  gitRoot визначає кореневий каталог репозиторію Git, якщо він є.
18
18
  createWriteGuard створює механізм захисту записів до файлової системи, який перевіряє, чи запис знаходиться в межах кореневого каталогу Git, чи не стосується він директорій `.git` або ігнорованих файлів Git, і робить резервні копії файлів перед будь-якою зміною.
19
+ Під veto/snapshot підпадають tool-виклики `edit`, `write` і `edit_anchored` (anchored-профіль Фази A2, див. anchored-edit.md) — кастомний anchored-tool проходить той самий контроль, що й built-in write-tools.
19
20
 
20
21
  ## Публічний API
21
22
 
@@ -27,8 +27,8 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'nod
27
27
  /** Sentinel pre-image для файлу, якого до запису не існувало (rollback = видалити). */
28
28
  export const NEW_FILE = Symbol('new-file')
29
29
 
30
- /** Tool-и, що пишуть на диск і підлягають veto. */
31
- const WRITE_TOOLS = new Set(['edit', 'write'])
30
+ /** Tool-и, що пишуть на диск і підлягають veto (включно з anchored-варіантом A2). */
31
+ const WRITE_TOOLS = new Set(['edit', 'write', 'edit_anchored'])
32
32
 
33
33
  /**
34
34
  * git-root для cwd (`git rev-parse --show-toplevel`) або null, якщо не git-репо.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/llm-lib",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Тонкий шар роботи з LLM (локальні omlx + хмарні провайдери) поверх pi: model tiers, one-shot, agentic-раннери, write-guard, trace, telemetry, prompt-budget",
5
5
  "keywords": [
6
6
  "nitra",