16 KiB
Enforce-upstream-loop Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use the
sddskill (DSH) to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Добавить в DSH-бандл технические гейты upstream-лупа: /approve (единственный писатель approval.md), гейт A (план не пишется без утверждённого дизайна), гейт B (sdd/диспатчи не стартуют без утверждённого плана), гейт C (approval.md не пишется инструментами), включая bash-обходы.
Architecture: Чистое ядро dsh/bundle/lib/gates.js (decision-функции + io approval.md + разрешение план→change→approval + sha256) поверх существующего ctx.tools.guard; /approve — команда с подтверждением через ctx.userQuestions.ask; Bash-цели — через проверенный hooks/harness_target.py targets; wiring — в index.js после хуков (монотонная цепочка deny). CC-сторона не трогается.
Tech Stack: Node builtins (без @deepseek-ai/*-импортов), python3 для targets, bash для тестовых песочниц.
Spec: openspec/changes/enforce-upstream-loop/ — proposal.md + design.md (D-1…D-13; план спорит от них).
Models (D-9 стоящее решение владельца): Tasks 1, 2, 4 — deepseek-official/deepseek-flash; Task 3 — deepseek-official/deepseek-v4-pro; все ревью — deepseek-official/deepseek-v4-pro.
Global Constraints
- Runtime-зависимости — только Node builtins;
package.test.mjsenforcement обновляется при новых файлах. - Гейты — DSH-only (бандл); CC-хуки/роли/скрипты не меняются.
- Гейты монотонны: отказ не отменяется последующим слушателем; ошибка разбора — пропуск (как в хуках).
- bounded-задачи (.bounded, нет change/плана) не блокируются (D-6).
- Маркеры находок и статусы — по
~/.dsh/roles/dev.md; коммиты с(#59). - Язык субагентов — английский (R-8 от #69); маркеры/статусы/термины — без перевода; отчёты владельцу — по-русски.
Review Focus
| # | Вход/условие | Ожидаемое поведение |
|---|---|---|
| RF1 | Write плана в docs/superpowers/plans/ без **Spec:** (или legacy-путь) |
DENY (D-9) |
| RF2 | Write плана со Spec на change без design-секции | DENY «design not approved» (гейт A) |
| RF3 | subagent/workflow с план-ссылкой при sha256-расхождении или без plan-секции |
DENY «plan not approved / changed» (гейт B, D-10) |
| RF4 | Write в **/approval.md инструментами (в т.ч. bash heredoc) |
DENY (гейт C, D-1/D-8) |
| RF5 | /approve design без builder-check.md; /approve plan без design-секции или до плана |
Отказ с причиной (D-5/D-1) |
Task 1: Ядро gates.js (чистые decision-функции + approval io)
Files:
- Create:
dsh/bundle/lib/gates.js - Create:
dsh/tests/gates.test.mjs
Interfaces:
-
Consumes: ничего (чистые функции + fs только в io-функциях).
-
Produces:
parseApproval(text) -> { design?: {by,at}, plan?: {by,at,sha256} }(yaml-подмножество; толерантен к отсутствию файла →{}).serializeApproval(parsed) -> string(детерминированный вывод секций).extractSpec(content) -> string|null— строка**Spec:** <путь>из markdown (варианты: бэктики, трейлинг-слэш; нормализация).resolveChangeFromSpec(specPath, repoRoot) -> changeId|null—openspec/changes/<id>/...→ id; иначе null (legacydocs/superpowers/specs/— null).resolvePlanFromSdd(sddPath, repoRoot) -> planPath|null—.superpowers/sdd/<план>/...→<repoRoot>/docs/superpowers/plans/<план>.mdесли существует.planApproved(approval, planPath) -> {ok:boolean, reason:string}— естьplan:иsha256 === hashPlan(planPath);hashPlan(path) -> string(sha256 содержимого).checkPlanWrite({tool, filePath, content, repoRoot}) -> {deny?: string}— гейт A: path подdocs/superpowers/plans/; Spec извлечён → change → approval.md: нет design → deny «design not approved; run /approve design»; Spec нет/legacy → deny (D-9); иначе ok.checkApprovalWrite({tool, filePath, repoRoot}) -> {deny?: string}— гейт C: basenameapproval.md→ deny «approval.md is written only by /approve».checkDispatch({tool, prompt, script, repoRoot}) -> {deny?: string}— гейт B: план-ссылки в prompt/script → план → Spec → change →planApproved; fail → deny «plan not approved; run /approve plan» / «plan changed after approval; re-approve».checkBashTargets(targets, repoRoot)— гейты A/C по списку файловых целей (изharness_target.py targets).
-
Step 1: Падающие тесты в
dsh/tests/gates.test.mjs(tempDir-фикстуры: репозиторий сdocs/superpowers/plans/,openspec/changes/demo/, approval.md):
test('extractSpec: backticks and trailing slash normalize', ...) // '**Spec:** `openspec/changes/demo/`' -> 'openspec/changes/demo'
test('checkPlanWrite: no Spec -> deny', ...) // RF1
test('checkPlanWrite: legacy docs/superpowers/specs -> deny', ...)
test('checkPlanWrite: design approved -> ok', ...)
test('checkPlanWrite: design not approved -> deny with /approve hint', ...) // RF2
test('checkApprovalWrite: approval.md via tool -> deny', ...) // RF4
test('checkDispatch: prompt plan ref, plan approved -> ok', ...)
test('checkDispatch: no plan section -> deny', ...) // RF3
test('checkDispatch: sha256 mismatch after edit -> deny re-approve', ...) // RF3
test('checkDispatch: workflow script with plan ref -> deny/ok same as subagent', ...)
test('planApproved: happy / missing / mismatch', ...)
test('serialize/parse roundtrip preserves sections', ...)
- Step 2: Прогони — FAIL (
node --test dsh/tests/gates.test.mjs, модуля нет). - Step 3: Реализуй
lib/gates.jsпо интерфейсам. Io-функции читают ФС только по переданным путям; никаких сетевых/глобальных сайд-эффектов. - Step 4: Прогони — PASS; затем
bash dsh/tests/run.sh(baseline 110/0). - Step 5: Коммит
git add dsh/bundle/lib/gates.js dsh/tests/gates.test.mjs
git commit -m "feat(dsh): gates — чистые decision-функции гейтов A/B/C и approval io (#59)"
Task 2: Команда /approve
Files:
- Create:
dsh/bundle/command-approve.js - Create:
dsh/tests/approve.test.mjs
Interfaces:
-
Consumes:
parseApproval/serializeApproval(Task 1),ctx.commands.register(object-form, как command-role.js),ctx.userQuestions.ask({...}) -> Promise<{confirmed:boolean}>-подобное (фейк в тестах). -
Produces:
registerApproveCommand({commands, userQuestions, repoRoot, logger})— команда/approve <change-id> design|plan:design: требует<repoRoot>/openspec/changes/<id>/proposal.md,design.md,builder-check.md; отказ — текст с перечнем отсутствующего.plan: требует design-секцию в approval.md и ровно один план со Spec на change (несколько — отказ с перечнем); пишетplan: {by:'owner', at:<ISO>, sha256:hashPlan(plan)}.- Оба: перед записью —
userQuestions.ask(текст «Approve ?»); нет подтверждения — отказ «not confirmed»; отсутствует userQuestions — отказ «userQuestions service unavailable». - Запись: read→parse→обновить секцию→serialize→write; чужие секции не трогает.
-
Step 1: Падающие тесты (fake commands.register захватывает handler; fake userQuestions возвращает confirmed true/false; temp-репо):
test('approve design refuses without builder-check.md', ...) // RF5
test('approve design writes design section (by owner, at ISO)', ...)
test('approve design refuses without confirmation', ...)
test('approve plan refuses without design section', ...) // RF5
test('approve plan refuses when two plans reference the change', ...)
test('approve plan writes plan section with sha256 of the plan', ...)
test('approve refuses when userQuestions missing', ...)
- Step 2: Прогони — FAIL.
- Step 3: Реализуй
command-approve.jsпо интерфейсам (парсинг аргументов<id> <kind>; неизвестный kind → текст usage). - Step 4: Прогони — PASS;
bash dsh/tests/run.sh. - Step 5: Коммит
git add dsh/bundle/command-approve.js dsh/tests/approve.test.mjs
git commit -m "feat(dsh): /approve — единственный писатель approval.md с подтверждением владельца (#59)"
Task 3: Wiring в index.js + bash-цели + списки пакета
Files:
- Modify:
dsh/bundle/index.js - Modify:
dsh/tests/selfcheck.test.mjs(apply-тесты гейтов) - Modify:
dsh/bundle/package.json(files+=command-approve.js) - Modify:
dsh/tests/run.sh(node --check+=command-approve.js) - Modify:
dsh/tests/package.test.mjs(no-@deepseek-ai-список += новые файлы)
Interfaces:
-
Consumes: Task 1 (gates.js), Task 2 (command-approve.js), существующая цепочка
ctx.tools.guard(lib/guards.js). -
Produces: в
apply():DEFAULTS += { approvalGates: true }; gatesOk — из selfcheck (tools.guard).- В guard-цепочке после hook-проверок: Edit/Write/MultiEdit →
checkPlanWrite+checkApprovalWrite;subagent/subagent_fork/workflow→checkDispatch(prompt/script изexec.arguments);Bash→ если в команде грубая подстрокаdocs/superpowers/plansилиapproval.md—spawnSync python3 hooks/harness_target.py targets '<cmd>' <cwd>, цели →checkBashTargets. Любой deny → причина отказа (hook-стильBLOCKED [approval-gates]: …). Ошибка/пропуск парсинга — allow (инвариант хуков). /approveрегистрируется в том жеctx.inject(['commands', 'userQuestions'])(userQuestions может отсутствовать — команда сама откажет, D-12).repoRootдля гейтов — cwd сессии (sessionCwd), как в guards.
-
Step 1: Падающие тесты (в selfcheck.test.mjs, fake ctx с tools.guard-захватом: apply() → вызвать захваченный guard с fake exec; temp-репо-фикстуры):
test('apply: guard denies plan write without design approval', ...)
test('apply: guard allows plan write after /approve-like approval.md', ...)
test('apply: guard denies subagent dispatch referencing unapproved plan', ...)
test('apply: guard allows dispatch after plan approval (sha match)', ...)
test('apply: bash heredoc into approval.md denied', ...) // RF4
test('apply: approvalGates:false disables all gates', ...)
- Step 2: Прогони — FAIL.
- Step 3: Реализуй wiring + списки пакета (файлы выше). Монотонность: порядок deny не отменяется; пропуск на ошибку разбора.
- Step 4: Прогони — PASS;
bash dsh/tests/run.sh(110+новые) иbash tests/run.sh(CC 630/0 — хуки не трогаются). - Step 5: Коммит
git add dsh/bundle/index.js dsh/bundle/package.json dsh/tests/selfcheck.test.mjs dsh/tests/run.sh dsh/tests/package.test.mjs
git commit -m "feat(dsh): wiring гейтов A/B/C в guard-цепочку + bash-цели + /approve (#59)"
Task 4: Скиллы, роли, спека-дельта, доки
Files:
- Modify:
dsh/skills/sdd/SKILL.md(шаг до первого диспатча: проверить approval-маркер плана; BLOCKED → не обходить, вернуть NEEDS_CONTEXT/владельцу) - Modify:
dsh/install.sh(install_role_dsh_notes— DSH-заметка в roles/dev.md: builder-check обязателен,/approve— маркер, гейты A/B/C и их DENY-тексты) - Modify:
openspec/changes/enforce-upstream-loop/specs/approval-gates/spec.md(новая capability: ADDED требования — артефакты, /approve-контракт, гейты A/B/C, bash-обход, bounded-исключение, deployment D-13) - Modify:
dsh/README.md(Configuration +=approvalGates; Known limits — гейты активны только в сессиях с новым бандлом),dsh/PORT.md(§1/§7 — чем закрыт риск «инструкции ≠ enforcement»)
Interfaces: Consumes: поведение Tasks 1–3. Produces: спека-дельта (валидируется openspec validate enforce-upstream-loop --strict), тексты скилла/роли, доки.
- Step 1: Тесты — в
dsh/tests/добавить assert:dsh/skills/sdd/SKILL.mdсодержит строку «approval» / «/approve» (текстовый пин),dsh/install.sh --rolesвставляет заметку с «/approve» (расширить существующий install-тест или добавить в install.test.mjs). - Step 2: Прогони — FAIL.
- Step 3: Реализуй тексты и дельту;
openspec validate enforce-upstream-loop --strict— valid. - Step 4: Прогони — PASS:
bash dsh/tests/run.sh,bash tests/run.sh,openspec validate … --strict. - Step 5: Коммит
git add dsh/skills/sdd/SKILL.md dsh/install.sh dsh/tests openspec/changes/enforce-upstream-loop/specs dsh/README.md dsh/PORT.md
git commit -m "docs(dsh): sdd-скилл, roles-заметка, спека approval-gates, доки (#59)"
Предусловия окружения (контроллер проверяет один раз до Task 1)
python3,git,nodeв PATH;bash dsh/tests/run.shзелёный на входной точке веткиfeat/59-loop-gates(baseline 110/0).- Сеть не нужна; тесты — temp-репозитории; реальный
~/.dshне трогается. - Гейты в этой ветке ещё НЕ активны (D-13): исполнение идёт на старом рантайме, retro-утверждений не требуется; переустановка бандла после merge — шаг чеклиста закрытия.