feat: B+C часть 2 — сеанс осмотра op:"session" под стеной
Новый тип шага плана op:"session" {goal, tools, produces} для интерактивного
осмотра (логин/формы/чужой сайт) под планом: внутри сеанса смотреть/кликать по
живым ref свободно, указатель не двигается; сеанс закрывает запись последнего
produces (матч-якорь). Снят дедлок op:"Skill"-как-шаг.
- plan-lock: sessionProduces, actionMatchesStep матчит последний produces,
validatePlanTree валидирует session (produces>=1) и запрещает op:"Skill",
sanitizeSessionTools (предохранитель §3.3: дроп Write/Edit/Bash/floor + warn).
- enforce-supreme-gate decide: ветка указатель-на-сеансе — tools сеанса и
промежуточные produces allow без сдвига, пол применяется (defense-in-depth).
- plan-steps-parse: распознаёт op:"session" (goal/tools/produces, без object/ref),
отвергает op:"Skill" с явным сообщением.
- mentor-verdict: наставник понимает op:"session" — не заворачивает как непонятный шаг.
- сеанс+tools/produces в хеше и подписи плана (подмена ломает печать).
Спека: docs/superpowers/specs/2026-06-18-wall-interactive-session-design.md §3.2-3.3.
+37 тестов, свод 4266 passed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
* plan-steps-parse (C2, §5) — план markdown → [{op,object,ref}]. fail-CLOSE на любой брак.
|
||||
* Reject op:'Task' (VA-4). Канон object: файлы → repo-relative POSIX (SE-5). ref обязателен (SE-1).
|
||||
*/
|
||||
import { sessionProduces } from './plan-lock.mjs';
|
||||
|
||||
const BLOCK_RE = /```steps-json\s*\n([\s\S]*?)\n```/;
|
||||
const FILE_OPS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
||||
|
||||
@@ -34,10 +36,24 @@ export function parsePlanSteps(md) {
|
||||
return arr.map((s, i) => {
|
||||
if (!s || typeof s !== 'object') throw new Error(`plan-steps-parse: шаг ${i} не объект`);
|
||||
const op = String(s.op || '');
|
||||
const object = String(s.object ?? '');
|
||||
const ref = String(s.ref ?? '');
|
||||
if (!op) throw new Error(`plan-steps-parse: шаг ${i} без op`);
|
||||
if (op === 'Task') throw new Error('plan-steps-parse: op:Task запрещён (субагенты, VA-4)');
|
||||
// B+C ч.2 (точка 4): op:'Skill' не может быть шагом плана — навык объявляется в skills-json
|
||||
// и вызывается свободно (isPlanDeclaredSkill), указатель не двигая. Явное сообщение, не «без object».
|
||||
if (op === 'Skill') throw new Error(`plan-steps-parse: шаг ${i} op:'Skill' запрещён как шаг — объяви навык в skills-json (он вызывается свободно)`);
|
||||
// B+C ч.2 (точка 1): сеанс осмотра — особый тип шага (нет object/ref; интерактив по живым ref).
|
||||
// Якорь закрытия — produces (≥1 итоговый файл); goal — намерение для наставника/судьи; tools —
|
||||
// действующие инструменты сеанса (предохранитель §3.3 сработает в decide/sanitizeSessionTools).
|
||||
if (op === 'session') {
|
||||
const goal = String(s.goal ?? '').trim();
|
||||
if (!goal) throw new Error(`plan-steps-parse: шаг ${i} (session) без goal — намерение осмотра обязательно`);
|
||||
const produces = sessionProduces(s);
|
||||
if (produces.length === 0) throw new Error(`plan-steps-parse: шаг ${i} (session) без produces — нужен ≥1 итоговый файл (он закрывает сеанс)`);
|
||||
const tools = Array.isArray(s.tools) ? s.tools.map((t) => String(t ?? '')).filter(Boolean) : [];
|
||||
return { op: 'session', goal, tools, produces };
|
||||
}
|
||||
const object = String(s.object ?? '');
|
||||
const ref = String(s.ref ?? '');
|
||||
if (!object.trim()) throw new Error(`plan-steps-parse: шаг ${i} без object`);
|
||||
if (!ref.trim()) throw new Error(`plan-steps-parse: шаг ${i} без ref (closed-door, SE-1)`);
|
||||
return { op, object: canonObject(op, object), ref };
|
||||
|
||||
Reference in New Issue
Block a user