claude-harness/docs/superpowers/plans/2026-10-25-enforce-upstream-loop.md

16 KiB
Raw Blame History

Enforce-upstream-loop Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use the sdd skill (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.mjs enforcement обновляется при новых файлах.
  • Гейты — 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 (legacy docs/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: basename approval.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 — шаг чеклиста закрытия.