diff --git a/openspec/changes/dsh-prestep/.openspec.yaml b/openspec/changes/archive/2026-10-25-dsh-prestep/.openspec.yaml similarity index 100% rename from openspec/changes/dsh-prestep/.openspec.yaml rename to openspec/changes/archive/2026-10-25-dsh-prestep/.openspec.yaml diff --git a/openspec/changes/dsh-prestep/approval.md b/openspec/changes/archive/2026-10-25-dsh-prestep/approval.md similarity index 100% rename from openspec/changes/dsh-prestep/approval.md rename to openspec/changes/archive/2026-10-25-dsh-prestep/approval.md diff --git a/openspec/changes/dsh-prestep/builder-check.md b/openspec/changes/archive/2026-10-25-dsh-prestep/builder-check.md similarity index 100% rename from openspec/changes/dsh-prestep/builder-check.md rename to openspec/changes/archive/2026-10-25-dsh-prestep/builder-check.md diff --git a/openspec/changes/dsh-prestep/design.md b/openspec/changes/archive/2026-10-25-dsh-prestep/design.md similarity index 100% rename from openspec/changes/dsh-prestep/design.md rename to openspec/changes/archive/2026-10-25-dsh-prestep/design.md diff --git a/openspec/changes/dsh-prestep/proposal.md b/openspec/changes/archive/2026-10-25-dsh-prestep/proposal.md similarity index 100% rename from openspec/changes/dsh-prestep/proposal.md rename to openspec/changes/archive/2026-10-25-dsh-prestep/proposal.md diff --git a/openspec/changes/dsh-prestep/specs/session-prestep/spec.md b/openspec/changes/archive/2026-10-25-dsh-prestep/specs/session-prestep/spec.md similarity index 100% rename from openspec/changes/dsh-prestep/specs/session-prestep/spec.md rename to openspec/changes/archive/2026-10-25-dsh-prestep/specs/session-prestep/spec.md diff --git a/openspec/specs/session-prestep/spec.md b/openspec/specs/session-prestep/spec.md new file mode 100644 index 0000000..c7014f8 --- /dev/null +++ b/openspec/specs/session-prestep/spec.md @@ -0,0 +1,271 @@ +# 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] Проект не подключён к обвязке: +нет: <список отсутствующего через '; '>. Предложи владельцу настройку: +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] Проект не + подключён к обвязке: нет: 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** сессия стартует в `/dsh/bundle`, а сигналы лежат в `` +- **THEN** корнем считается ``, и текст инъекции называет `` + +#### 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 считаться текст, начинающийся с +``, который строится из хэша текста +инъекции и стоит первой строкой тела; одна и та же видимая инъекция 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** в нём есть требование обязательного падающего теста до кода, и это + требование закреплено тестом