19 KiB
Raw Blame History

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 без 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 в нём есть требование обязательного падающего теста до кода, и это требование закреплено тестом