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:
Дмитрий
2026-06-18 11:54:42 +03:00
parent e9ba6fb9a2
commit bc1d2a370a
8 changed files with 328 additions and 9 deletions
+28
View File
@@ -36,4 +36,32 @@ describe('plan-steps-parse', () => {
it('non-JSON block → throws', () => {
expect(() => parsePlanSteps('```steps-json\nnot json\n```')).toThrow();
});
// B+C ч.2: сеанс осмотра op:'session' — особый тип шага (нет object/ref; goal+tools+produces).
it('парсит op:session (goal/tools/produces сохранены, object/ref не нужны)', () => {
const md = '```steps-json\n[{"op":"session","goal":"осмотр логина","tools":["browser_click","browser_type"],"produces":["docs/observer/notes.md","docs/observer/report.md"]}]\n```';
const s = parsePlanSteps(md);
expect(s).toHaveLength(1);
expect(s[0]).toMatchObject({
op: 'session', goal: 'осмотр логина',
tools: ['browser_click', 'browser_type'],
produces: ['docs/observer/notes.md', 'docs/observer/report.md'],
});
});
it('сеанс со строковым produces → нормализован в массив', () => {
const md = '```steps-json\n[{"op":"session","goal":"g","tools":[],"produces":"docs/observer/s.md"}]\n```';
expect(parsePlanSteps(md)[0].produces).toEqual(['docs/observer/s.md']);
});
it('сеанс без goal → throws', () => {
const md = '```steps-json\n[{"op":"session","tools":[],"produces":"r.md"}]\n```';
expect(() => parsePlanSteps(md)).toThrow(/goal/i);
});
it('сеанс без produces → throws (нужен ≥1 итоговый файл)', () => {
const md = '```steps-json\n[{"op":"session","goal":"g","tools":[]}]\n```';
expect(() => parsePlanSteps(md)).toThrow(/produces/i);
});
it('op:Skill как шаг → throws с явным сообщением (объяви в skills-json)', () => {
const md = '```steps-json\n[{"op":"Skill","object":"superpowers:test-driven-development","ref":"r"}]\n```';
expect(() => parsePlanSteps(md)).toThrow(/skills-json|Skill/i);
});
});