2026-10-25 13:54:39 +04:00

272 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# session-prestep Specification
## 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`.
## 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` без `openspec` MUST изолировать
PATH (каталог-заглушка как весь PATH), а не наследовать host PATH с
фиксированным системным префиксом: глобальный npm-префикс на Linux кладёт
`openspec` в `/usr/bin`, и тест «бинарника нет» иначе проверяет не то.
- O-2: `install.sh` MUST предупреждать (WARN, прогон продолжается) о смешанном
состоянии, когда `DSH_HOME` пришёл из окружения, указывает на существующий
каталог и не равен `$HOME/.dsh`, при нестандартном HOME: изоляция только HOME
пишет в реальный `DSH_HOME`. WARN MUST быть один на прогон и MUST
срабатывать и в `--global` (`install_global`), а не только в `--skills`.
- T3.4: `dsh/skills/sdd/SKILL.md` MUST содержать порядок 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** в нём есть требование обязательного падающего теста до кода, и это
требование закреплено тестом