19 KiB
Spec Delta: session-prestep
Purpose
Две неблокирующие инъекции шага agent/pre-step в DSH-бандле: harness-check
(проект не подключён к обвязке → список отсутствующего и шаги настройки) и
role-hint (первое пользовательское сообщение шага подсказывает роль или задаёт
один вопрос с четырьмя вариантами). В Claude Code это SessionStart-хуки
harness-check.sh и role-hint.sh; в DSH слоя shell-хуков нет, поэтому та же
логика живёт в бандле и использует общую с bootstrap машинерию маркера и
видимости. Обе инъекции — текстовые, неблокирующие и отключаются конфигом.
Здесь же едут follow-ups #62 того же цикла: изоляция PATH в негативном тесте,
WARN смешанного HOME/DSH_HOME и обязательный TDD-порядок в sdd-скилле.
Тексты собираются модулем dsh/bundle/lib/prestep.js (чистые функции: только
fs-чтения, без ctx-сервисов и без spawn), а решает об инъекции
dsh/bundle/index.js.
ADDED Requirements
Requirement: Инъекция harness-check
Инъекция SHALL проверять подключение проекта к обвязке и MUST оставаться неблокирующей: при неподключённом проекте шаг продолжается, к сообщениям шага добавляется одно пользовательское сообщение с текстом подсказки.
Корень проекта MUST определяться upward-walk от рабочей директории сессии до
ближайшего каталога с .git (каталог или файл — worktree-чекаут тоже корень);
при отсутствии .git до корня ФС проект считается вне git и инъекции нет.
Сигналы подключения MUST быть ровно три и проверяться в этом порядке:
openspec/ (каталог), .codegraph/ (каталог), секция ## OpenSpec и superpowers в CLAUDE.md (строка заголовка второго уровня). Все сигналы на
месте — инъекция MUST NOT появляться; часть отсутствует — инъекция SHALL
перечислять отсутствующие в этом порядке.
Текст инъекции MUST быть: [dsh-harness] Проект <root> не подключён к обвязке: нет: <список отсутствующего через ; >. Предложи владельцу настройку: openspec init --tools agents, codegraph init и секцию «OpenSpec и superpowers» в CLAUDE.md. В тексте MUST NOT быть Claude Code-артефактов: ни путей ~/.claude,
ни adopt-project.sh, ни .claude/code-paths (D-9).
Инъекция SHALL управляться конфигом harnessCheck (по умолчанию true) вместе
с общим enabled бандла и MUST требовать только наличия ctx.on
(typeof ctx.on === 'function', D-5), а не успеха self-check bootstrap: при
bootstrap: false harness-check обязан работать. Реализация MUST использовать
fs-чтения (stat/readFile) и MUST NOT запускать git.
Scenario: Проект не подключён
- WHEN сессия стартует в git-репозитории, где нет
openspec/и секции вCLAUDE.md, а.codegraph/есть - THEN в сообщения шага добавляется
[dsh-harness] Проект <root> не подключён к обвязке: нет: openspec/; секция «OpenSpec и superpowers» в CLAUDE.md.и готовые команды настройки
Scenario: Проект подключён
- WHEN в корне проекта есть
openspec/,.codegraph/и секция## OpenSpec и superpowersвCLAUDE.md - THEN harness-check молчит, сообщений не добавляется
Scenario: Вне git-репозитория
- WHEN рабочая директория сессии не имеет
.gitни в одном родительском каталоге до корня ФС - THEN инъекции нет: harness-check не подсказывает настройку вне проекта
Scenario: Рабочая директория в подкаталоге проекта
- WHEN сессия стартует в
<repo>/dsh/bundle, а сигналы лежат в<repo> - THEN корнем считается
<repo>, и текст инъекции называет<repo>
Scenario: Отключённый bootstrap не выключает harness-check
- WHEN профиль задаёт
bootstrap: falseиharnessCheck: true - THEN harness-check по-прежнему инъецирует подсказку: гейт —
ctx.on, а неbootstrapOk
Scenario: Конфиг выключает инъекцию
- WHEN профиль задаёт
harnessCheck: false - THEN harness-check не инъецируется, остальные pre-step инъекции не затрагиваются
Requirement: Инъекция role-hint
Инъекция SHALL выбирать роль по первому пользовательскому сообщению шага; она MUST NOT блокировать шаг и MUST NOT утверждать роль как факт.
Триггер MUST быть таким: в сообщениях текущего шага есть сообщение с
source.kind === 'user', чей текст непуст и не является собственной инъекцией
обвязки. Собственной инъекцией MUST считаться текст, начинающийся с
<!-- DSH-HARNESS-, либо содержащий [dsh-harness], либо содержащий
[claude-harness] (legacy-префикс до обновления install.sh, D-10). Фильтр MUST
применяться до выбора роли: иначе текст вопроса сам содержит все ключевики.
Сообщений без нового пользовательского текста (в т.ч. автономный раунд) —
инъекции нет.
Ключевики SHALL проверяться по нижнему регистру, в порядке приоритета admin → qa → analyst → dev; побеждает первый совпавший класс:
- admin:
jira,гант,спринт; - qa:
стенд,прогон,скриншот,макет; - analyst:
тз,oq,спека,методика; - dev:
срез,issue,pr,тест,миграция.
Совпадение MUST быть подстрокой вхождения целиком, кроме pr, который MUST
матчиться только отдельным словом (\bpr\b): PR, pr. — совпадение,
preview, пр — нет. Семантика подстрок заявлена явно (T1.2): словоформы,
меняющие основу, ключевик не содержат и MUST NOT матчиться — «миграцию»,
«миграции», «спеку» остаются без совпадения, тогда как формы, содержащие
ключевик целиком («теста», «issue-трекер», «ТЗ-документ»), матчатся.
При совпадении инъекция MUST быть: [dsh-harness] Роль сессии: <r>. Вызови skill role. Если не так — скажи. Без совпадения инъекция MUST быть вопросом:
[dsh-harness] Роль сессии неясна. Вызови skill role: dev — код, issue, PR, тест, миграция; admin — Jira, Гант, спринт; analyst — ТЗ, OQ, спека, методика; qa — стенд, прогон, скриншот, макет. Задай владельцу один вопрос с этими четырьмя вариантами.
Инъекция SHALL управляться конфигом roleHint (по умолчанию true) и, как
harness-check, MUST требовать только ctx.on (D-5), независимо от bootstrap.
Scenario: Первое сообщение с ключевиком
- WHEN первое пользовательское сообщение шага — «Напиши тест для модуля»
- THEN добавляется
[dsh-harness] Роль сессии: dev. Вызови skill role. Если не так — скажи.
Scenario: Приоритет классов ролей
- WHEN сообщение содержит и
jira, испринт, итест - THEN подсказка называет admin: admin выигрывает у qa, analyst и dev
Scenario: Бесключевое сообщение
- WHEN первое пользовательское сообщение шага не содержит ни одного ключевика
- THEN добавляется вопрос с четырьмя вариантами ролей
Scenario: Своя инъекция не считается сообщением
- WHEN единственное пользовательское сообщение шага дословно равно вопросу role-hint
- THEN инъекции нет: фильтр собственных сообщений срабатывает до выбора роли
Scenario: Legacy-префикс Claude Code
- WHEN единственное пользовательское сообщение начинается с
[claude-harness] - THEN роль по нему не выбирается и инъекции нет
Scenario: Шаг без нового пользовательского сообщения
- WHEN в сообщениях шага нет пользовательского сообщения с непустым текстом
- THEN role-hint молчит
Scenario: Флексия ключевика не матчится (T1.2)
- WHEN первое сообщение шага — «Сделай миграцию и обнови спеку»
- THEN ни
миграция, ниспекане совпадают, подсказки роли нет — выдаётся вопрос с четырьмя вариантами; это заявленный контракт подстрок, а не дефект
Scenario: pr только отдельным словом
- WHEN первое сообщение содержит
preview, но не содержит словаpr - THEN dev не выбирается;
PRилиpr.в сообщении дают dev
Requirement: Инъекции пропускаются в сабагентах
Обе инъекции MUST пропускаться на делегированном шаге: шаг сабагента не должен
получать ни harness-check, ни role-hint. Делегирование SHALL определяться по
реальным сигналам рантайма: agent.session.header.origin === 'subagent', либо
agent.session.header.delegationDepth > 0, либо
agent.options.subagentDepth > 0 (T2.1). Форк сессии MUST NOT считаться
сабагентом: форк (в т.ч. parentSession без origin) получает все инъекции
(T2.3).
Scenario: Делегированный шаг
- WHEN pre-step выполняется для агента с
origin: 'subagent'(или с положительной глубиной делегирования) - THEN ни harness-check, ни role-hint не инъецируются, даже если проект не подключён, а сообщение содержит ключевик
Scenario: Форк сессии получает инъекции
- WHEN pre-step выполняется для форка (
parentSessionбезorigin) - THEN пропуска нет: инъекции работают как в главной сессии
Requirement: Повторная инъекция и видимость
Инъекции SHALL дедуплицироваться по маркеру
<!-- DSH-HARNESS-PRESTEP:<16-hex> -->, который строится из хэша текста
инъекции и стоит первой строкой тела; одна и та же видимая инъекция MUST NOT
добавляться дважды. Видимость SHALL проверяться общим хелпером
isInjectionVisible(agent, messages, marker): сначала сообщения текущего шага,
затем durable-поверхность session.surface.nodes + eventAt(seq); после
компакции маркер исчезает с поверхности и инъекция повторяется (D-6).
Дедупликация MUST быть по содержимому (маркеру), а не «ровно один раз за сессию» (T2.2). Фактическое поведение, которое и заявляется контрактом:
- роль, определённая на другом шаге иначе, даёт другой текст и другой маркер — повторная подсказка роли возможна;
- если на более позднем шаге первый пользовательский текст бесключевой, role-hint может выдать вопрос с четырьмя вариантами после уже выданной подсказки роли: это следствие per-text дедупа, а не дефект (D-11);
- harness-check при неизменном состоянии проекта даёт тот же текст и тот же маркер — повтор возможен только после компакции или изменения набора отсутствующих сигналов.
Scenario: Та же инъекция не дублируется
- WHEN тот же текст инъекции уже виден в сообщениях шага или на поверхности сессии
- THEN вторая копия не добавляется
Scenario: Другой текст даёт вторую инъекцию (T2.2)
- WHEN подсказка роли уже выдана на первом шаге, а на следующем шаге первое пользовательское сообщение бесключевое
- THEN добавляется вопрос с четырьмя вариантами: маркер другого текста не виден, и дедуп по содержимому его не подавляет
Scenario: Повтор после компакции
- WHEN маркер инъекции пропал с поверхности сессии после
/compact - THEN следующий шаг добавляет инъекцию заново
Scenario: Смена роли на новом шаге
- WHEN на первом шаге роль была dev, а на следующем сообщение содержит
jira - THEN добавляется подсказка admin: текст и маркер другие
Requirement: Follow-ups #62 в этом же change
Пункты #62 SHALL ехать задачами этого change, а не отдельными фичами (D-4), и каждый MUST быть закрыт тестом или пином текста:
- T2.1/M-2: негативный тест
install.sh --checkбезopenspecMUST изолировать PATH (каталог-заглушка как весь PATH), а не наследовать host PATH с фиксированным системным префиксом: глобальный npm-префикс на Linux кладётopenspecв/usr/bin, и тест «бинарника нет» иначе проверяет не то. - O-2:
install.shMUST предупреждать (WARN, прогон продолжается) о смешанном состоянии, когдаDSH_HOMEпришёл из окружения, указывает на существующий каталог и не равен$HOME/.dsh, при нестандартном HOME: изоляция только HOME пишет в реальныйDSH_HOME. WARN MUST быть один на прогон и MUST срабатывать и в--global(install_global), а не только в--skills. - T3.4:
dsh/skills/sdd/SKILL.mdMUST содержать порядок TDD: шаг «напиши тест и убедись, что он падает (RED)» обязателен до кода; текст добавляется с нуля и пинится тестом (D-8).
Scenario: Негативный тест --check без openspec
- WHEN тест запускает
install.sh --checkс PATH, состоящим только из каталога-заглушки безopenspec - THEN проверка сообщает об отсутствии
openspec, а host-бинарник не влияет на результат
Scenario: Смешанный HOME и DSH_HOME
- WHEN
--globalзапускается с нестандартным HOME иDSH_HOME, указывающим на существующий каталог - THEN печатается
WARN: DSH_HOME=… both HOME and DSH_HOME must be isolated together, и установка продолжается
Scenario: TDD-порядок в sdd-скилле
- WHEN модель читает
sdd-скилл перед первым диспатчем - THEN в нём есть требование обязательного падающего теста до кода, и это требование закреплено тестом