From 5f48a425143e525398ce5457040aea2cada2a8b0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 10 Jun 2026 22:51:31 -0400 Subject: [PATCH] =?UTF-8?q?docs(#1022):=20resolve=20step-vs-gate=20model?= =?UTF-8?q?=20question=20=E2=80=94=20steps=20additive,=20gates=20block,=20?= =?UTF-8?q?mode=20self-gates=20(#1025)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the resolution of #1022 (surfaced scoping the §5.6 ui-phase cutover): a step is purely additive and never halts the host; host-blocking preconditions are gates (blocking/onError:halt) — no hook-model change. Runtime/mode context (auto/chain vs manual) self-gates in the skill, not via when (config-only). §5.6 decomposes into the existing plan:pre step (ui-phase, self-gates on frontend + pipeline) + a new plan:pre gate (frontend-and-no-UI-SPEC → halt, when: workflow.ui_safety_gate) that blocks planning in manual mode — preserving the "run /gsd:ui-phase first" UX (maintainer call: pipelines-only auto-fire). The render-hooks dispatch template grows to handle gates, not just steps. Recorded in ADR-894 (clarification) + CONTEXT.md (RULESET.CAPABILITY.step-additive-gate-blocks). Unblocks the §5.6 cutover. Closes #1022 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 --- CONTEXT.md | 2 ++ docs/adr/894-capability-declaration-format.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index a492e459f..8cc1d93fb 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -173,6 +173,8 @@ A Capability whose integration shape brings its own external process, service, o `RULESET.CAPABILITY.cutover-self-gating=a phase-6 per-feature cutover moves the host's phase-context detection + mode/flag logic INTO the skill (self-gating, per ADR-894); the loop hook is intentionally COARSE — "invoke skill X at point Y when config Z" — and carries no detection/mode. WORKED EXAMPLE: plan-phase.md §5.6 UI gate (frontend-detection via ui-safety-gate.cjs + --auto/manual branch + --skip-ui bypass) must move into gsd-ui-phase before its plan:pre hook can replace the inline call without behavior loss. Spike #1018 finding.` +`RULESET.CAPABILITY.step-additive-gate-blocks=a `step` hook is purely additive (invoke skill + produce artifacts, NEVER halts the host); host-blocking preconditions are `gate`s (blocking:true, onError:halt); runtime/mode context (auto/chain vs manual) self-gates IN THE SKILL, not via `when` (config-only). §5.6 = plan:pre step (ui-phase; skill self-gates on frontend+pipeline, auto-fires only in pipelines) + a NEW plan:pre gate (frontend-and-no-UI-SPEC → halt, when:workflow.ui_safety_gate); the loop.render-hooks dispatch template handles steps AND gates. Resolves #1022.` + ### Runtime Config Adapter Registry Module owning the explicit per-runtime config-mutation dispatch table for the installer. `resolveRuntimeConfigIntent(runtime)` projects a typed config intent — `installSurface` (`settings-json` | `codex-toml` | `copilot-instructions` | `cline-rules` | `cursor-hooks-json` | `profile-marker-only`), `writesSharedSettings` (the `finishInstall` shared-settings write gate), and `finishPermissionWriter` (`opencode` | `kilo` | none) — that `bin/install.js` dispatches on instead of inline `runtime === '...'` branching. Owns adapter selection only: it performs no filesystem IO and does not execute config mutations (the install/finishInstall handlers and the per-runtime writers do that). Unknown runtimes fail loudly with a `TypeError`, guarded by an `Object.hasOwn` own-property check so prototype-chain keys (`__proto__`, `constructor`) also throw. Realizes the adapter-selection half of the Runtime Install Policy Module boundary. Source: `gsd-core/bin/lib/runtime-config-adapter-registry.cjs`. See ADR-58, #60. diff --git a/docs/adr/894-capability-declaration-format.md b/docs/adr/894-capability-declaration-format.md index c178ed6e8..5b4eba5fa 100644 --- a/docs/adr/894-capability-declaration-format.md +++ b/docs/adr/894-capability-declaration-format.md @@ -74,6 +74,14 @@ Schema-validated JSON. Common envelope + role-typed body (`role: feature | runti **Hook activation (`when`).** A hook may declare a cheap, **deterministic `when`** over config keys + capability-enablement (e.g. `"workflow.ui_phase"`); `loop.render-hooks` evaluates it to decide whether the hook is active. **Deeper context applicability** ("is this actually a frontend phase?", "are ORM files in scope?") is *not* declared — it stays inside the dispatched skill/agent, which no-ops if inapplicable, exactly where that judgment lives today. This deliberately avoids a phase-context predicate vocabulary that would drift from reality. Consequence: when an entry step self-gates (produces no artifact), its downstream same-capability gate/step must **degrade gracefully** (e.g. `ui.safety-gate` passes when there is no `UI-SPEC.md`) — that is the skill/query's responsibility. +### Clarification — steps are additive, gates block, mode self-gates (resolving #1022) + +A **`step` is purely additive** — it invokes a skill and may produce artifacts, but it **never halts or redirects the host workflow**. A host-blocking precondition (e.g. *"do not plan a frontend phase without a UI design contract"*) is modeled as a **`gate`** (`blocking: true`, `onError: halt`); gates already block, so steps gain no halt power. (Surfaced cutting over `plan-phase.md` §5.6, whose manual-mode branch hard-exits the host — that behavior is a gate, mis-inlined as step logic.) + +**Runtime/mode context** — whether we are in a `--auto`/`--chain` pipeline vs a manual invocation — is likewise **not** a hook-activation concern (`when` is config-only and deterministic). It **self-gates inside the skill**, exactly as phase-context applicability does: the skill no-ops when its mode precondition isn't met. + +**§5.6 worked decomposition.** The `plan:pre` **step** (`ref.skill: ui-phase`, `when: workflow.ui_phase`) auto-fires `gsd-ui-phase`, which self-gates on (a) frontend detection and (b) pipeline context — auto-generating `UI-SPEC.md` only in `--auto`/`--chain` runs. A new `plan:pre` **gate** (`check`: a "frontend phase with no `UI-SPEC.md`" query, `blocking: true`, `onError: halt`, `when: workflow.ui_safety_gate`) blocks planning in manual mode when a frontend phase still lacks a UI-SPEC — preserving today's "run `/gsd:ui-phase` first (or `--skip-ui`)" UX without forcing the interactive skill inline. Consequently the `loop.render-hooks` dispatch template handles **both** active steps (invoke the skill) and active gates (run the check; halt if blocking + failed), not steps alone. + **Gate `check`** is one of: - `{ query: "" }` — deterministic first-party code; **may block**. - `{ predicate: { kind: "artifact-exists" | "config-equals" | …, … } }` — declarative, no code; **may block**.