feat: B+C part1 - смотрящие инструменты свободны под/без плана (isQueryOnly)
Новый предикат isQueryOnly (ToolSearch/WebFetch/WebSearch/read-only браузер: navigate/snapshot/wait/screenshot) проведён во все ветки стены: смотрящие и спрашивающие инструменты проходят и в разговорном режиме (осмотр чужого сайта без плана), и под опечатанным планом, не двигая указатель шагов. Действующие инструменты (клик/ввод) сюда не входят - они пойдут через сеанс осмотра (часть 2 B+C). Свод зелёный: 4229 passed, 2 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# Дизайн: осмотр и инструменты под стеной — «сеанс» (объединяет пункты B + C)
|
||||
|
||||
**Дата:** 2026-06-18 · **Репозиторий:** claude-brain (управляющий слой) · **Кодовая фраза:** «роутер-наставник».
|
||||
**Источник:** баги `bags/2026-06-17-wall-interactive-walk-blockers-bug.md` (Баги 1/2/3). Пункты **B** (`tools-json`)
|
||||
и **C** (интерактивный осмотр) допила эталона — слиты в один механизм.
|
||||
**Поглощает:** `2026-06-18-wall-impl-tool-allowlist-design.md` (B как отдельная спека — отменена этой).
|
||||
**Брат:** `2026-06-18-wall-impl-read-freedom-design.md` (A — чтение). Тот же принцип «не-шаг сбоку от очереди».
|
||||
**Тип:** дизайн-доказательство (brainstorming → writing-plans). Правки `tools/*.mjs` — далее по церемонии TDD.
|
||||
|
||||
---
|
||||
|
||||
## 1. Проблема (по факту, с цитатами)
|
||||
|
||||
Три независимых стопора, все про «использовать инструменты / осматривать сайт под стеной»:
|
||||
|
||||
1. **Инструменты режутся под планом.** observe-only = `{Read, Grep, Glob}` + read-only Bash
|
||||
([enforce-supreme-gate.mjs:78-82](../../../tools/enforce-supreme-gate.mjs#L78-L82)). `ToolSearch`, MCP
|
||||
(`mcp__*`), `WebFetch`/`WebSearch`, браузер (`browser_*`) — НЕ в списке → под планом блок. Механизма
|
||||
`tools-json` (заявить инструменты в плане) в коде нет (строка есть только в баге).
|
||||
2. **В разговорном режиме инструменты тоже режутся.** Без печати плана пускаются только seed/observe/
|
||||
authoring ([enforce-supreme-gate.mjs:365-368](../../../tools/enforce-supreme-gate.mjs#L365-L368)) —
|
||||
`ToolSearch`/браузер блокируются. Значит «просто посмотреть сайт без плана» невозможно.
|
||||
3. **Интерактивный проход нельзя выразить шагами (корень).** Чтобы кликнуть/ввести, нужен `ref` элемента
|
||||
из ЖИВОГО снимка страницы — на момент печати плана его не существует. Шаги матчатся по op+object
|
||||
([plan-lock.mjs:89-104](../../../tools/plan-lock.mjs#L89-L104)) — клик по динамическому `ref` так не
|
||||
задать. Плюс `op:"Skill"` опечатывается, но указатель не двигает
|
||||
([enforce-supreme-gate.mjs:265](../../../tools/enforce-supreme-gate.mjs#L265)) → дедлок.
|
||||
|
||||
**Вывод владельца (2026-06-18):** «заранее расписать, что и сколько смотреть, нельзя — ни на нашем сайте,
|
||||
ни на чужом». Значит «план просмотра по кликам» — тупик. План должен фиксировать **намерение и
|
||||
результат**, а не клики.
|
||||
|
||||
## 2. Принцип
|
||||
|
||||
- **Не-мутирующее действие не расширяет набор разрешённого → безопасно** (как чтение в A). Смотреть/
|
||||
спрашивать инструментом — это «чтение», только внешнее.
|
||||
- **Клик/ввод — действие, но НЕ над нашим репозиторием.** Несущая защита стены — мутация репозитория
|
||||
через шаг. Внешний клик репозиторий не меняет; деттерминированный якорь интерактива — то, что агент
|
||||
в итоге **запишет к нам** (отчёт/наблюдения).
|
||||
- **Указатель двухтактный (F-J)** — не-шаг очередь не двигает
|
||||
([enforce-supreme-gate.mjs:446-448](../../../tools/enforce-supreme-gate.mjs#L446-L448)).
|
||||
|
||||
## 3. Решение — три части
|
||||
|
||||
### 3.1 Смотрящие инструменты — свободны в ОБОИХ режимах
|
||||
Именованный набор заведомо read-only пускается и в разговорном режиме, и под планом, без объявления:
|
||||
- `ToolSearch`, `WebFetch`, `WebSearch`;
|
||||
- read-only браузер: `browser_navigate`, `browser_snapshot`, `browser_wait_for`, `browser_take_screenshot`.
|
||||
|
||||
Механика: новый предикат `isQueryOnly(toolUse)` рядом с `isObserveOnly`
|
||||
([:78-99](../../../tools/enforce-supreme-gate.mjs#L78-L99)); `decideMode` пускает его И в ветке «нет
|
||||
frozenPlan» (разговорный, [:365-368](../../../tools/enforce-supreme-gate.mjs#L365-L368)), И под планом
|
||||
([:282](../../../tools/enforce-supreme-gate.mjs#L282)). Указатель не двигают, пишутся в тихий лог (A).
|
||||
**Это закрывает «чужой сайт без плана»: смотрю и рассказываю, никакой печати не нужно.**
|
||||
|
||||
### 3.2 «Сеанс осмотра» — новый тип шага для интерактива
|
||||
Когда надо реально пройтись и пощёлкать (логин, формы, сценарий клиента) — план объявляет НЕ список
|
||||
кликов, а **один шаг-сеанс**:
|
||||
|
||||
```
|
||||
{ "op": "session", "goal": "<что осмотреть>", "tools": [<действующие инструменты>], "produces": "<путь итогового файла>" }
|
||||
```
|
||||
|
||||
- **`tools`** — действующие инструменты сеанса (`browser_click`, `browser_type`, `browser_fill_form`,
|
||||
`browser_select_option`, конкретные MCP). Это и есть бывший `tools-json`, теперь привязанный к сеансу.
|
||||
- **`produces`** — файл(ы), которые сеанс ОБЯЗАН произвести (наблюдения/отчёт). ≥1.
|
||||
- Пока указатель на шаге-сеансе: разрешены `tools` сеанса + смотрящие (3.1), **сколько угодно, по живым
|
||||
`ref`** — стена клики с шагами НЕ сверяет (динамический `ref` — не проблема). Указатель НЕ двигается.
|
||||
- **Сеанс закрывается** записью `produces`-файла: это обычный `Write`-шаг, он матчится и двигает
|
||||
указатель дальше. Деттерминированный якорь = итоговый файл, а не клики.
|
||||
|
||||
Этим снимается и **дедлок `op:"Skill"`**: навык не делается шагом — он объявляется в `skills-json` и
|
||||
вызывается свободно (как сейчас, [:265](../../../tools/enforce-supreme-gate.mjs#L265)). `op:"Skill"` в
|
||||
`steps-json` запрещаем с явным сообщением «Skill не может быть шагом — объяви в skills-json». «Проверка»
|
||||
осмотра — это `produces`-отчёт, а не шаг-навык (убирает требование наставника «дай шаг verify»).
|
||||
|
||||
### 3.3 Предохранитель (ядро безопасности)
|
||||
`tools` сеанса **НИКОГДА** не содержат:
|
||||
- `Write`/`Edit`/`MultiEdit`/мутирующий `Bash` — правки репозитория остаются ШАГАМИ плана (вкл. сам
|
||||
`produces`-файл сеанса — это обычный шаг записи);
|
||||
- floor-опасное (install/cloud-CLI/ssh/redirect/destructive, `matchBashHardBlacklist`, PowerShell-записи,
|
||||
запись в `~/.claude/runtime`/секреты) — остаётся за полом/escape.
|
||||
|
||||
Запрещённое имя в `tools` **отбрасывается с видимым предупреждением** (решение владельца Q2 = вариант «а»:
|
||||
не валить план целиком из-за описки). Сеанс/`tools` входят в **хеш плана** — подмена после печати ломает
|
||||
печать; наставник и судья видят `goal`/`tools`/`produces` при разборе.
|
||||
|
||||
### 3.4 Логирование
|
||||
Все вызовы инструментов и кликов под планом/в сеансе пишутся в тот же тихий лог, что impl-чтения (A):
|
||||
только для ретро, без баннера, без порога-warn (зеркало решений A; решения владельца Q1/Q2 спеки A).
|
||||
|
||||
### 3.5 Точки изменения в коде
|
||||
- **`enforce-supreme-gate.mjs`**: `isQueryOnly` (новый, рядом [:78-99](../../../tools/enforce-supreme-gate.mjs#L78-L99));
|
||||
`decideMode` — пускать query-only в обоих режимах ([:282](../../../tools/enforce-supreme-gate.mjs#L282),
|
||||
[:365-368](../../../tools/enforce-supreme-gate.mjs#L365-L368)); `decide` — ветка «указатель на
|
||||
шаге-сеансе»: вызов из `frozenPlan` сеанса `tools` → allow без advance; `produces`-Write → обычный
|
||||
матч-шаг с advance.
|
||||
- **`plan-lock.mjs`**: распознать `op:"session"` (goal/tools/produces) в дереве плана; `produces` —
|
||||
матч-якорь шага; применить предохранитель §3.3 (отбросить чёрный набор с предупреждением); запретить
|
||||
`op:"Skill"` как шаг; сеанс+`tools` в хеш плана.
|
||||
- **наставник/судья** (`mentor-verdict.mjs`/`judge-*`): показать `goal`/`tools`/`produces` сеанса в
|
||||
разборе плана (чтобы ревью видело набор инструментов и обещанный результат).
|
||||
- **read-LOG** (общий с A): принять записи tool-use/клик под планом.
|
||||
|
||||
## 4. Что НЕ трогаем
|
||||
- **Шаг-гейт для мутаций репозитория** (Write/Edit/Bash-write, вкл. `produces`-файл сеанса) — несущая
|
||||
защита, без изменений.
|
||||
- **Пол M5 / escape** — необратимое и floor-опасное по-прежнему через них.
|
||||
- **Чтение A** — отдельная спека; общий тихий лог переиспользуем.
|
||||
- **Машинерия указателя (F-J)** — не касаемся; инструмент/клик-не-шаг очередь не двигает.
|
||||
|
||||
## 5. Критерий «починено»
|
||||
- «Чужой сайт без плана»: в разговорном режиме работают `ToolSearch`/`WebFetch`/read-only браузер.
|
||||
- Под планом-сеансом: `browser_click`/`type`/`fill` работают по живым `ref`, без escape на каждый клик,
|
||||
сколько нужно; указатель не двигается.
|
||||
- Сеанс закрывается записью `produces`-файла (обычный шаг двигает указатель).
|
||||
- `op:"Skill"` как шаг — отбит с явным сообщением; навык из `skills-json` вызывается свободно.
|
||||
- `tools` сеанса с `Write`/`rm -rf`/`ssh` → отброшены с предупреждением, под планом не проходят.
|
||||
- Сеанс+`tools` в хеше плана: подмена после печати ломает печать.
|
||||
- Ни клик, ни tool-вызов не двигают указатель (регресс F-J держит).
|
||||
- Лог фиксирует tool-use/клики (видно в ретро).
|
||||
|
||||
## 6. Тесты (TDD-набросок)
|
||||
- `isQueryOnly`: ToolSearch/WebFetch/read-only браузер → true; browser_click/Write → false.
|
||||
- `decideMode` без frozenPlan: query-only → allow (разговорный осмотр чужого сайта).
|
||||
- `plan-lock` парсит `op:"session"`: goal/tools/produces извлечены; `produces` пуст → план невалиден.
|
||||
- Предохранитель: `Write`/`ssh` в `tools` сеанса → отброшены + warn.
|
||||
- `op:"Skill"` в steps-json → план отвергает шаг с сообщением.
|
||||
- `decide` указатель на сеансе: `browser_click` (в `tools`) → allow, advance=false.
|
||||
- `decide` указатель на сеансе: запись `produces`-файла → allow, advance=true (сеанс закрыт).
|
||||
- `decide` указатель на сеансе: инструмент НЕ в `tools` и не query-only → block (default-deny держит).
|
||||
- Хеш: план с другим `tools`/`produces` → другой `plan_id`.
|
||||
- Регресс F-J: клик/tool-use не двигает указатель.
|
||||
|
||||
## 7. Риски / открытые вопросы
|
||||
- **Q-A (для владельца):** ограничивать ли длительность/объём сеанса (тайм-аут, максимум кликов)?
|
||||
Предлагаю **нет** (зеркало «без порога» из A) — сеанс и так обязан кончиться записью `produces`, иначе
|
||||
план не сдвинется. Незавершённый сеанс просто висит, как любой недоделанный план.
|
||||
- **Q-B (для владельца):** один сеанс = один `produces`-файл, или можно несколько (наблюдения + отчёт)?
|
||||
Предлагаю **разрешить несколько** — гибче; последний из списка закрывает сеанс.
|
||||
- Наставник/судья должны понимать новый тип шага — иначе завернут как «непонятный шаг». Часть §3.5.
|
||||
- Связанный хвост (баг read-block Баг-2 Дефект 1): наставник видит не все поля шага. Пересекается с
|
||||
«показать goal/tools/produces наставнику» — закрывается тем же изменением §3.5.
|
||||
@@ -77,6 +77,20 @@ export function isPlanDeclaredSkill(toolUse, frozenPlan) {
|
||||
// Зелёный проход по СПОСОБНОСТИ = «нет долговременного И нет исходящего эффекта».
|
||||
const OBSERVE_ONLY_TOOLS = new Set(['Read', 'Grep', 'Glob']); // локальные «смотрят»
|
||||
const EPHEMERAL_META_TOOLS = new Set(['TodoWrite']); // меняют только черновик сессии, не мир
|
||||
|
||||
// B+C (2026-06-18): «смотрящие/спрашивающие» внешние инструменты — ничего не меняют, как чтение.
|
||||
// Свободны и в разговорном, и под планом (decideMode). НЕ путать с isObserveOnly (локальный zero-effect):
|
||||
// здесь намеренно входят WebFetch/WebSearch/ToolSearch и read-only браузер. Действующий браузер
|
||||
// (click/type/fill/select) и MCP-запись сюда НЕ входят — они идут через tools-json сеанса.
|
||||
const QUERY_ONLY_TOOLS = new Set(['ToolSearch', 'WebFetch', 'WebSearch']);
|
||||
const READONLY_BROWSER_SUFFIXES = ['browser_navigate', 'browser_snapshot', 'browser_wait_for', 'browser_take_screenshot'];
|
||||
export function isQueryOnly(toolUse) {
|
||||
if (!toolUse || typeof toolUse.name !== 'string') return false;
|
||||
const n = toolUse.name;
|
||||
if (QUERY_ONLY_TOOLS.has(n)) return true;
|
||||
return READONLY_BROWSER_SUFFIXES.some((s) => n.endsWith(s));
|
||||
}
|
||||
|
||||
export function isObserveOnly(toolUse, { classifyBash = classifyBashCommand } = {}) {
|
||||
if (!toolUse) return false;
|
||||
if (OBSERVE_ONLY_TOOLS.has(toolUse.name)) return true;
|
||||
@@ -279,6 +293,7 @@ export function decide({ toolUse, frozenPlan, frozenArtifact = null, stepPtr = 0
|
||||
return { decision: 'block', reason: `${ev.gate.reason}${t} (W2)` };
|
||||
}
|
||||
}
|
||||
if (isQueryOnly(toolUse)) return { decision: 'allow', reason: 'смотрящий инструмент (query-only) — свободно под планом (B+C), указатель не двигается' };
|
||||
if (isObserveOnly(toolUse)) return { decision: 'allow', reason: 'observe-only (зелёный проход по способности)' };
|
||||
if (!frozenPlan) return { decision: 'block', reason: 'нет замороженного плана — действие вне плана запрещено (default-deny)' };
|
||||
if (!verifyImpl(frozenPlan, key)) return { decision: 'block', reason: 'печать плана невалидна (seal/signature) — требуется заново одобрить план' };
|
||||
@@ -363,14 +378,14 @@ export function decideMode({ toolUse, frozenPlan, frozenArtifact, stepPtr = 0, k
|
||||
return { decision: 'allow', mode: 'conversational', finishPlan: true, reason: 'владелец завершил план досрочно (plan-done) — печать снята, возврат в разговор' };
|
||||
}
|
||||
if (!frozenPlan) {
|
||||
if (isSeed(toolUse) || isObserveOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/authoring (разговорный режим)' };
|
||||
if (isSeed(toolUse) || isObserveOnly(toolUse) || isQueryOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query/authoring (разговорный режим)' };
|
||||
return { decision: 'block', mode: 'conversational', reason: 'разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана)' };
|
||||
}
|
||||
if (!frozenArtifact || !verifyArtifactImpl(frozenArtifact, key)) {
|
||||
// F-B (аудит 2026-06-07): observe-only (Read/Grep/Glob/readonly-Bash/TodoWrite) пускаем
|
||||
// и в этом деградированном состоянии — инвариант finding 9 «смотрящие не душатся» +
|
||||
// согласованность с decide() (там observe-only allow безусловно). Бэкстоп держит только мутаторы.
|
||||
if (isSeed(toolUse) || isObserveOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/authoring (бэкстоп: артефакт не опечатан)' };
|
||||
if (isSeed(toolUse) || isObserveOnly(toolUse) || isQueryOnly(toolUse) || isAuthoringWrite(toolUse)) return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query/authoring (бэкстоп: артефакт не опечатан)' };
|
||||
return { decision: 'block', mode: 'conversational', reason: 'нет опечатанного артефакта разговорной фазы — вернись в разговор (бэкстоп C-10)' };
|
||||
}
|
||||
// SE-2 (fail-closed whitelist): энфорсмент ТОЛЬКО при live-block на ОБЕИХ печатях.
|
||||
@@ -382,8 +397,8 @@ export function decideMode({ toolUse, frozenPlan, frozenArtifact, stepPtr = 0, k
|
||||
// (→ разговорный), но ГРОМКО: warn-поле в возврат (owner-резюме гейта-1 покажет, wiring в C).
|
||||
const warnFields = judgeModeMismatch(frozenPlan.judge_mode, frozenArtifact.judge_mode)
|
||||
? { warn: true, warnReason: 'judge_mode рассинхрон план≠артефакт — энфорсмент off (O18)' } : {};
|
||||
if (isSeed(toolUse) || isObserveOnly(toolUse)) {
|
||||
return { decision: 'allow', mode: 'conversational', reason: 'seed/observe (не live-block-печать, SE-2)', ...warnFields };
|
||||
if (isSeed(toolUse) || isObserveOnly(toolUse) || isQueryOnly(toolUse)) {
|
||||
return { decision: 'allow', mode: 'conversational', reason: 'seed/observe/query (не live-block-печать, SE-2)', ...warnFields };
|
||||
}
|
||||
return { decision: 'block', mode: 'conversational', reason: 'не live-block-печать (shadow/наблюдение) — не одобрение к энфорсменту (SE-2)', ...warnFields };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isSeed, SEED_SKILLS } from './enforce-supreme-gate.mjs';
|
||||
import { decide, actionOf } from './enforce-supreme-gate.mjs';
|
||||
import { runGate } from './enforce-supreme-gate.mjs';
|
||||
import { decideMode } from './enforce-supreme-gate.mjs';
|
||||
import { isObserveOnly } from './enforce-supreme-gate.mjs';
|
||||
import { isObserveOnly, isQueryOnly } from './enforce-supreme-gate.mjs';
|
||||
import { resolveStepPtr } from './enforce-supreme-gate.mjs';
|
||||
import { resolveSessionId } from './enforce-supreme-gate.mjs';
|
||||
import { signStepState, verifyStepState } from './enforce-supreme-gate.mjs';
|
||||
@@ -337,6 +337,48 @@ describe('isObserveOnly (зелёный проход = нет долговрем
|
||||
});
|
||||
});
|
||||
|
||||
describe('isQueryOnly (B+C: смотрящие инструменты — свободны в обоих режимах)', () => {
|
||||
it('ToolSearch/WebFetch/WebSearch → true (только смотрят/спрашивают)', () => {
|
||||
for (const n of ['ToolSearch', 'WebFetch', 'WebSearch']) expect(isQueryOnly({ name: n })).toBe(true);
|
||||
});
|
||||
it('read-only браузер (navigate/snapshot/wait_for/take_screenshot, в т.ч. mcp-префикс) → true', () => {
|
||||
for (const n of ['mcp__playwright__browser_navigate', 'mcp__playwright__browser_snapshot', 'mcp__playwright__browser_wait_for', 'mcp__playwright__browser_take_screenshot'])
|
||||
expect(isQueryOnly({ name: n })).toBe(true);
|
||||
});
|
||||
it('действующий браузер (click/type/fill) → false (это не «смотреть»)', () => {
|
||||
for (const n of ['mcp__playwright__browser_click', 'mcp__playwright__browser_type', 'mcp__playwright__browser_fill_form'])
|
||||
expect(isQueryOnly({ name: n })).toBe(false);
|
||||
});
|
||||
it('мутаторы и null → false', () => {
|
||||
for (const n of ['Write', 'Edit', 'MultiEdit', 'Bash']) expect(isQueryOnly({ name: n })).toBe(false);
|
||||
expect(isQueryOnly(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('B+C: смотрящие инструменты пускаются в обоих режимах (wiring)', () => {
|
||||
it('разговорный (нет плана): ToolSearch/WebFetch/браузер-смотр → allow', () => {
|
||||
for (const n of ['ToolSearch', 'WebFetch', 'WebSearch', 'mcp__playwright__browser_navigate'])
|
||||
expect(decideMode({ toolUse: { name: n, input: {} }, frozenPlan: null, frozenArtifact: null, stepPtr: 0, key: 'k' }).decision).toBe('allow');
|
||||
});
|
||||
it('под планом: смотрящий (WebFetch) → allow без сдвига указателя', () => {
|
||||
const plan = { artifact_id: null, steps: [{ n: 1, op: 'Edit', object: 'tools/foo.mjs' }] };
|
||||
const r = decide({ toolUse: { name: 'WebFetch', input: {} }, frozenPlan: plan, frozenArtifact: null, stepPtr: 0, key: 'k', verifyImpl: () => true, verifyArtifactImpl: () => true, normalize: (p) => String(p).toLowerCase() });
|
||||
expect(r.decision).toBe('allow');
|
||||
expect(r.advance).not.toBe(true);
|
||||
});
|
||||
it('shadow-режим (не live-block печать): смотрящий тоже allow', () => {
|
||||
const plan = { artifact_id: null, steps: [{ n: 1, op: 'Edit', object: 'tools/foo.mjs' }], judge_mode: 'shadow' };
|
||||
const art = { artifact_id: null, judge_mode: 'shadow' };
|
||||
const r = decideMode({ toolUse: { name: 'WebFetch', input: {} }, frozenPlan: plan, frozenArtifact: art, stepPtr: 0, key: 'k', verifyImpl: () => true, verifyArtifactImpl: () => true, normalize: (p) => String(p).toLowerCase() });
|
||||
expect(r.decision).toBe('allow');
|
||||
});
|
||||
it('артефакт не опечатан (бэкстоп C-10): смотрящий тоже allow', () => {
|
||||
const plan = { artifact_id: null, steps: [{ n: 1, op: 'Edit', object: 'tools/foo.mjs' }] };
|
||||
const r = decideMode({ toolUse: { name: 'WebFetch', input: {} }, frozenPlan: plan, frozenArtifact: null, stepPtr: 0, key: 'k', verifyImpl: () => true, verifyArtifactImpl: () => false, normalize: (p) => String(p).toLowerCase() });
|
||||
expect(r.decision).toBe('allow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decide() пропускает зелёный проход без шага плана', () => {
|
||||
it('Read проходит без плана', () => {
|
||||
const r = decide(ctx({ toolUse: { name: 'Read', input: { file_path: 'x' } }, frozenPlan: null, stepPtr: 0 }));
|
||||
|
||||
Reference in New Issue
Block a user