diff --git a/CLAUDE.md b/CLAUDE.md index c620b13..6b1d766 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -251,6 +251,114 @@ not all of `.context/`, which would strip the committed `codex-gate.on` adoption merely shares the name (`docs/commands/reference.md`) fires too, which is the redundant reminder invariant 2 accepts by name. +### Profiles — how much review this story gets + +A story may carry a profile in its header: `**Risk:**` (`trivial|standard|high`), +`**Security:**` (`none|standard|high`), and a `**Validation:**` mode derived from them +(`battery` / `battery+check` / `battery+check+verification`, plus `+abuse-path` when and +only when security is `high`). The **story header is the single writable copy** — specs, +plans, commit bodies and this file's prompts carry the story **path** and read the values +fresh at each pass, never a remembered or copied value. + +**The axes steer the questions; the mode steers the evidence.** Separate levers: one aims +the reviewer, the other obliges the author. + +**Lens sets, appended to the gate prompt:** +- **risk `high`** → threats, abuse, rollback, data loss, idempotency, compatibility, + observability. +- **security `standard` or `high`** → assets, trust boundaries, roles, external systems, + abuse paths. +- **both** → the union appended **once**, each lens labelled with the axis that motivated + it; risk's *abuse* and security's *abuse paths* are **one lens carrying both labels**, + not two questions. + +Lenses are **different questions, not more passes.** The 3-pass floor, the Blocker/Major +filter, the file-first findings protocol and the clean-final-pass rule are unchanged. + +**Reading the profile — three cases, three answers:** +1. The artifact **cites no story** → run unprofiled and **say so** in the pass. Artifacts + predating this rule are the common case; stopping on them would halt in-flight work. +2. The cited story has **no profile line** → same: today's behaviour. +3. A profile is **present but unresolvable** → **stop and surface the cause**. That covers + the syntactic failures — unparseable line, a value outside the enums, two profile + blocks, a citation resolving to nothing — **and the semantic ones**: a `**Validation:**` + value disagreeing with `max(risk, security)`, or `+abuse-path` present without security + `high` or absent with it. Only the **latest `mode override`** in the log, moving in a + direction compatible with the current value, can explain such a mismatch — and if the + log also contains an `axis change`, only when that override was recorded **after** the + latest one, since an axis change voids every prior override. A log with no `axis + change` at all is the ordinary intake-time override, and its entry resolves the + mismatch on its own. A well-formed value can still be the wrong value, and a stale mode steers + weaker evidence while looking entirely valid; recomputing it is a profile change like + any other — proposed, human-confirmed, logged. Falling back to the lighter behaviour on + a malformed profile would under-review exactly the stories most likely to have one. + +**The Gate-B triviality skip needs two independent conditions**, and an eligible profile +never makes a behaviour-changing diff skippable: the change itself is **behaviourally +trivial** (the pre-existing judgement, unchanged by profiles), **and** for a profiled story +`max(risk, security)` is 0 — risk `trivial` *and* security `none`, never risk alone. The +**skip reason is recorded in the commit body** — not in the profile log, which records +profile *changes*, and a skip changes no profile value. **A skip removes the review, never +the evidence**, and what is owed follows the profile: a skipped **profiled** story runs the +battery and lands its evidence entry beside the reason; a skipped **unprofiled** story +records the reason and the battery result and nothing more, because it owes no mode-derived +entry and keeps exactly today's judgement-based skip. + +**A cycle citing several stories** aggregates along separate dimensions, never through one +winning mode: the **battery runs once** for the cycle; **each cited _profiled_ story +satisfies its own mode and suffix**, with its own named evidence entry, while a cited +**unprofiled** story has no mode and owes no entry; the **lens sets are unioned** across +all cited stories; and the cycle is skip-eligible only if **every** cited story is. A +single "max" would either under-serve the strictest story or impose its obligations on +unrelated ones. + +**What the author owes before Gate B**, by mode: `battery` = the quality battery green · +`battery+check` = battery + **a check that fails without the change** · +`battery+check+verification` = battery + that check + a **named** verification of the risk +path · `+abuse-path` = one **named** abuse scenario plus evidence that the expected control +rejects or contains it. Level 2's two obligations are distinct; one artifact serves both +only if it demonstrates both. + +A check need not be an automated test — where none is possible, a **named verification** +satisfies it and the entry says which route was taken and why. Either route owes the +**counterfactual**: the observation against the prior state. An **unobservable +counterfactual is a blocking evidence gap**, not a free pass — stop and surface; the human +may then lower the mode as a logged override. A fabricated test satisfies nothing. + +**The evidence entry lives in the commit body** (see Mechanics), carries the **story path +and the named evidence but not the mode value**, and is **revalidated before every Gate-B +re-review and before the cycle-closing amend** — a fix changes the diff even when the +profile sits still. If revalidation changes the entry, the clean pass no longer covers what +is being committed: fix, re-review, close on the entry that pass validated. + +**Every Gate-B call and re-review carries the path of every cited story**, so the reviewer +reads each profile itself, **plus the current evidence entry, quoted verbatim, for each +cited *profiled* story** — an unprofiled one owes no mode-derived evidence, so it +contributes a path and nothing else. One profiled story means one pair; a cycle citing +several carries all of them, because the reviewer cannot union lenses it cannot see or +judge evidence it was never given. A reviewer handed neither can only review the diff — +the lenses and the evidence obligations would exist and never be consumed. + +**Two evidence gaps, two different answers.** Evidence that is *absent or inadequate* for +the mode is a **work gap**: produce it, then call. A project whose `AGENTS.md` names **no +verified quality command** cannot satisfy even `battery` — a **setup gap**: say what is +missing (`/workflow-init`'s battery step) rather than reviewing around it. Neither is a +reason to call Gate B against a weaker claim. + +**Changing a profile:** the pass **proposes the complete resulting header** — both axes, +the recomputed mode, any renewed override — and the **human confirms it**, in both +directions; an agent never moves it alone. On confirmation, correct the header and append +one profile-log line. Any axis change **voids every prior override**, raised or lowered, +and `+abuse-path` follows the current security value. Passes already run under the lower +profile **keep counting** toward the floor; only the **final clean pass** must run under +the current profile. Inside an active Gate-B cycle, fold the edit into the active `WIP:` +snapshot by amend — a non-`WIP` commit reads to the hook as the cycle closing and would +discard the accumulated passes. + +**What this does not do:** nothing checks which file a model actually read, whether the +header changed mid-call, or whether the lens sets were appended. This is instruction-backed +like the rest of §5; the detection is a reader comparing the pass against the story. + ### Mechanics (reference) - **Severity:** Blocker (wrong/unsafe/breaks invariant) · Major (design flaw → rework) → both must resolve. Minor · Nit → collect, never iterate. @@ -270,6 +378,11 @@ not all of `.context/`, which would strip the committed `codex-gate.on` adoption follow-up commit for two reasons: a `WIP: …` commit left in history defeats the naming convention it exists for, and a follow-up commit has nothing to commit when the review produced no fixes. + **The closing message carries the validated evidence entry for every cited profiled + story** — one each, and none for a cited unprofiled story, which owes no entry. The + amend replaces the WIP message wholesale, so an entry written only into the WIP body is + destroyed exactly when the cycle closes. The final commit body is the durable record; + a PR shows commit messages, so there is no second home to keep in sync. - **Timeout / abort:** a codex call that dies at the MCP tool-call timeout is retried once before surfacing to the user, and that retry *is* the single shared recovery attempt above — not a second one. An abort is an incomplete pass, so treat it as one: diff --git a/docs/coding-workflow.md b/docs/coding-workflow.md index 231bff9..b145332 100644 --- a/docs/coding-workflow.md +++ b/docs/coding-workflow.md @@ -74,8 +74,12 @@ the actual skills, commands, agent definitions, and hook messages of this plugin **1. Intake — from idea to story.** The front door turns a raw idea into a scoped story that captures *what* and defers *how*: the problem, the desired outcome, the acceptance criteria, which core invariants the change touches, the open questions, -and a rough size. The design ("how") is deliberately left out — it belongs to the -next stage. The value here is a shared, reviewable definition of done before +a rough size, and a **profile** — risk and security relevance, confirmed by the human, +with a validation mode derived from the two. The two axes **add** review lenses at the +gates for a risky or security-relevant change (they never subtract any: Gate A's floor and +the baseline questions are the same at every level), while the derived mode calibrates +what evidence the author owes before Gate B. The design ("how") is deliberately left out — +it belongs to the next stage. The value here is a shared, reviewable definition of done before anyone argues about approach. **2. Brainstorming to a spec.** Approaches are explored and decisions are settled @@ -119,8 +123,20 @@ checks is not enforcement. **8. Gate B on the code.** Before the change is committed, the independent reviewer reads the actual *diff* and checks it against the invariants file. It is re-run after every fix, because each fix changes the diff and invalidates the -prior review. Genuinely trivial changes may skip it; documentation-only changes -are considered covered by Gate A instead, since there is no code diff to review. +prior review. Trivial changes may skip it, on terms that depend on the story: an +unprofiled one keeps the judgement call, while a profiled one qualifies only at +effective level 0 — trivial risk *and* no security relevance — so a trivial-looking +change on security-relevant surface is not eligible. A skip removes the review, never +the evidence: the battery still runs, the reason is recorded in the commit body, and +so is one evidence entry per cited profiled story. **Explanatory** +documentation carries no gate at all — a wrong sentence there costs a confused reader +rather than broken behaviour. Prompt artifacts are not explanatory prose: in a project +whose product is prompts, the text *is* the behaviour, so the review policy requires Gate +B for them even though they are Markdown. Which paths count is spelled out in the policy +file, and a reminder hook classifies them independently; both err toward firing, and the +hook only reminds — it never blocks, and it is not what makes the review happen. When it +is unclear whether an artifact counts, review it: a redundant pass costs minutes, a +missed one costs the defect this loop exists to catch. **9. Pull request and bot review.** Automated reviewers comment on the PR. Their findings are processed *systematically*: pre-existing issues are tracked rather diff --git a/docs/getting-started.md b/docs/getting-started.md index 5282bf2..0ec38c9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,7 +11,16 @@ answering questions, approving drafts, judging findings. **1. Capture the idea.** Say "users want to export their invoices as CSV" (or paste a voice transcript — German is fine). The `intake` skill turns it into a story: problem, outcome, ≥3 checkable acceptance criteria, which `AGENTS.md` invariants it -touches. Too thin → it asks once and waits; it refuses to invent what you didn't +touches, and a **profile** — how risky this is (`trivial|standard|high`), how +security-relevant (`none|standard|high`), and the validation mode derived from the two. +It proposes both axes and the derived mode with a reason; you confirm or correct the +axes, and any change to the mode is an override that carries its own reason and is +logged. The two axes decide +which extra questions the review gates ask; the derived mode decides what evidence you +owe before Gate B, and the levels **accumulate**: a green battery, then a check that +fails without the change, then a named verification of the risk path on top of both. At +security `high` one more is added — a named abuse scenario and evidence that the control +rejects or contains it. Too thin → it asks once and waits; it refuses to invent what you didn't say. You approve the draft (the criteria are what "done" will mean), and it lands as a committed file under `docs/superpowers/stories/`. @@ -68,8 +77,12 @@ workflow gets stricter exactly where your project actually fails. The rhythm: minutes of questions and approval (1–2), two review loops where you mostly arbitrate (3–4), hands-off implementation (5–6), one more loop (7), PR -close-out (8–9). Trivial changes skip the ceremony — the caution bias is for -non-trivial work, judgment is allowed. Two knobs: `.context/codex-gate.floor` (any +close-out (8–9). Trivial changes travel lighter, within limits: **Gate B** may be skipped +only when the change is behaviourally trivial **and** the story is eligible — a profiled +one at effective level 0 (risk `trivial` *and* security `none`), an unprofiled one by the +prior judgement call. The profile supplies eligibility, never the skip itself; the battery +is still owed and Gate A's floor is unchanged at every level. The caution bias is +for non-trivial work, judgment is allowed. Two knobs: `.context/codex-gate.floor` (any positive integer) moves the 3-pass floor, and `touch .context/codex-gate.off` silences the reminders in a scratch workspace (delete to re-enable; state keeps tracking while off, so nothing goes stale). @@ -86,8 +99,12 @@ smallest that matches your intent: 1. **Don't adopt:** never run `/workflow-init` in a project → the plugin does nothing there. -2. **One trivial change:** just commit — the hook warns, it never blocks, and §5 - explicitly leaves trivial changes to your judgment. +2. **One trivial change:** the hook warns, it never blocks. What §5 permits depends on + the story: an **unprofiled** one keeps the old judgement call, while a **profiled** + one may skip Gate B only at effective level 0 (risk `trivial` *and* security `none`), + still owes the battery, and records both the skip reason and its evidence entry in the + commit body. Gate A is not + skippable at any level. 3. **Pause a project:** `touch .context/codex-gate.off` (delete to re-enable; state keeps tracking, so nothing goes stale). 4. **Leave for good:** remove §5 from the project's `CLAUDE.md` (and diff --git a/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md new file mode 100644 index 0000000..c78c9ad --- /dev/null +++ b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md @@ -0,0 +1,779 @@ +# Risk, Security, and Validation Profiles — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Story:** `docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md` +**Spec:** `docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md` (Gate A clean at pass 8) + +**Goal:** Add two human-confirmed profile axes and a derived validation mode to the story +header, and make the §5 gate prompts consume them, so review intensity scales with what a +story risks. + +**Architecture:** Prompt-and-template changes only. The `intake` skill gains a proposal +step and two template lines; `CLAUDE.md` §5 gains a **Profiles** subsection plus one +Mechanics clause; `commands/workflow-init.md` carries the same two additions in its inline +§5 template (invariant 8 keeps them inline). `process-pr-review` requires **both** conditions for a +Gate-B skip — the change is behaviourally trivial *and* every cited story is eligible — +where before it turned on the fix's size alone. No new script, no new scaffolded file, and no hook change beyond the +one advisory reminder string covered by the waiver below. + +**Tech Stack:** Markdown prompts. Verification is the repo battery (shellcheck, hook +tests, `check-invariants.sh`, `check-version-bump.sh`, `claude plugin validate`) plus +Gate B. + +## Global Constraints + +Copied verbatim from the spec and AGENTS.md. Every task's requirements implicitly include +this section. + +- **Invariant 8** — `/workflow-init`'s templates stay **inline in the command body**. + Never read a template from disk. +- **Invariant 9** — `/workflow-init` never overwrites silently. +- **Invariant 10** — the trigger lists, level criteria and lens sets stay **stack-neutral**. +- **Invariant 11** — every touched prompt passes all 12 items of `docs/prompt-standards.md`. +- **Invariant 12** — a plugin change requires a `plugins/dev-workflow/.claude-plugin/plugin.json` + version bump (Task 5). +- **The docs-drift pair:** `CLAUDE.md` §5 and the inline §5 template in + `plugins/dev-workflow/commands/workflow-init.md` **change in the same commit, always.** + Task 2 owns both; splitting them across commits is a plan violation. +- **No profile VALUE is echoed** outside the story header — not in the spec, plan, profile + log, or commit body. Those carry the story **path**. +- **No gate-off path is added.** The 3-pass floor, the Blocker/Major filter and the + file-first findings protocol are untouched by every task here. +- **Downstream-neutral template text:** the `workflow-init` copy must not reference this + repo's own files, incident counts, or `docs/` paths that `/workflow-init` never + scaffolds. + +**Scope-guard waiver, granted 2026-07-26 during Gate B pass 11.** The story's guard +excluded `plugins/dev-workflow/hooks/**`. One string in `hooks/codex-gate.sh` is changed +anyway: the below-floor reminder said "proceed only if this change is trivial", which the +narrowed skip rule makes false, and it says so at exactly the moment an author decides +whether to skip. The waiver holds because the edit runs *opposite* to what the guard +protects — it **removes** a rule statement from the hook and defers to the policy file, so +the hook reads no profile and gains no machinery; it ends up more profile-agnostic than +before. The replacement points at the policy without paraphrasing the rule, so no new sync +surface is created. Same-commit propagation applies: §5's skip rule changed, so its echo in +the hook changes with it, not in a follow-up. No test asserts that clause (the two hook +tests match only `below floor|floor NOT met`), and 0.7.0 already covers the plugin bump. + +**Commit discipline for this plan:** Tasks 1–4 end with a `WIP:` commit — the hook treats a +`wip`-prefixed message as cycle-internal, so it neither fires a Gate-B STOP nor resets the +pass counters. Task 5 runs the battery, Gate B, and closes the whole cycle with one real +commit via `git reset --soft` + commit. + +--- + +### Task 1: `intake` proposes the profile and the story template records it + +**Files:** +- Modify: `plugins/dev-workflow/skills/intake/SKILL.md` — a **new Flow step 3** inserted + before the existing "One question round, then pause" (line 45 region), the question-round + step itself, the story template (lines 94–120), and the calibration block after it + (lines 122–128) +- Test: none — prompt artifact, no test harness exists (AGENTS.md: "there is no + application code, so there is no typechecker to catch a defect"). Verification is + Task 5's battery + Gate B. + +**Interfaces:** +- Produces: the header line `**Risk:** … · **Security:** … · **Validation:** …` and the + `**Profile log:**` block that Task 2's §5 text reads. The field names and the enum + spellings defined here are quoted verbatim by Task 2 — change one, change both. + +- [ ] **Step 1: Insert the profile assessment as a new Flow step 3, BEFORE the question round** + +Ordering is the whole point: the proposal must exist before the single question round +opens, or it cannot ride in it. Insert the block below as the **new step 3**, immediately +after "**Detect ambiguity**" and immediately before "**One question round, then pause**", +then renumber the existing steps 3–10 to 4–11. + +```markdown +3. **Assess the profile** — derive a proposal for two axes and a validation mode, so the + single question round below can carry it. Nothing is recorded yet: + - **Risk** — `trivial` (no behavioural effect in the artifact's own execution + context; for a **prompt artifact the text is the behaviour**, so a wording change + to a skill, command, agent definition, hook message or template is not trivial by + default) · `standard` (a behaviour change hitting no named trigger — the default) + · `high` (the change affects a named **domain** trigger — auth, permissions, + payments, migrations, data deletion, public APIs, personal data, supply chain — or + a named **effect** trigger — irreversibility, data loss or corruption, outage + exposure). + - **Security relevance** — `none` (touches no asset, trust boundary, role or + external system — a real answer and the common one) · `standard` (touches one + without changing what it permits) · `high` (changes a trust boundary, an + authorization decision, or the handling of secret or personal data). + - **Validation mode**, *derived not asked*: effective level = `max(risk, security)` + over `none|trivial → 0`, `standard → 1`, `high → 2` → `battery` / + `battery+check` / `battery+check+verification`, plus `+abuse-path` when and only + when security is `high`. + + Triggers match **surfaces, not words**: a doc that mentions auth is not `high`; a + change to an authorization decision is `high` even if the word never appears. + + Carry all three into the next step's question round with one line of reason each. +``` + +Then extend the question-round step (now step 4) so it asks them. Its current text reads +"**One question round, then pause** — ask at most one round, then **wait** for the user"; +append to that step: + +```markdown + The round also carries the **profile proposal** from step 3 — alongside any + clarifying questions, or as the whole round when nothing needs clarifying. Intake + gains no second pause. The human confirms or corrects, and what a correction means + depends on what it touches: + - **An axis correction sets that axis freely, up or down — and the mode is + recomputed** from the corrected axes. Never carry the proposed mode over beside a + changed axis: `**Security:** high` next to `**Validation:** battery` is a header + the gates classify as unresolvable and stop on. + - **A mode correction is an override**, and it is bounded: it may raise or lower the + derived mode, but it cannot drop `+abuse-path` while security is `high`. Record it + as the first `mode override` entry in the profile log, with its direction and the + human's reason. **Say in the proposal that an override needs a reason**, since the + log entry cannot be written without one. + + **State the derivation in the proposal, so one answer settles the whole header.** The + mode is a function of the axes, not an independent choice: show `max(risk, security)` + and what each level yields, and the human's single answer then confirms the axes, any + override, *and* the mode that derivation produces from them — including after a + correction. That is why no second pause is needed and none is taken: nothing is left + for the human to choose once the axes are settled. + + **An answer that cannot be recorded ends this intake attempt** — an override with no + reason, an override dropping `+abuse-path` while security is `high`, a mode outside the + enums. Say which rule the answer collides with and **stop without writing**; do not + hold a pause open waiting for a repair, and do not invent the missing piece. This is + the grounding floor's shape, and it is honest about what happens next: the human's + corrected answer arrives at a **new intake run**, which opens its own single round with + that answer already in hand. What is forbidden is a second round *inside* one run, not + the human coming back. + + "Proceed anyway" or "I don't know" **is** an answer: it accepts the proposal as it + stands, and the values are recorded as proposed and **never lower** — a redundant lens + costs a paragraph, a missing one costs a review. There is no "unconfirmed" profile, so + the story is not written until the profile is settled. +``` + +- [ ] **Step 2: Add the header line and profile log to the story template** + +Replace the template's date line (line 97): + +```markdown +**Date:** YYYY-MM-DD · **Size:** chore | story | epic-needs-splitting +``` + +with: + +```markdown +**Date:** YYYY-MM-DD · **Size:** chore | story | epic-needs-splitting +**Risk:** trivial | standard | high · **Security:** none | standard | high · **Validation:** + + +**Profile log:** +- YYYY-MM-DD · axis change · risk ↑ · Gate-B pass 2 finding on the migration path · adds the risk lens set +``` + +- [ ] **Step 3: Document the profile log after the size calibration block** + +The code block above shows *where* it goes. Insert this after the "**Acceptance +criteria** describe observable outcomes…" paragraph (line 128) to say *what* each field +means: + +```markdown +**Profile log.** A `**Profile log:**` label directly beneath the profile header, followed +by `-` entries — written **on the first change and never before**, so a story whose +profile never moves carries no empty block. One line per event: date · **event kind** · +**direction** · **reason or trigger** (a finding reference when a finding caused it, plain +prose when none did) · what it changes downstream. The three kinds: + +- `axis change` — names the **axis identity** (`risk`, `security`, or both in one entry + when both move) and a **direction per named axis**, since both may move at once and in + opposite directions: + `- 2026-07-26 · axis change · risk ↑, security ↓ · pass-3 finding: no auth surface after all · swaps the risk lens set for none` +- `mode override` — names its own **direction**, because a human may raise or lower the + derived mode. A lowering removes an obligation, so its reason says which one and why: + `- 2026-07-26 · mode override · ↓ · the risk-path verification duplicates the migration rehearsal already run in staging · drops the named verification, keeps the counterfactual check` + + Choosing a **named verification** because no automated test is possible is **not** an + override: it is one of the two routes that satisfy `+check` at the same mode, and it + still owes the counterfactual. Logging it as a lowering would record a mode the header + never moved to. *(Corrected during Gate B pass 2 — the original example illustrated the + opposite of the rule.)* +- `adoption` — has no previous value, so **no direction**: + `- 2026-07-26 · adoption · in-flight story adopting a profile at its plan checkpoint · gates now read this header` + +The log never restates the values — the header is the single writable copy, and git +history already holds what the previous values were. A mode override chosen during intake +confirmation is the first `mode override` event and creates the log. +``` + +- [ ] **Step 4: Update the six-sections wording** + +The template's intro (line 92) reads "Write the file with exactly these six `##` +sections, in order:". The profile lines are header, not a `##` section, so this sentence +stays true and **must not change**. Confirm by reading it; do not edit. + +Also update the step that re-validates an edited draft (was step 8, now step 9) — its +parenthetical currently reads `(six sections, no-HOW except §4, grounding floor, ≥3 +checkable criteria)`. Replace with: + +```markdown + **re-validate the edited draft against every constraint** (six sections, the profile + header line, no-HOW except §4, grounding floor, ≥3 checkable criteria), and re-present. +``` + +- [ ] **Step 5: Verify no other numbered cross-reference broke** + +Run: `grep -n "step [0-9]\|steps [0-9]" plugins/dev-workflow/skills/intake/SKILL.md` +Expected: every referenced number matches the renumbered flow. Fix any that point at the +old numbering. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/dev-workflow/skills/intake/SKILL.md +git commit -m "WIP: intake proposes risk/security profile and records it in the story header" +``` + +--- + +### Task 2: §5 gains the Profiles subsection and the Mechanics clause — BOTH copies, one commit + +**Files:** +- Modify: `CLAUDE.md` — insert the Profiles subsection before `### Mechanics (reference)` + (line 254); amend the "Finishing the cycle" bullet (lines 266–269) +- Modify: `plugins/dev-workflow/commands/workflow-init.md` — the same two edits in the + inline §5 template, before `### Mechanics (reference)` (line 429) and in its + "Finishing the cycle" bullet (lines 441–444) +- Test: none (prompt artifact); Task 5 verifies + +**Interfaces:** +- Consumes: Task 1's header field names and enum spellings, quoted verbatim below. +- Produces: the gate-prompt rules every later gate run follows. + +**This task is one commit covering both files.** A commit touching only one of them is the +docs-drift class this repo logs most often. + +- [ ] **Step 1: Write the Profiles subsection into `CLAUDE.md`** + +Insert immediately **before** the line `### Mechanics (reference)`: + +```markdown +### Profiles — how much review this story gets + +A story may carry a profile in its header: `**Risk:**` (`trivial|standard|high`), +`**Security:**` (`none|standard|high`), and a `**Validation:**` mode derived from them +(`battery` / `battery+check` / `battery+check+verification`, plus `+abuse-path` when and +only when security is `high`). The **story header is the single writable copy** — specs, +plans, commit bodies and this file's prompts carry the story **path** and read the values +fresh at each pass, never a remembered or copied value. + +**The axes steer the questions; the mode steers the evidence.** Separate levers: one aims +the reviewer, the other obliges the author. + +**Lens sets, appended to the gate prompt:** +- **risk `high`** → threats, abuse, rollback, data loss, idempotency, compatibility, + observability. +- **security `standard` or `high`** → assets, trust boundaries, roles, external systems, + abuse paths. +- **both** → the union appended **once**, each lens labelled with the axis that motivated + it; risk's *abuse* and security's *abuse paths* are **one lens carrying both labels**, + not two questions. + +Lenses are **different questions, not more passes.** The 3-pass floor, the Blocker/Major +filter, the file-first findings protocol and the clean-final-pass rule are unchanged. + +**Reading the profile — three cases, three answers:** +1. The artifact **cites no story** → run unprofiled and **say so** in the pass. Artifacts + predating this rule are the common case; stopping on them would halt in-flight work. +2. The cited story has **no profile line** → same: today's behaviour. +3. A profile is **present but unresolvable** → **stop and surface the cause**. That covers + the syntactic failures — unparseable line, a value outside the enums, two profile + blocks, a citation resolving to nothing — **and the semantic ones**: a `**Validation:**` + value disagreeing with `max(risk, security)`, or `+abuse-path` present without security + `high` or absent with it. Only the **latest `mode override`** in the log, moving in a + direction compatible with the current value, can explain such a mismatch — and if the + log also contains an `axis change`, only when that override was recorded **after** the + latest one, since an axis change voids every prior override. A log with no `axis + change` at all is the ordinary intake-time override, and its entry resolves the + mismatch on its own. A well-formed value can still be the wrong value, and a stale mode steers + weaker evidence while looking entirely valid; recomputing it is a profile change like + any other — proposed, human-confirmed, logged. Falling back to the lighter behaviour on + a malformed profile would under-review exactly the stories most likely to have one. + +**The Gate-B triviality skip needs two independent conditions**, and an eligible profile +never makes a behaviour-changing diff skippable: the change itself is **behaviourally +trivial** (the pre-existing judgement, unchanged by profiles), **and** for a profiled story +`max(risk, security)` is 0 — risk `trivial` *and* security `none`, never risk alone. The +**skip reason is recorded in the commit body** — not in the profile log, which records +profile *changes*, and a skip changes no profile value. **A skip removes the review, never +the evidence**, and what is owed follows the profile: a skipped **profiled** story runs the +battery and lands its evidence entry beside the reason; a skipped **unprofiled** story +records the reason and the battery result and nothing more, because it owes no mode-derived +entry and keeps exactly today's judgement-based skip. + +**A cycle citing several stories** aggregates along separate dimensions, never through one +winning mode: the **battery runs once** for the cycle; **each cited _profiled_ story +satisfies its own mode and suffix**, with its own named evidence entry, while a cited +**unprofiled** story has no mode and owes no entry; the **lens sets are unioned** across +all cited stories; and the cycle is skip-eligible only if **every** cited story is. A +single "max" would either under-serve the strictest story or impose its obligations on +unrelated ones. + +**What the author owes before Gate B**, by mode: `battery` = the quality battery green · +`battery+check` = battery + **a check that fails without the change** · +`battery+check+verification` = battery + that check + a **named** verification of the risk +path · `+abuse-path` = one **named** abuse scenario plus evidence that the expected control +rejects or contains it. Level 2's two obligations are distinct; one artifact serves both +only if it demonstrates both. + +A check need not be an automated test — where none is possible, a **named verification** +satisfies it and the entry says which route was taken and why. Either route owes the +**counterfactual**: the observation against the prior state. An **unobservable +counterfactual is a blocking evidence gap**, not a free pass — stop and surface; the human +may then lower the mode as a logged override. A fabricated test satisfies nothing. + +**The evidence entry lives in the commit body** (see Mechanics), carries the **story path +and the named evidence but not the mode value**, and is **revalidated before every Gate-B +re-review and before the cycle-closing amend** — a fix changes the diff even when the +profile sits still. If revalidation changes the entry, the clean pass no longer covers what +is being committed: fix, re-review, close on the entry that pass validated. + +**Every Gate-B call and re-review carries the path of every cited story**, so the reviewer +reads each profile itself, **plus the current evidence entry, quoted verbatim, for each +cited *profiled* story** — an unprofiled one owes no mode-derived evidence, so it +contributes a path and nothing else. One profiled story means one pair; a cycle citing +several carries all of them, because the reviewer cannot union lenses it cannot see or +judge evidence it was never given. A reviewer handed neither can only review the diff — +the lenses and the evidence obligations would exist and never be consumed. + +**Two evidence gaps, two different answers.** Evidence that is *absent or inadequate* for +the mode is a **work gap**: produce it, then call. A project whose `AGENTS.md` names **no +verified quality command** cannot satisfy even `battery` — a **setup gap**: say what is +missing (`/workflow-init`'s battery step) rather than reviewing around it. Neither is a +reason to call Gate B against a weaker claim. + +**Changing a profile:** the pass **proposes the complete resulting header** — both axes, +the recomputed mode, any renewed override — and the **human confirms it**, in both +directions; an agent never moves it alone. On confirmation, correct the header and append +one profile-log line. Any axis change **voids every prior override**, raised or lowered, +and `+abuse-path` follows the current security value. Passes already run under the lower +profile **keep counting** toward the floor; only the **final clean pass** must run under +the current profile. Inside an active Gate-B cycle, fold the edit into the active `WIP:` +snapshot by amend — a non-`WIP` commit reads to the hook as the cycle closing and would +discard the accumulated passes. + +**What this does not do:** nothing checks which file a model actually read, whether the +header changed mid-call, or whether the lens sets were appended. This is instruction-backed +like the rest of §5; the detection is a reader comparing the pass against the story. +``` + +- [ ] **Step 2: Amend the "Finishing the cycle" bullet in `CLAUDE.md`** + +Find (line 266–267): + +```markdown + **Finishing the cycle:** after the final clean pass, close it with + `git commit --amend -m ""` — that replaces the WIP commit, and the hook +``` + +Insert a sentence at the end of that bullet's paragraph, after "…produced no fixes.": + +```markdown + **The closing message carries the validated evidence entry for every cited profiled + story** — one each, and none for a cited unprofiled story, which owes no entry. The + amend replaces the WIP message wholesale, so an entry written only into the WIP body is + destroyed exactly when the cycle closes. The final commit body is the durable record; + a PR shows commit messages, so there is no second home to keep in sync. +``` + +- [ ] **Step 3: Write the same Profiles subsection into the inline template** + +In `plugins/dev-workflow/commands/workflow-init.md`, insert the **same text as Step 1** +before its `### Mechanics (reference)` line. It contains no repo-specific paths, counts or +file references, so it transfers verbatim — confirm that by re-reading it before pasting; +if any sentence names this repo, neutralize it in **both** copies rather than letting them +diverge. + +- [ ] **Step 4: Add the same Mechanics sentence to the inline template** + +Apply Step 2's inserted sentence to the template's "Finishing the cycle" bullet (line +441–444), verbatim. + +- [ ] **Step 5: Verify the two copies agree — BOTH halves of the paired edit** + +Capture all four extractions, assert each is non-empty, then diff the pairs. The guard is +not optional bookkeeping: a missing anchor on **both** sides produces two empty captures +that diff clean, so an unguarded check passes while verifying nothing. + +```bash +P_REPO=$(sed -n '/^### Profiles — how much review this story gets$/,/^### Mechanics (reference)$/p' CLAUDE.md) +P_TMPL=$(sed -n '/^### Profiles — how much review this story gets$/,/^### Mechanics (reference)$/p' plugins/dev-workflow/commands/workflow-init.md) +M_REPO=$(sed -n '/\*\*The closing message carries the validated evidence entry/,/second home to keep in sync\./p' CLAUDE.md) +M_TMPL=$(sed -n '/\*\*The closing message carries the validated evidence entry/,/second home to keep in sync\./p' plugins/dev-workflow/commands/workflow-init.md) + +for v in P_REPO P_TMPL M_REPO M_TMPL; do + eval "[ -n \"\$$v\" ]" || { echo "EMPTY CAPTURE: $v — anchor missing"; exit 1; } +done +diff <(printf '%s\n' "$P_REPO") <(printf '%s\n' "$P_TMPL") && diff <(printf '%s\n' "$M_REPO") <(printf '%s\n' "$M_TMPL") && echo "BOTH HALVES AGREE" +``` + +Expected: `BOTH HALVES AGREE`, no diff output, no `EMPTY CAPTURE`. Any diff output is +drift — reconcile before committing. Both files changing is not evidence that they agree; +this check is. + +- [ ] **Step 6: Commit both files together** + +```bash +git add CLAUDE.md plugins/dev-workflow/commands/workflow-init.md +git commit -m "WIP: profiles subsection and evidence clause in both §5 copies" +``` + +--- + +### Task 3: Backlog — split P2+P6, re-point P5 light + +**Files:** +- Modify: `todos.md` — the `## Next` section, the P2+P6 row (lines 134–141) and the P5 + light row (lines 142–146) + +**Interfaces:** +- Consumes: nothing. Produces: nothing consumed by later tasks. + +- [ ] **Step 1: Replace the P2+P6 row with a split record** + +Replace the whole `- [ ] **P2 + P6 — risk/security profiles…**` row (lines 134–141) with: + +```markdown +- [x] **P2 — risk/security profiles, and the derived validation mode.** Shipped: two + human-confirmed axes in the story header, a mode derived as `max(risk, security)`, + lens sets appended to the §5 gate prompts, and the Gate-B skip narrowed to need both a + behaviourally trivial change and effective level 0. Spec: + `docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md`. +- [ ] **P6 — standalone security sections in the intake, spec and gate templates: + DELIBERATELY REJECTED, not shipped.** The profile *is* the heading: a standalone + section would be a second surface to keep in sync with it (the docs-drift class), + and it invites boilerplate-filling on stories where nobody knows what to write. + Security content lives in the spec's decision record and risks discussion and in + `AGENTS.md` invariants; the security lens set is what asks about assets, trust + boundaries, roles, external systems and abuse paths. *Reopens when:* field use shows + high-security content scattering incoherently across specs — that recurrence is the + trigger, not a fresh opinion. +``` + +- [ ] **Step 2: Re-point the P5-light trigger** + +In the P5 light row, replace `*Trigger: rides with P2*` and the sentence following it with: + +```markdown + *Trigger: the first story that runs under profiles* — the IDs exist to label what + profiles produce, so the numbering scheme should meet a real profiled story before + it gets a template slot. +``` + +- [ ] **Step 3: Verify no dangling reference to the old row** + +Run: `grep -n "P2 + P6\|rides with P2\|P2+P6" todos.md docs/*.md README.md` +Expected: no hits outside this plan and the spec's own prose. Fix any that remain. + +- [ ] **Step 4: Commit** + +```bash +git add todos.md +git commit -m "WIP: split P2+P6 in the backlog, re-point P5 light" +``` + +--- + +### Task 4: Documentation audit — the docs that teach the workflow + +**Files:** +- Modify: `docs/getting-started.md` — step 1, the intake paragraph (lines 11–17) +- Read and decide: `README.md` (the skill table row at line 21, the flow line at 83), + `docs/coding-workflow.md`, `docs/architecture.md`, `MANIFEST.md` + +**Interfaces:** +- Consumes: Task 1's header shape (quoted below). Produces: nothing. + +- [ ] **Step 1: Update the getting-started intake paragraph** + +In `docs/getting-started.md`, replace exactly this text — which begins mid-line at "The +`intake` skill" on line 12 and ends with "touches." on line 14: + +```markdown +The `intake` skill turns it into a story: +problem, outcome, ≥3 checkable acceptance criteria, which `AGENTS.md` invariants it +touches. +``` + +with: + +```markdown +The `intake` skill turns it into a story: +problem, outcome, ≥3 checkable acceptance criteria, which `AGENTS.md` invariants it +touches, and a **profile** — how risky this is (`trivial|standard|high`), how +security-relevant (`none|standard|high`), and the validation mode derived from the two. +It proposes all three with a reason; you confirm or correct them. The two axes decide +which extra questions the review gates ask; the derived mode decides what evidence you +owe before Gate B — a green battery, a check that fails without the change, or a named +verification on top. +``` + +The preceding sentence ("Say "users want to export their invoices as CSV" (or paste a +voice transcript — German is fine).") is **not** part of the replacement — starting the +new text mid-sentence would duplicate or corrupt it. + +- [ ] **Step 2: Audit the remaining four files and record the verdict** + +For each of `README.md`, `docs/coding-workflow.md`, `docs/architecture.md`, `MANIFEST.md`: +read the passages describing intake, the story artifact, or the gates, and either update +them or record in the commit message why the existing text stays accurate. Shipping +profiled prompts beside docs teaching the unprofiled workflow is the docs-drift class. + +Run: `grep -rn "story\b" README.md docs/coding-workflow.md docs/architecture.md MANIFEST.md | grep -viE "stories/|story file"` +Expected: a short list to read; most will be accurate as-is because they describe the +workflow shape rather than the story's fields. + +**Record each verdict in the plan itself**, as a line under this step — updated, or the +reason its text stays accurate. + +**Verdicts (recorded during execution):** +- `docs/getting-started.md` — **updated**: step 1 now names the profile and both halves + (axes → lenses, mode → evidence). +- `docs/coding-workflow.md` — **updated**: the intake paragraph names the profile, and + says axes *add* lenses while Gate A's floor is unchanged. +- `README.md` — **unchanged, accurate**: its intake row is a one-line capability summary, + not a field list, and its flow line (`idea → intake → brainstorm → …`) is unaffected — + profiles change what each stage asks, not the stages. +- `docs/architecture.md` — **unchanged, accurate**: it describes file layout and the two + non-obvious design decisions; no story-field claims appear in it. +- `MANIFEST.md` — **unchanged, accurate**: an inventory of the frozen `source-files/` + extraction seed, which this change does not touch. + +The WIP commit message is not the place: Task 5's soft reset discards every WIP message, +and a verdict that vanishes cannot tell a later reader "read and accurate" from "never +checked". These lines are carried into the final commit body in Task 5 Step 6. + +- [ ] **Step 3: Commit** + +```bash +git add docs/getting-started.md README.md docs/coding-workflow.md docs/architecture.md MANIFEST.md +git commit -m "WIP: docs audit for profiles" +``` + +--- + +### Task 5: Version bump, battery, Gate B, close the cycle + +**Files:** +- Modify: `plugins/dev-workflow/.claude-plugin/plugin.json` — `"version": "0.6.0"` → `"0.7.0"` +- Modify: `plugins/dev-workflow/CHANGELOG.md` — new top entry + +**Interfaces:** +- Consumes: every prior task's committed WIP state. + +- [ ] **Step 1: Bump the manifest version** + +`"version": "0.6.0"` → `"version": "0.7.0"`. Minor, not patch: this adds a capability to +the intake skill and the §5 rules. + +- [ ] **Step 2: Add the CHANGELOG entry** + +Insert as the newest entry, matching the file's existing style (no dates): + +```markdown +## 0.7.0 + +- `intake` proposes a **risk** and **security relevance** profile per story and derives a + **validation mode** from the two; all three are human-confirmed and recorded in the story + header, which is their single writable copy. +- §5 gains a **Profiles** subsection: lens sets appended per axis, the Gate-B triviality + skip narrowed for profiled stories to need both a behaviourally trivial change and + effective level 0, and the author's evidence + obligations per mode. The 3-pass floor and the findings protocol are unchanged, and no + new way to skip a gate is added. +- §5's Mechanics: the cycle-closing amend carries one validated evidence entry per cited + profiled story (and none for an unprofiled one), so the final commit + body is its durable record. +- Unprofiled stories behave exactly as before, including today's judgement-based skip. +``` + +- [ ] **Step 3: Snapshot FIRST — the version bump must be committed before the battery runs** + +`check-version-bump.sh` compares **commits**, not the working tree. HEAD already carries +Tasks 1–2's plugin edits, so running the battery with the bump still uncommitted fails the +check for a bump that exists on disk. Snapshot first, then measure — and note that +whole-path `git add` carries whatever else is in those files: if any listed path was +already dirty before this cycle, stage only this cycle's hunks or stop and ask, exactly as +the fix loop in Step 6 requires. The two `--cached` lines verify the result either way: + +```bash +git status --short # confirm nothing unrelated is dirty +git add plugins/dev-workflow/skills/intake/SKILL.md \ + plugins/dev-workflow/commands/workflow-init.md \ + plugins/dev-workflow/commands/process-pr-review.md \ + plugins/dev-workflow/.claude-plugin/plugin.json \ + plugins/dev-workflow/CHANGELOG.md \ + plugins/dev-workflow/hooks/codex-gate.sh \ + CLAUDE.md todos.md docs/getting-started.md docs/coding-workflow.md +git diff --cached --name-only # the staged set is exactly those paths +git diff --cached # and its content is only this cycle's work +git commit -m "WIP: profiles — snapshot for Gate B (0.7.0)" +git log --oneline -8 +``` + +Record two SHAs from that listing: `SNAPSHOT` (the commit just made — the state the +battery measures and Gate B reviews) and `BASE` (the commit **before** Task 1's first WIP +commit — Gate B's `baseSha`). + +- [ ] **Step 4: Run the full battery at `SNAPSHOT`** + +Run (the AGENTS.md quality row, verbatim and complete — the trailing +`claude plugin validate` included, since the closing evidence will claim it ran): + +```bash +shellcheck --shell=sh plugins/dev-workflow/hooks/codex-gate.sh && shellcheck --shell=sh --exclude=SC2015 plugins/dev-workflow/hooks/codex-gate.test.sh && shellcheck --shell=sh scripts/check-invariants.sh && shellcheck --shell=sh --exclude=SC2015 scripts/check-invariants.test.sh && shellcheck --shell=sh scripts/check-version-bump.sh && shellcheck --shell=sh scripts/check-version-bump.test.sh && sh plugins/dev-workflow/hooks/codex-gate.test.sh && sh scripts/check-invariants.test.sh && sh scripts/check-invariants.sh && sh scripts/check-version-bump.test.sh && sh scripts/check-version-bump.sh main && claude plugin validate . --strict +``` + +Expected: exit 0. `check-invariants.sh` scans the working tree, so move any untracked +scratch aside first if it trips (known, logged in `todos.md`). Every later fix invalidates +this result and requires a re-run — Step 6 says when. + +- [ ] **Step 5: Write the evidence entry into the WIP body, before the first Gate-B call** + +**This step applies to cited *profiled* stories only.** For each of them the spec makes the +evidence a **precondition** of the call and requires the call to quote the prepared entry +rather than compose its own. A cited **unprofiled** story — including this cycle's own +story, which predates the feature — owes no mode-derived entry: pass its path and stop +there, unless it is first adopted through §6's confirmed profile-change procedure. +Manufacturing an entry for it would impose exactly the obligation the compatibility +guarantee removes. + +Where an entry is owed, amend the snapshot so its body carries it — story **path** and +named evidence, and **no profile values**: + +```bash +git commit --amend -m "WIP: profiles — snapshot for Gate B (0.7.0) + +Story: docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md +Evidence: full battery green on the tree this commit records — shellcheck (6 files), +hook tests, check-invariants + suite, check-version-bump + suite, +claude plugin validate --strict; exit 0." +``` + +**The entry names no SHA, deliberately.** A commit body cannot name its own hash: every +amend produces a new one, so "green at ``" is stale the instant it is written. The +tested state *is* the tree the commit records, which stays true only because Step 6 forces +a re-run before every re-review and before the close. Amending keeps the message +`WIP:`-prefixed, so the cycle stays open and no counter resets. + +- [ ] **Step 6: Gate B, minimum 3 passes, file protocol** + +For each pass `p`: delete `.context/codex-reviews/gate-b-spec-pass-

.md` **and** +`gate-b-quality-pass-

.md`, confirm both are gone, then call `mcp__codex__review` with +`reviewType: full`, `baseSha: `, and an `additionalContext` that carries: the spec +path; **the path of every cited story**; **for each cited _profiled_ story, its exact +evidence entry from Step 5 quoted verbatim** — an unprofiled story contributes its path +only, and this cycle's story is unprofiled, so no entry is quoted for it; "report every +finding with severity and confidence; say `NO FINDINGS` if clean"; the one-line finding +format; and the file-first output protocol with **one path per branch**. Validate +each file (terminator, exact count, no extra lines, both branches present) before acting +on it. Fix Blocker/Major, re-review, repeat until a clean pass, writing +`-dispositions.md` per pass. + +**After every accepted fix, in this order** — the order is the point, because an +uncommitted fix sits outside `baseSha..HEAD`, where Gate B would return clean on the +pre-fix diff while the closing commit carried unreviewed changes: + +```bash +git add +git diff --cached --name-only # confirm the staged set is exactly those paths +git diff --cached # and that its CONTENT is only your fix +git commit --amend --no-edit # folds the fix into the active WIP snapshot, body intact + # the tree changed, so the previous result is void + # reviews the committed range, fixes included +``` + +`--amend --no-edit` preserves the evidence body; if a fix changes what the evidence claims, +rewrite the body with `--amend` and a full message instead of `--no-edit`. + +**Name the paths from what you edited, not from a computed delta.** You made the fix, so +you know its files; `git status` cannot tell your edit from pre-existing dirt on a path +that was already modified, and a recipe that claims otherwise is wrong exactly when the +worktree is dirty — the case it would exist for. The two `--cached` lines verify the staged +set rather than deriving it. + +**If a path you are about to stage was already dirty before this cycle**, whole-path +`git add` folds someone else's work into the reviewed range and the name-only check still +passes, because the path is one you meant to stage. Then: stage the fix's hunks only and +read `git diff --cached` before committing, or stop and ask. Whole-path staging is safe +only for a path that was clean when the cycle began. + +**This story's own profile:** read it fresh from the story header at +`docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md` — the plan +does not restate profile values, for the same reason nothing else does. That story predates +this feature and carries no profile line, so it is case 2 of §5: today's behaviour, no lens +sets, no mode-derived evidence. Running it profiled would mean adopting a profile through +§6's procedure first, human confirmation included. + +- [ ] **Step 7: Close the cycle with one real commit** + +The closing message must carry **the same evidence entry the final clean pass validated, +one per cited profiled story** (§5 Mechanics) — none for an unprofiled story, which is +this cycle's case — plus the docs-audit verdicts from Task 4, and **no profile values**, +since the story header is their single writable copy: + +```bash +git reset --soft +git commit -F - <<'EOF' +feat(intake,§5): risk, security, and validation profiles + +Two human-confirmed axes in the story header and a validation mode derived +from them; §5 appends lens sets per axis and narrows the Gate-B skip, which now needs +both a behaviourally trivial change and effective level 0. Unprofiled stories are unaffected. + +Story: docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md +Verification: full battery green on the tree this commit records — shellcheck (6 files), +hook tests, check-invariants + suite, check-version-bump + suite, +claude plugin validate --strict; exit 0. + +Docs audit: +EOF +``` + +**Why `Verification:` and not `Evidence:` here.** The cited story is unprofiled, so no +mode-derived evidence entry is owed and labelling this one would claim an obligation that +does not exist. Recording what was run stays worth doing — it is ordinary honest practice, +not a profile requirement. A cycle citing profiled stories writes one `Evidence:` entry per +profiled story instead, each validated by the final clean pass. + +The claim must be **re-verified immediately before this commit**, not inherited from the +last time it was run: the soft reset changes nothing about the tree, but any fix since the +last battery run would make the sentence false. + +- [ ] **Step 8: Open the PR** + +```bash +git push -u origin risk-security-validation-profiles +gh pr create --title "Risk, security, and validation profiles" --body "

" +``` + +--- + +## Self-Review + +**Spec coverage:** §3 axes → Task 1 Step 1; §3 header + log → Task 1 Steps 2–3; §4 modes, +counterfactual, evidence record, overrides, multi-story → Task 2 Step 1 and Step 2; §5 lens +sets, three read cases, skip rules → Task 2 Step 1; §6 change procedure, floor interaction, +mid-cycle amend → Task 2 Step 1; §7 surfaces → Tasks 3 (todos split), 4 (docs audit), 5 +(version + CHANGELOG); §7's paired-copy check → Task 2 Step 5; §8 invariants → Global +Constraints + Task 5 Step 3. + +**Placeholder scan:** the only `<…>` placeholders left are values that cannot be known +before execution — the base SHA in Task 5, the per-file audit verdicts in Task 4's commit +message, and the PR body. Each names exactly what to substitute. + +**Type consistency:** the field names `**Risk:**`, `**Security:**`, `**Validation:**`, the +mode names `battery` / `battery+check` / `battery+check+verification` / `+abuse-path`, the +event kinds `axis change` / `mode override` / `adoption`, and the level mapping +`none|trivial → 0`, `standard → 1`, `high → 2` are spelled identically in Task 1, Task 2, +Task 4 and Task 5's CHANGELOG. diff --git a/docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md b/docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md new file mode 100644 index 0000000..fc4b38a --- /dev/null +++ b/docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md @@ -0,0 +1,446 @@ +# Risk, security, and validation profiles — Design + +**Date:** 2026-07-26 · **Story:** +`docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md` + +Design altitude on purpose: this states WHAT each profile level changes, where the +values live, and how an upgrade is recorded. The exact wording of the lens paragraphs +and the template diffs are plan and implementation territory — prose review stops +converging on wording (nine plan passes in the last round re-proved it), and procedure +converges at Gate B. + +## 1. Problem and goal + +Review cost is the workflow's limiting factor, and every story pays the same price +regardless of what it risks (counted evidence in the story's §1: 51 Gate-A pass files +on one product project's 2 stories; 23 pass files and 160 findings in this repo's +just-closed round, 8 of them on prompt-text and docs alone). The gates have one +intensity and no vocabulary separating a payments migration from a copy tweak. + +Goal: two human-confirmed axes per story — **risk** and **security relevance** — plus a +**validation mode derived from them**, steering *what the gates ask* and *what the +author must show*, without adding passes and without introducing any new way to skip a +gate. + +## 2. Decisions, with what would reopen each + +| # | Decision | Why | Reopens when | +|---|---|---|---| +| D1 | The two axes are proposed by the agent and **confirmed by the human** before the story is written. A human *correction* may set any value, up or down; only a **non-answer** ("proceed anyway", "I don't know") is floored at the proposal. The validation mode is **derived** rather than asked, and the human may override it within the bounds in §4 | A third question invites an answer inconsistent with the first two; an overridable derived value keeps the human final. There is deliberately no "unconfirmed" state — a header the design calls human-owned must not be able to hold an agent's unanswered guess, and a floor on non-answers must not become a veto on answers | — | +| D2 | Profiles apply to stories **entering intake after this ships**; no retrofit | Retrofitting settled artifacts is a metadata backfill that changes no decision | An in-flight story may **adopt** a profile voluntarily at any natural checkpoint via the mechanism in §6. Adoption allowed, retrofit not required | +| D3 | `trivial` does **not** relax Gate A's floor and does not skip Gate A | A spec misjudged as trivial skips design review entirely and everything downstream inherits it — strictly more dangerous than a diff misjudged as trivial, which the documented Gate-B skip already covers | Field evidence that trivial stories burn Gate-A passes. Trigger discipline as usual: evidence, then a story | +| D4 | The **story header is the single writable copy**; spec and plan cite the story path; the gate prompt reads the header fresh at each pass | A second copy is a sync surface, and stale-copy drift is this repo's most recurrent defect class (§5 documents an incident) | — | +| D5 | The two **axes** govern the questions the reviewer asks (each maps to its own lens set); the **validation mode** governs the evidence the author must produce | Orthogonal levers — reviewer questions vs. author obligations. Had the mode also steered the gates it would be a second name for the axes. Saying "risk governs the questions" would be literally false: security maps its own lens set independently | — | +| D6 | Profiles **add no new gate-off path** and change no pass floor. The one skip that exists — §5's "Skip ONLY trivial changes" at Gate B — stays Gate-B-only and gains no new reach; for a **profiled** story its eligibility is *narrowed* to effective level 0 and its reason becomes recorded, while an unprofiled story keeps today's judgement-based skip | The mandatory-gates promise is the product. A lever that could switch a gate off is not an economics lever, and a skip whose justification is unrecorded is not a decision. Narrowing is the safe direction; saying "unchanged scope" would invite a later edit restoring risk-only skipping | — | + +Rejected, for the record: the mode as a **gate schedule** (full / reduced / minimal) — +it would let profiles switch gates off, the exact promise D6 protects; the mode as +**artifact depth** (full spec vs. design note vs. story-only) — "depth" is not checkable, +and prose review never converges on it; and a **spec-header echo** of the profile, +rejected under D4. + +## 3. The two axes + +Recorded in the story header, one line beside `Date` and `Size`: + +``` +**Risk:** trivial | standard | high · **Security:** none | standard | high · **Validation:** +``` + +Directly beneath it, a **profile log** that does not exist until something changes: the +`**Profile log:**` label is written on the first event and never before, so a story with +an untouched profile carries no empty block. Each entry is one line naming the **event +kind** — axis change, mode override, or adoption — with the **axis identity** on an axis +change (`risk`, `security`, or both in one entry when both move), a **direction per named +axis** (both may move at once and in opposite directions, so a single shared direction +could not say which belonged to which), and always a **reason or trigger**: a finding reference when a finding caused the event, and plain +prose when one did not, because intake overrides, adoptions and voluntary changes have no +finding and must not have to invent one. Never the values themselves. A mode override chosen during intake +confirmation is the first `mode override` event and creates the log, so the same decision +is equally auditable whether it happens at confirmation or a day later. The header is the only value-bearing copy (D4), and git +history already holds what the previous values were; a log that restated them would be +the second writable copy this design refuses. + +**Risk** answers *what breaks if this is wrong*, by observable criteria: + +- `trivial` — no behavioural effect **in the artifact's own execution context**. For + code, that is prose, comments, formatting, or a rename nothing resolves against. For a + **prompt artifact the text is the behaviour**, so a wording change to a skill, command, + agent definition, hook message or template is *not* trivial by default — a `trivial` + claim on one is a human decision, recorded like any other. +- `standard` — a behaviour change that hits no named trigger. The default. +- `high` — the change affects a named trigger. Two named lists, because domain and + consequence are different ways to be dangerous: + - **domains** — auth, permissions, payments, migrations, data deletion, public APIs, + personal data, supply chain; + - **effects** — irreversibility (no rollback path), data loss or corruption, outage + exposure (a critical path can stop serving). + + The effect list exists because a severe change need not sit in a named domain: a + migration-free job that truncates a table irreversibly is `high` on effect alone. It + stays a *named* list on purpose — replacing it with "use judgement about severity" + would reopen the door the named triggers exist to close. + +**Security relevance** answers *what an attacker could reach through this*: + +- `none` — touches no asset, trust boundary, role, or external system. A real answer and + the common one; it must not read as an admission of carelessness, or every story + drifts upward. +- `standard` — touches one of those without changing what it permits. +- `high` — changes a trust boundary, an authorization decision, or the handling of + secret or personal data. + +**Triggers match surfaces, not words.** A doc that mentions auth is not `high`; a change +to an authorization decision is `high` even if the word never appears in the story. The +question is what behaviour, data, or surface the change affects. + +**Where confirmation happens.** The profile proposal — all three values, one line of +reason each — rides **in intake's existing single question round**, alongside any +clarifying questions, and confirmation precedes the draft presentation. When nothing +needs clarifying, the proposal *is* the round. Intake gains no second pause. + +**Every recorded profile is human-confirmed**, because intake does not write the story +until the human answers — the existing one-round pause, unchanged. "Proceed anyway" or +"I don't know" **is** an answer: it accepts the proposal as it stands, and the values are +recorded as proposed and **never lower**. That is invariant 2 ("loose in the firing +direction") applied to intake — a redundant lens costs a paragraph, a missing one costs a +review — and it needs no "unconfirmed" state, which would have let an agent's guess sit +in a header the design calls human-owned. + +## 4. The derived validation mode + +Effective level = `max(risk, security)` over `none|trivial → 0`, `standard → 1`, +`high → 2`. The effective level names the mode; security `high` adds one obligation on +top of whichever mode applies. + +| effective | mode | the author must show, before Gate B | +|---|---|---| +| 0 | `battery` | the project's quality battery green | +| 1 | `battery+check` | battery + **a check that fails without the change** | +| 2 | `battery+check+verification` | battery + that check + a **named** verification of the risk path | +| security `high` | `+abuse-path` | added to the mode above | + +The middle level is named `+check`, not `+test`, because the obligation is a check that +fails without the change and an automated test is only its most common form (below). A +mode named `+test` that a manual verification can satisfy would overclaim in its own +title. + +**Field grammar.** `**Validation:**` carries exactly one mode name, followed by +`+abuse-path` **when and only when security is `high`**. Nothing else is a valid value, +and the suffix is not optional at that level — an obligation a header may omit is not an +obligation. + +`max()` is deliberate at the corner: a change judged `trivial` sitting on +security-relevant surface still lands at the security level's evidence. Trivial is a +statement about blast radius, not about where the code lives. + +**"A check that fails without the change" is not always a test**, and this repo is the +first case: its product is prompts, which no test can pin. Where an automated test is +possible it is the check. Where it is not, the obligation is met by a **named +verification** — and the entry says which route was taken and why. A fabricated test +never satisfies the mode; an honest "no automated check is possible here, verified by X" +does. + +**Either route owes the counterfactual.** The obligation is not "the behaviour works" +but "this change is what makes it work", so the entry records the observation against +the **prior state**: the test failing before the change, the command's output on the +unchanged tree, the manual check performed on both. Evidence that only shows the end +state proves the feature, not the change. + +**An unobservable counterfactual is a blocking evidence gap**, not a free pass. If the +prior state genuinely cannot be observed, the author stops and surfaces it; the human may +then lower the mode, which is an override — explicit, bounded by §4, and logged. What is +not available is claiming `battery+check` while showing no check that fails without the +change, because the mode's name would then describe evidence nobody produced. + +**"Named" is the honest-claim rule made checkable.** A mode-satisfying entry names the +concrete check and its recorded result: a test path, a reproducible command with its +output, or a documented manual check with its outcome. The word "verified" without a +named artifact satisfies nothing. + +**One durable evidence record: the commit body.** Gate B runs *before* the real commit, +so a PR body cannot hold the evidence the call itself must quote, and the `WIP:` body +cannot either — §5's cycle-closing `git commit --amend -m ""` replaces the +message wholesale. So §5's Mechanics gains **one clause**: the cycle-closing amend's +message **includes the evidence entry**, making the final commit body the authoritative +record. It is written before the call as the WIP body, it survives the close by being +re-stated in the real message, and it needs no PR — a PR shows commit messages anyway, so +no second home is specified. The Gate-B call quotes that entry rather than composing its +own version. No new file, no new story section. + +That clause is in scope rather than adjacent: this story already edits §5 in both copies, +and an evidence obligation with no durable home would be an unverified-enforcement claim +built in by design. + +**The entry carries the story citation and the named evidence — not the mode value.** +Restating the mode would put a profile value in a second writable place, which is what +D4 refuses; a reviewer handed a quoted mode would also assess the evidence against +whatever the entry claimed rather than what the header now says. Gate B derives the +obligations fresh from the cited header, the same way the lens sets are derived. + +The entry is **revalidated before every Gate-B re-review and before the cycle-closing +amend** — not only when the profile moves, which is what made naming the mode look +necessary in the first place. A +Gate-B fix changes the diff, which can invalidate the check, its result, or both while +the profile sits still; §5 already re-reviews after every fix for exactly that reason. +Same shape as the floor rule in §6: the last review covers the current state, and so must +the evidence it reads. + +**The close restates the entry the final pass saw.** If the pre-amend revalidation +changes the entry at all, the clean pass no longer covers what is being committed, and it +is invalidated: fix, re-review, and close on the entry that pass validated. Otherwise the +durable record and the review that blessed it would describe different work. + +**Producing the evidence is a precondition of the Gate-B call.** If the mode's evidence +cannot be produced, the author stops and surfaces the reason, exactly as an incomplete +pass is surfaced; the call is not made against a weaker claim. Two named failure states, +because they need different responses: evidence that is *absent or inadequate* is a work +gap → produce it; a project whose `AGENTS.md` has **no verified quality command** cannot +satisfy even mode `battery` → that is a setup gap, and the response is to say what is +missing (`/workflow-init`'s battery step), not to review around it. + +**Overriding the derived mode, within bounds.** The derivation is a recommendation, per +D1: the human may **raise** the mode freely. **Lowering** it requires the human to say so +explicitly — an agent never lowers it — and cannot drop `+abuse-path` while security is +`high`; that suffix is the one obligation the axes make non-negotiable, because a story +that reaches an abuse path is exactly the story whose evidence nobody should be able to +trade away quietly. The header then carries the **effective** mode, and the profile log +records it as a `mode override` event (direction, reason), so a lowered mode is a +decision a later reader can find and question. + +**An axis change voids every prior override**, raised or lowered. When either axis moves +(§6) the mode is recomputed from the new values, any override lapses, and holding the +mode away from its new derivation requires the human to say so again, logged again, +before the next pass. The `+abuse-path` suffix follows the current security value by +grammar, so a security downgrade removes it rather than leaving an obligation nobody +reconfirmed — and an upgrade cannot leave yesterday's lowered mode standing, which would +have made the promised revalidation change nothing. + +**A cycle covering several stories** aggregates along separate dimensions rather than +through one winning mode: the **battery runs once** for the cycle, **each cited *profiled* +story satisfies its own mode and suffix** with its own named evidence entry — a cited +unprofiled story has no mode and owes no entry, contributing only its path — and the +**lens sets are unioned** across all cited stories. A single "max" would either under-serve the +high-security story or impose its obligations on unrelated ones. + +**Level 2's two obligations are distinct.** `battery+check+verification` owes the +counterfactual check *and* a named verification of the risk path. One artifact may serve +both only if it actually demonstrates both — the failure on the prior state and the risk +path exercised; otherwise they are two entries. An author and a reviewer reaching +opposite readings of this is a Gate-B stop nobody needs. + +**What `+abuse-path` minimally shows:** one **named** abuse scenario for the surface the +change touches, and evidence that the expected control rejects or contains it. Naming +the scenario without showing the control's response is half the obligation. + +**Forward-compatible by design.** Today `named verification` and `+abuse-path` resolve to +whatever a project's battery and manual practice provide. When a project grows the +parked validation lane (agentic smoke, coded E2E), those become additional ways to +satisfy the *same* obligations — no renaming, no second axis. That lane is not designed +here. + +## 5. What the profile changes at the gates + +§5 gains one short **Profiles** subsection — the axes, the derivation, and the two lens +sets named — in both copies (this repo's `CLAUDE.md` and the inline template +`/workflow-init` writes). The gate-prompt rule gains one instruction: **read the cited +story's header fresh at each pass, and append the lens set(s) the current values call +for.** + +- **risk `high`** → risk lens set: threats, abuse, rollback, data loss, idempotency, + compatibility, observability. +- **security `standard` or `high`** → security lens set: assets, trust boundaries, + roles, external systems, abuse paths. +- **both** → the union, appended **once**, each lens labelled with the axis that + motivated it. Duplicated questions would spend exactly the budget this design exists + to save. The one genuine overlap — risk's *abuse* and security's *abuse paths* — is a + **single lens carrying both labels**, not two questions; leaving that undefined is how + the two §5 copies would drift into asking it twice or dropping one axis's version. +- **risk `trivial`** → nothing appended. It supplies *one part* of a profiled story's + eligibility for §5's pre-existing Gate-B skip — never the justification on its own, and + never a skip of its own. + +**The skip needs two independent conditions**, and the profile supplies only one: the +change is **behaviourally trivial**, *and* for a profiled story §5's pre-existing Gate-B +skip becomes available only when `max(risk, security)` is 0 — risk `trivial` *and* +security `none`. The **skip reason is recorded in the commit body**, beside the evidence +entry where one is owed. Not in the profile log: that log records *profile changes*, and a skip changes no +profile value; it is a per-cycle decision belonging with the other per-cycle record. +(Corrected during Gate B pass 2, which found the earlier "goes in the profile log" wording +irreconcilable with the log's three event kinds.) A `trivial` risk call on security-relevant surface derives real evidence +obligations, `+abuse-path` among them at security `high`, and a skip keyed on risk alone +would let a single judgement call throw away the one obligation §4 says cannot be traded +away. + +**A skip removes the review, never the evidence.** A skipped **profiled** story is still +mode `battery` by derivation, so the battery runs and its evidence entry lands in the +commit body before the commit. A skipped **unprofiled** story owes no mode-derived entry +(§7): it records its skip reason and the battery result, and nothing more — manufacturing +an entry for it would impose the obligation the compatibility guarantee removes. Otherwise the profile would quietly become an evidence-off path +while advertising that it is not a gate-off path — the same claim D6 refuses. + +**In a cycle citing several stories, every cited story must be skip-eligible** for the +cycle to skip: each profiled one at effective level 0, each unprofiled one trivial by +today's judgement. One light story does not carry the others. + +**How Gate B locates the story.** Gate B reviews a diff, not an artifact with a header, +so the story path is an **input to the call** — cited in the call's own context, and in +the commit body beside the evidence entry. That is a path, not a profile copy: the values +are still read from the story at each pass. + +**When the profile cannot be read.** Three situations, deliberately separated, because +they need different answers: + +1. **No story is cited** → run unprofiled, and say so in the pass: today's behaviour, + announced rather than assumed. Artifacts that predate the citation rule are the common + case, and stopping on them would halt in-flight work that D2 promises not to disturb. +2. The cited story exists and has **no profile line** → same: a pre-profile story gets + today's behaviour. +3. A profile is **present but unresolvable** → stop and surface the cause. That covers + the syntactic failures — unparseable line, a value outside the enums, two profile + blocks, a citation that resolves to nothing — **and the semantic ones**: a + `**Validation:**` value that disagrees with `max(risk, security)`, or a + `+abuse-path` suffix present without security `high` or absent with it, unless the + profile log explains it — and only the **latest `mode override`**, moving in a + direction compatible with the current value, can. If the log also holds an `axis + change`, that override must postdate the latest one, since an axis change voids every + prior override (§4); a log with no axis change at all is the ordinary intake-time + override, which resolves the mismatch by itself. + A well-formed value + can still be the wrong value, and a stale mode steers weaker evidence while looking + entirely valid. Recomputing it is a profile change like any other: proposed, human + confirmed, logged. Falling back to the lighter behaviour on a *malformed* profile + would under-review exactly the stories most likely to have one. + +**"Today's behaviour" means today's**, including the judgement-based Gate-B trivial skip +§5 already permits. Unprofiled stories get neither the lens sets nor the effective-level +skip rule, and they get no stricter than they are now — a rollout that quietly tightened +the gates for every in-flight story would break D2 and D6 at once. + +Lenses are **different questions, not more identical passes**. The 3-pass floor, the +Blocker/Major filter, the file-first findings protocol and the clean-final-pass rule are +all unchanged, as is the coverage rule: Codex reports every finding with severity and +confidence, and the filtering stays downstream. + +## 6. Changing a profile after intake + +Any pass may reveal the profile was set too low. Then: + +1. The pass **proposes the complete resulting header** — both axes, the recomputed mode, + and any override the human wants to renew — and the **human confirms it**, in both + directions. The header is human-owned per D1, and an agent that could raise an axis by + itself would make that claim false; "raise now, confirm later" is just the + `unconfirmed` state under another name. Confirming the whole header rather than the + moved axis alone matters because the mode is *computed*: confirming only the axis would + leave the one value the machine chose unconfirmed. Upgrades are rare and this workflow + already has the human in the loop, so the cost is a question, not a stall. +2. On confirmation the header is **corrected**, and one line is **appended to the profile + log**: date · event kind · axis identity (on an axis change) · direction · reason or + trigger · what it changes downstream. The line does not restate values; the + header carries those. +3. The record is written **before the next pass begins**. That, not the commit, is what + closes the window in which a pass could run against a value nobody has written down. +4. **How it is committed depends on where the cycle is.** Outside an active Gate-B cycle + — during Gate A, or between cycles — it is a docs-only commit at the next natural + point. **Inside** an active Gate-B cycle it is folded into the active `WIP:` snapshot + by amend, because a non-`WIP` commit reads to the hook as the cycle closing: it would + discard the accumulated passes and move the reviewed range out from under `baseSha`. + Either way the profile does not sit uncommitted on one machine's disk while the gates + consume it. +5. The new profile takes effect at the next pass, because the prompt re-reads the header + rather than reusing a value quoted earlier in the session. + +**Adoption** by a story that never had a profile is the same mechanism — a header line +added plus one log entry naming the adoption and its reason — and it needs the same human +confirmation as an intake proposal, because an adopted profile nobody confirmed is a +guess wearing a header line. + +**Upgrade × the 3-pass floor.** Passes already run under the lower profile **keep +counting** toward the floor — the floor is per cycle, and re-running them would be the +more-identical-passes this design exists to avoid — but the **final clean pass must run +under the current profile**. Net effect: an upgrade costs at minimum one additional +pass, never a restart of the cycle. It is the Gate-B rule "the last review must cover +the current state", applied to profiles. + +**What enforces that is instruction, not machinery.** The fresh read is a prompt rule; +nothing checks which file the model actually read, whether the header changed during a +call, or whether the lens sets were appended. The detection that does exist is the +ordinary one — a reader comparing the pass against the story. Stating this is not a +caveat for its own sake: a design that claimed the fresh read *guarantees* the final +pass ran under the current profile would be this repo's most-logged defect class, +written into the spec that is supposed to prevent it. + +## 7. Compatibility and scope + +A story with **no profile line behaves exactly as today** (§5): standard intensity, no +lens sets appended, no effective-level eligibility rule — and today's judgement-based +Gate-B triviality skip still available to it, because "exactly as today" has to include +the parts that favour the author. It also owes **no mode-derived evidence +entry**: the validation modes of §4 apply to profiled stories only, and imposing them on +in-flight work would be the same tightening in a different place. What a skipped +unprofiled cycle *does* record in the commit body is what every skipped cycle records — +its skip reason and the battery result — which is today's practice, not a new obligation. That is what makes D2 free — nothing in flight breaks, +and adoption is a header line plus one log entry through §6 whenever a story wants it. + +**How spec and plan cite the story.** Both carry a `**Story:**` header line with the +story's path, and the gate prompt resolves the profile through it. The surface that +states this rule is §5's Profiles subsection, because `superpowers:brainstorming` and +`superpowers:writing-plans` are upstream skills this repo depends on and does not edit +(AGENTS.md, dependency direction). A spec or plan that cites no story is case 1 of §5: the +pass runs unprofiled and **says so**, rather than either stopping or falling back +silently. + +**Where security content lives** for security relevance `standard` or `high`: in the +spec's **decision record** and **risks** discussion, and in the project's **AGENTS.md +invariants** — the places that already exist. The security lens set is what *asks* about +assets, trust boundaries, roles, external systems and abuse paths; a spec that has not +covered them draws findings, which is a review outcome, not enforcement. No template +grows a standalone security section (non-goal, below). + +**Surfaces touched:** `plugins/dev-workflow/commands/process-pr-review.md` (its Gate-B +skip needs **both** conditions — the change is behaviourally trivial, judged by effect +rather than line count, **and** every cited story is eligible — where before it turned on +the fix's size alone), +`plugins/dev-workflow/skills/intake/SKILL.md` (proposal step, +header line, profile log in the story template), `CLAUDE.md` §5 and the inline §5 +template in `plugins/dev-workflow/commands/workflow-init.md` (both in the same commit — +docs-drift class, and including §5's Mechanics clause on the cycle-closing amend), +`todos.md` — where **P2+P6 is split rather than closed**: the profile and lens work +closes, while P6's promised "security sections in the intake, spec and gate templates" +is recorded as *deliberately rejected*, with the reason, because closing the row whole +would claim scope that never shipped. P5 light's trigger is re-pointed at the first story +that runs under profiles. Plus CHANGELOG + plugin version bump (invariant 12), and a +**reference audit of the user-facing workflow docs** — `README.md`, +`docs/coding-workflow.md`, `docs/getting-started.md` at minimum — which teach intake, +the story artifact and the gates: each affected site is updated, or its continued +accuracy is recorded. Shipping profiled prompts beside docs that describe the unprofiled +workflow is the docs-drift class this spec keeps naming. + +**Carried to the plan, not designed here:** the plan must include an explicit paired +check that the two §5 copies say the same thing after the edit — both files changing is +not evidence that they agree. The method is the plan's to choose; inventing one in prose +here is the altitude mistake this spec opens by naming. + +**Non-goals:** no hook change beyond one advisory reminder string (waived by the human +during Gate B pass 11: the hook's below-floor reminder asserted a skip rule this design +makes false, and the edit *removes* that statement in favour of citing the policy, so the +hook still counts passes and reads no profile); no new script; no new scaffolded file; no standalone security section in any +template. If field use later shows high-security content scattering incoherently across +specs, that recurrence is the trigger for a dedicated section. Stable AC-/SEC-IDs (P5 +light) are deferred by the same reasoning that dated their trigger: profiles are what +the IDs would number. + +## 8. Invariants this change touches + +- **8** — the §5 template stays inline in the command body; the Profiles subsection is + written there, not read from disk. +- **9** — `/workflow-init` stays idempotent; a template that changed means show-the-diff + and ask, never a silent overwrite. +- **10** — the trigger list, the level criteria and the lens sets stay stack-neutral; + project vocabulary belongs in that project's own files. +- **11** — every touched prompt passes all 12 items of `docs/prompt-standards.md`. +- **12** — the plugin change carries a version bump. +- **Don'ts** — no doc section renamed or deleted without grepping references first; no + sentence claiming what a gate proves beyond what it actually compares. The profile + changes the *questions* a gate asks and the *evidence* an author owes; it changes + nothing about what either gate compares. diff --git a/docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md b/docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md new file mode 100644 index 0000000..9b11ee2 --- /dev/null +++ b/docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md @@ -0,0 +1,112 @@ +# Risk, security, and validation profiles for the workflow — Story + +**Date:** 2026-07-26 · **Size:** story + +## 1. Problem statement + +Review cost is the limiting factor of this workflow, and every story pays the same +price regardless of what it risks. Counted from the review artifacts: +infinite-portfolio-canvas spent 51 Gate-A pass files across 2 stories (spec 14, plan +14, replan 4, amend 12, a3-spec 7); this repo's just-closed canvas field-findings round +spent 23 Gate-A pass files and 160 findings across its two PRs — 6 on the combined +plan, then 9 and 8 after the 2-PR split — of which the second PR's 8 passes and 45 +findings went on prompt-text and docs changes alone. The gates have one intensity and +no vocabulary separating "this touches payments" from "this fixes a typo", so a trivial +change is reviewed like a migration, and a high-risk change is reviewed with the same +generic questions as a trivial one. Nothing records what a story risks, so CLAUDE.md +§5's "Skip ONLY trivial changes" rests on unrecorded judgement. + +## 2. Desired outcome + +Every story carries two human-confirmed profile axes — risk and security relevance — +plus a validation mode derived from them, and the gates spend effort in proportion: +high risk asks *different* review questions (threats, abuse, rollback, data loss, +idempotency, compatibility, observability), while effective level 0 — risk `trivial` +*and* security `none` — legitimizes the documented Gate-B skip with a recorded reason, +for a change that is itself behaviourally trivial. Different questions, not more identical passes. Why: profiles +are the economics lever on the workflow's dominant cost, and the precondition for the +batch/orchestrator mode parked in todos — that mode cannot decide what to batch without +knowing what each item risks. + +## 3. Acceptance criteria + +- [ ] Intake proposes both axes with their levels (risk: trivial | standard | high; + security relevance: none | standard | high) and the human confirms or corrects + them before the story is written. +- [ ] Risk `high` is reachable by named triggers, not by unaided judgement: named + **domains** — auth, permissions, payments, migrations, data deletion, public APIs, + personal data, supply chain — and named **effects** — irreversibility, data loss or + corruption, outage exposure. + *(Amended 2026-07-27 from PR-review finding: the effect triggers were added during + Gate A spec pass 3, because a severe change need not sit in a named domain, and the + criterion still named only the domains.)* +- [ ] The validation mode is derived from the two axes and presented as a + recommendation the human can override; it is never asked as a third question. +- [ ] All three values are recorded in one fixed location that survives spec revisions, + so a later reader and a later gate read the same profile. +- [ ] A mid-story finding revealing higher risk than the intake profile assumed can + upgrade the profile, and the upgrade is recorded — silently continuing under a + stale profile is not an available outcome. +- [ ] The §5 gate prompts consume the profile: at risk `high` they carry the additional + lenses; the documented Gate-B skip needs **both** a behaviourally trivial change + *and* effective level 0 for every cited profiled story — risk `trivial` *and* + security `none` — with its reason and the battery result recorded. Each cited + profiled story also owes one evidence entry; an unprofiled story owes none and keeps + the prior judgement-based skip. + *(Amended 2026-07-27 from PR-review finding: the criterion keyed the skip on risk + alone, which Gate A spec pass 2 corrected to the effective level so a trivial-risk + change on security-relevant surface cannot skip away its obligations.)* +- [ ] For security relevance standard | high, the story states *where* security content + lives (the spec's existing sections and AGENTS.md invariants) — no new standalone + security section is introduced anywhere. +- [ ] Whatever the profile changes about CLAUDE.md §5, the 3-pass floor included, + changes in both the `/workflow-init` template and this repo's own CLAUDE.md in the + same commit. +- [ ] `todos.md` reflects what shipped: the P2+P6 row is **split** — the profile and lens + work closes, while P6's promised "security sections in the intake, spec and gate + templates" is recorded as deliberately rejected with its reason — and P5 light's + trigger is re-pointed at "the first story that runs under profiles". + *(Amended 2026-07-26 during Gate A, spec pass 6: the original criterion said "P2+P6 + closed", which would claim scope that was deliberately not built.)* +- [ ] Scope holds: no new script is added, and nothing under + `plugins/dev-workflow/hooks/` changes except one advisory reminder string, waived + by the human during Gate B pass 11 — the hook still reads no profile and gains no + machinery. + *(Amended 2026-07-27 from PR-review finding: the waiver was recorded in the plan's + decision record and the CHANGELOG, but this criterion and the spec's non-goal still + asserted the unwaived scope.)* + +## 4. Affected AGENTS.md invariants + +- `## Prompts and scaffolding` — "8. **`/workflow-init`'s templates stay inline** in the + command body." +- `## Prompts and scaffolding` — "9. **`/workflow-init` never overwrites silently.** + Idempotent: missing → write; identical → report unchanged; present and different → + show the diff and ask; additive files … → merge." +- `## Prompts and scaffolding` — "10. **The base taxonomy stays stack-neutral.** Project + vocabulary — tables, auth helpers, framework APIs — goes only in that project's + `docs/hardening-taxonomy.md`". +- `## Prompts and scaffolding` — "11. **Prompt changes pass + `docs/prompt-standards.md`** — all 12 checklist items, for any skill, command, agent + definition, hook message, or scaffolded template." +- `## Packaging` — "12. **A plugin change requires a version bump.**" +- `## Don'ts` — "**Never rename or delete a doc section without grepping for references + first.**" +- `## Don'ts` — "**Never describe what a gate proves without checking what it actually + compares.**" + +## 5. Open questions + +- Do profiles apply to stories already in flight (canvas has two on a long-lived + branch), or only to stories entering intake after this ships? +- Does `trivial` relax Gate A as well, or only the Gate-B triviality skip that §5 + documents today? +- Deferred by decision, not open: a dedicated spec security section is *not* built now + — if field use shows high-security content scattering incoherently across spec + sections, that recurrence is its trigger. + +## 6. Suggested size + +story — one coherent decision (two axes + derived mode + lens paragraphs) that fits one +spec → plan → PR, even though it touches the intake skill, the `/workflow-init` +templates, §5 in both copies, and docs. diff --git a/plugins/dev-workflow/.claude-plugin/plugin.json b/plugins/dev-workflow/.claude-plugin/plugin.json index eeacb31..4515c49 100644 --- a/plugins/dev-workflow/.claude-plugin/plugin.json +++ b/plugins/dev-workflow/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "dev-workflow", "displayName": "Cross-Model Review Workflow", - "version": "0.6.0", + "version": "0.7.0", "description": "Spec-driven workflow with two independent cross-model review gates, an append-only hardening ledger with an escalation ladder, and repo-enforced quality. Requires the superpowers plugin.", "author": { "name": "Daniel Sänger", diff --git a/plugins/dev-workflow/CHANGELOG.md b/plugins/dev-workflow/CHANGELOG.md index c583731..a8d4539 100644 --- a/plugins/dev-workflow/CHANGELOG.md +++ b/plugins/dev-workflow/CHANGELOG.md @@ -22,6 +22,39 @@ unambiguously, still fails. Deleting only a plugin's *manifest* while the direct keeps shipping fails too. AGENTS.md invariant 12 carries the complete list. +## 0.7.0 + +- **`intake` proposes a risk and security-relevance profile per story** and derives a + **validation mode** from the two (`max(risk, security)` → `battery` / `battery+check` / + `battery+check+verification`, plus `+abuse-path` at security `high`). All three are + human-confirmed inside intake's existing single question round — there is no + "unconfirmed" state — and the story header is their single writable copy. A + `**Profile log:**` records later changes without restating values. +- **§5 gains a Profiles subsection**: lens sets appended by level — risk `high` → threats, + abuse, rollback, data loss, idempotency, compatibility, observability; security + `standard` or `high` → assets, trust boundaries, roles, external systems, abuse paths + (risk `standard` and `trivial` append nothing) — three defined answers for + reading a profile, and the author's evidence obligations per mode. Lenses are different + questions, not more passes: the 3-pass floor, the Blocker/Major filter and the + file-first findings protocol are unchanged. +- **The Gate-B skip narrows** for a profiled story: it now needs **both** a behaviourally + trivial change **and** effective level 0, where the profile supplies only the second. + It removes the review but never the evidence. No new way to skip a gate is added. +- **§5 Mechanics: the cycle-closing amend carries one validated evidence entry per cited + profiled story** (and none for an unprofiled one), so the final + commit body is its durable record — an entry written only into the `WIP:` body is + destroyed by the amend that closes the cycle. +- **Unprofiled stories are unaffected**, including today's judgement-based skip: a story + with no profile line behaves exactly as it did before this release. +- **`process-pr-review` requires both conditions for a Gate-B skip** — the change is + behaviourally trivial (judged by effect, never by line count) **and** every cited story + is eligible — where it previously turned on the fix's size alone. Explicit no-story, + one-story and several-stories branches. +- **The hook's below-floor reminder stops restating the skip rule** and defers to the + policy file instead — one string, no new hook behaviour. The hook still only counts + passes and reads no profile; the edit removes a rule statement that the narrowed skip + made false, rather than teaching the hook anything about profiles. + ## 0.6.0 - **Optional companion files beside a gate findings file.** The §5 protocol knew only diff --git a/plugins/dev-workflow/commands/process-pr-review.md b/plugins/dev-workflow/commands/process-pr-review.md index 87b2fb4..ced1a7b 100644 --- a/plugins/dev-workflow/commands/process-pr-review.md +++ b/plugins/dev-workflow/commands/process-pr-review.md @@ -138,10 +138,28 @@ be ambiguous, which is not an instruction. a decision already made, and to item 5 would let `harden-finding` change the repository for something just ruled out of this PR. -3. Implement accepted **and** actionable findings. Severity gate per CLAUDE.md §5: a - trivial fix (one-liner, comment, naming) → commit with a documented Gate-B triviality - skip in the commit message; a substantial fix (logic, new/changed paths) → run Gate B - (`mcp__codex__review` on the new diff) before committing. +3. Implement accepted **and** actionable findings. Severity gate per CLAUDE.md §5. **Every + substantial fix (logic, new or changed paths) runs Gate B** (`mcp__codex__review` on the + new diff) before committing — no profile makes a substantial fix skippable. A skip needs + **both** conditions, never either alone: + - the **fix is trivial** — judged by **behavioural effect, not line count**: a comment, + a doc typo, a rename nothing resolves against. A one-line change that alters + behaviour is not trivial, and in a prompt product the text *is* behaviour; **and** + - **every cited story is eligible.** Resolve the cited story path(s) first, looking in + **both** the PR body and the **commit bodies in the range** — the workflow puts the + story path in the closing commit message, so a PR that cites nothing in its + description may still be profiled. Only when neither carries a citation is this §5's + "no story cited" case, which takes the unprofiled judgement call. A profiled story is + eligible only at effective level 0 (risk `trivial` *and* security `none`); an + unprofiled story is eligible on the old judgement call. With several cited stories, each must be + eligible on its own; one eligible story does not carry the rest. A cited profile that + is **present but unresolvable** (§5's third case) stops the run — surface the cause; + it is never treated as unprofiled. + + A skip removes the review and never the evidence. Every skipped cycle runs the battery + and records, in the commit body, **the skip reason and the battery result**. On top of + that: one mode-derived evidence entry per cited **profiled** story, and none for an + unprofiled one — which owes the reason and battery result and nothing further. 4. Stop and ask the user for: every `escalate-to-user` verdict, and every accepted finding that contradicts a settled decision. Do not implement these. A finding already recorded as out of scope by item 2 does **not** come here — it is terminal diff --git a/plugins/dev-workflow/commands/workflow-init.md b/plugins/dev-workflow/commands/workflow-init.md index b71c272..497d1d5 100644 --- a/plugins/dev-workflow/commands/workflow-init.md +++ b/plugins/dev-workflow/commands/workflow-init.md @@ -426,6 +426,114 @@ Add `/.context/codex-reviews/` to `.gitignore` — that entry specifically, not (`docs/commands/reference.md`) fires too — a redundant reminder, never a missed review. +### Profiles — how much review this story gets + +A story may carry a profile in its header: `**Risk:**` (`trivial|standard|high`), +`**Security:**` (`none|standard|high`), and a `**Validation:**` mode derived from them +(`battery` / `battery+check` / `battery+check+verification`, plus `+abuse-path` when and +only when security is `high`). The **story header is the single writable copy** — specs, +plans, commit bodies and this file's prompts carry the story **path** and read the values +fresh at each pass, never a remembered or copied value. + +**The axes steer the questions; the mode steers the evidence.** Separate levers: one aims +the reviewer, the other obliges the author. + +**Lens sets, appended to the gate prompt:** +- **risk `high`** → threats, abuse, rollback, data loss, idempotency, compatibility, + observability. +- **security `standard` or `high`** → assets, trust boundaries, roles, external systems, + abuse paths. +- **both** → the union appended **once**, each lens labelled with the axis that motivated + it; risk's *abuse* and security's *abuse paths* are **one lens carrying both labels**, + not two questions. + +Lenses are **different questions, not more passes.** The 3-pass floor, the Blocker/Major +filter, the file-first findings protocol and the clean-final-pass rule are unchanged. + +**Reading the profile — three cases, three answers:** +1. The artifact **cites no story** → run unprofiled and **say so** in the pass. Artifacts + predating this rule are the common case; stopping on them would halt in-flight work. +2. The cited story has **no profile line** → same: today's behaviour. +3. A profile is **present but unresolvable** → **stop and surface the cause**. That covers + the syntactic failures — unparseable line, a value outside the enums, two profile + blocks, a citation resolving to nothing — **and the semantic ones**: a `**Validation:**` + value disagreeing with `max(risk, security)`, or `+abuse-path` present without security + `high` or absent with it. Only the **latest `mode override`** in the log, moving in a + direction compatible with the current value, can explain such a mismatch — and if the + log also contains an `axis change`, only when that override was recorded **after** the + latest one, since an axis change voids every prior override. A log with no `axis + change` at all is the ordinary intake-time override, and its entry resolves the + mismatch on its own. A well-formed value can still be the wrong value, and a stale mode steers + weaker evidence while looking entirely valid; recomputing it is a profile change like + any other — proposed, human-confirmed, logged. Falling back to the lighter behaviour on + a malformed profile would under-review exactly the stories most likely to have one. + +**The Gate-B triviality skip needs two independent conditions**, and an eligible profile +never makes a behaviour-changing diff skippable: the change itself is **behaviourally +trivial** (the pre-existing judgement, unchanged by profiles), **and** for a profiled story +`max(risk, security)` is 0 — risk `trivial` *and* security `none`, never risk alone. The +**skip reason is recorded in the commit body** — not in the profile log, which records +profile *changes*, and a skip changes no profile value. **A skip removes the review, never +the evidence**, and what is owed follows the profile: a skipped **profiled** story runs the +battery and lands its evidence entry beside the reason; a skipped **unprofiled** story +records the reason and the battery result and nothing more, because it owes no mode-derived +entry and keeps exactly today's judgement-based skip. + +**A cycle citing several stories** aggregates along separate dimensions, never through one +winning mode: the **battery runs once** for the cycle; **each cited _profiled_ story +satisfies its own mode and suffix**, with its own named evidence entry, while a cited +**unprofiled** story has no mode and owes no entry; the **lens sets are unioned** across +all cited stories; and the cycle is skip-eligible only if **every** cited story is. A +single "max" would either under-serve the strictest story or impose its obligations on +unrelated ones. + +**What the author owes before Gate B**, by mode: `battery` = the quality battery green · +`battery+check` = battery + **a check that fails without the change** · +`battery+check+verification` = battery + that check + a **named** verification of the risk +path · `+abuse-path` = one **named** abuse scenario plus evidence that the expected control +rejects or contains it. Level 2's two obligations are distinct; one artifact serves both +only if it demonstrates both. + +A check need not be an automated test — where none is possible, a **named verification** +satisfies it and the entry says which route was taken and why. Either route owes the +**counterfactual**: the observation against the prior state. An **unobservable +counterfactual is a blocking evidence gap**, not a free pass — stop and surface; the human +may then lower the mode as a logged override. A fabricated test satisfies nothing. + +**The evidence entry lives in the commit body** (see Mechanics), carries the **story path +and the named evidence but not the mode value**, and is **revalidated before every Gate-B +re-review and before the cycle-closing amend** — a fix changes the diff even when the +profile sits still. If revalidation changes the entry, the clean pass no longer covers what +is being committed: fix, re-review, close on the entry that pass validated. + +**Every Gate-B call and re-review carries the path of every cited story**, so the reviewer +reads each profile itself, **plus the current evidence entry, quoted verbatim, for each +cited *profiled* story** — an unprofiled one owes no mode-derived evidence, so it +contributes a path and nothing else. One profiled story means one pair; a cycle citing +several carries all of them, because the reviewer cannot union lenses it cannot see or +judge evidence it was never given. A reviewer handed neither can only review the diff — +the lenses and the evidence obligations would exist and never be consumed. + +**Two evidence gaps, two different answers.** Evidence that is *absent or inadequate* for +the mode is a **work gap**: produce it, then call. A project whose `AGENTS.md` names **no +verified quality command** cannot satisfy even `battery` — a **setup gap**: say what is +missing (`/workflow-init`'s battery step) rather than reviewing around it. Neither is a +reason to call Gate B against a weaker claim. + +**Changing a profile:** the pass **proposes the complete resulting header** — both axes, +the recomputed mode, any renewed override — and the **human confirms it**, in both +directions; an agent never moves it alone. On confirmation, correct the header and append +one profile-log line. Any axis change **voids every prior override**, raised or lowered, +and `+abuse-path` follows the current security value. Passes already run under the lower +profile **keep counting** toward the floor; only the **final clean pass** must run under +the current profile. Inside an active Gate-B cycle, fold the edit into the active `WIP:` +snapshot by amend — a non-`WIP` commit reads to the hook as the cycle closing and would +discard the accumulated passes. + +**What this does not do:** nothing checks which file a model actually read, whether the +header changed mid-call, or whether the lens sets were appended. This is instruction-backed +like the rest of §5; the detection is a reader comparing the pass against the story. + ### Mechanics (reference) - **Severity:** Blocker (wrong/unsafe/breaks invariant) · Major (design flaw → rework) → both must resolve. Minor · Nit → collect, never iterate. @@ -445,6 +553,11 @@ Add `/.context/codex-reviews/` to `.gitignore` — that entry specifically, not follow-up commit for two reasons: a `WIP: …` commit left in history defeats the naming convention it exists for, and a follow-up commit has nothing to commit when the review produced no fixes. + **The closing message carries the validated evidence entry for every cited profiled + story** — one each, and none for a cited unprofiled story, which owes no entry. The + amend replaces the WIP message wholesale, so an entry written only into the WIP body is + destroyed exactly when the cycle closes. The final commit body is the durable record; + a PR shows commit messages, so there is no second home to keep in sync. - **Timeout / abort:** a codex call that dies at the MCP tool-call timeout is retried once before surfacing to the user, and that retry *is* the single shared recovery attempt above — not a second one. An abort is an incomplete pass, so treat it as one: diff --git a/plugins/dev-workflow/hooks/codex-gate.sh b/plugins/dev-workflow/hooks/codex-gate.sh index 5b6019b..e57c31a 100755 --- a/plugins/dev-workflow/hooks/codex-gate.sh +++ b/plugins/dev-workflow/hooks/codex-gate.sh @@ -470,7 +470,7 @@ case "$event" in # was reviewed. emit "STOP — Codex Gate B not satisfied: the hook cannot confirm that the content you are about to commit is the content mcp__codex__review last saw ($passes recorded pass(es) this cycle). Usually that means the working tree or the index changed since the review. It can also mean you only staged already-reviewed content — the bytes are fine, but the hook cannot tell staging from editing; that this hook was upgraded and the recorded fingerprint uses the older format (see CHANGELOG); or that the fresh fingerprint could not be computed or could not be stored. Run Gate B (mcp__codex__review) now — one clean pass is the complete remedy for the staging and post-upgrade cases too. If a fresh pass leaves this unchanged with nothing edited in between, the fault is in the machinery rather than the code: check that .context/ is writable, that TMPDIR is writable, that a checksum tool (shasum, sha1sum or cksum) runs, that git status works, and that the disk is not full — then run one more pass to record a usable fingerprint. Per $policy you MUST re-review after every fix." "⚠ Codex Gate B not satisfied (cannot confirm review)" elif [ "$passes" -lt "$floor" ]; then - emit "Codex Gate B floor NOT met: only $passes/$floor mcp__codex__review pass(es) since the last commit. Per $policy the review is a LOOP with a hard minimum of $floor passes — run more (the ONLY early exit is a pass that returned zero findings), or proceed only if this change is trivial." "⚠ Codex Gate B below floor ($passes/$floor)" + emit "Codex Gate B floor NOT met: only $passes/$floor mcp__codex__review pass(es) since the last commit. Per $policy the review is a LOOP with a hard minimum of $floor passes — run more (the ONLY early exit is a pass that returned zero findings), or proceed only if $policy's skip rule applies to this change — if you cannot locate and check that rule, run the remaining passes." "⚠ Codex Gate B below floor ($passes/$floor)" else # Distinguish the two counts (Finding 9): the cycle total includes passes # made BEFORE later edits, so they carry a different fingerprint. diff --git a/plugins/dev-workflow/skills/intake/SKILL.md b/plugins/dev-workflow/skills/intake/SKILL.md index 77d88df..ed0506a 100644 --- a/plugins/dev-workflow/skills/intake/SKILL.md +++ b/plugins/dev-workflow/skills/intake/SKILL.md @@ -24,7 +24,8 @@ call to the new handler" is a HOW leak anywhere else. Target model: Claude via Claude Code. This skill is a prompt artifact and follows the checklist in `docs/prompt-standards.md`. -**Requires `AGENTS.md`** (the project's invariants file) — Section 5 greps it. If the +**Requires `AGENTS.md`** (the project's invariants file) — the Flow's invariant-tagging +step greps it, and story Section 4 quotes what it finds. If the repo has none, say so and offer `/dev-workflow:workflow-init`, which walks you through writing one. @@ -49,17 +50,75 @@ Follow in order. Each step names why it exists. the input without guessing (most often vague acceptance criteria or an unclear outcome), ask targeted questions first — surface gaps rather than filling them silently (CLAUDE.md §1). -3. **One question round, then pause** — ask at most one round, then **wait** for +3. **Assess the profile** — derive a proposal for two axes and a validation mode, so + the single question round below can carry it. Nothing is recorded yet: + - **Risk** — `trivial` (no behavioural effect in the artifact's own execution + context; for a **prompt artifact the text is the behaviour**, so a wording change + to a skill, command, agent definition, hook message or template is not trivial by + default) · `standard` (a behaviour change hitting no named trigger — the default) + · `high` (the change affects a named **domain** trigger — auth, permissions, + payments, migrations, data deletion, public APIs, personal data, supply chain — or + a named **effect** trigger — irreversibility, data loss or corruption, outage + exposure). + - **Security relevance** — `none` (touches no asset, trust boundary, role or + external system — a real answer and the common one) · `standard` (touches one + without changing what it permits) · `high` (changes a trust boundary, an + authorization decision, or the handling of secret or personal data). + - **Validation mode**, *derived not asked*: effective level = `max(risk, security)` + over `none|trivial → 0`, `standard → 1`, `high → 2` → `battery` / + `battery+check` / `battery+check+verification`, plus `+abuse-path` when and only + when security is `high`. + + Triggers match **surfaces, not words**: a doc that mentions auth is not `high`; a + change to an authorization decision is `high` even if the word never appears. + + Carry all three into the next step's question round with one line of reason each. +4. **One question round, then pause** — ask at most one round, then **wait** for the user; a later turn resumes with their answers. Do not proceed on silence — this bounds the interaction without barrelling past an unanswered question. -4. **Apply the grounding floor** — once answered (or the user says "proceed + + The round also carries the **profile proposal** from step 3 — alongside any + clarifying questions, or as the whole round when nothing needs clarifying. Intake + gains no second pause. The human confirms or corrects, and what a correction means + depends on what it touches: + - **An axis correction sets that axis freely, up or down — and the mode is + recomputed** from the corrected axes. Never carry the proposed mode over beside a + changed axis: `**Security:** high` next to `**Validation:** battery` is a header + the gates classify as unresolvable and stop on. + - **A mode correction is an override**, and it is bounded: it may raise or lower the + derived mode, but it cannot drop `+abuse-path` while security is `high`. Record it + as the first `mode override` entry in the profile log, with its direction and the + human's reason. **Say in the proposal that an override needs a reason**, since the + log entry cannot be written without one. + + **State the derivation in the proposal, so one answer settles the whole header.** The + mode is a function of the axes, not an independent choice: show `max(risk, security)` + and what each level yields, and the human's single answer then confirms the axes, any + override, *and* the mode that derivation produces from them — including after a + correction. That is why no second pause is needed and none is taken: nothing is left + for the human to choose once the axes are settled. + + **An answer that cannot be recorded ends this intake attempt** — an override with no + reason, an override dropping `+abuse-path` while security is `high`, a mode outside the + enums. Say which rule the answer collides with and **stop without writing**; do not + hold a pause open waiting for a repair, and do not invent the missing piece. This is + the grounding floor's shape, and it is honest about what happens next: the human's + corrected answer arrives at a **new intake run**, which opens its own single round with + that answer already in hand. What is forbidden is a second round *inside* one run, not + the human coming back. + + "Proceed anyway" or "I don't know" **is** an answer: it accepts the proposal as it + stands, and the values are recorded as proposed and **never lower** — a redundant lens + costs a paragraph, a missing one costs a review. There is no "unconfirmed" profile, so + the story is not written until the profile is settled. +5. **Apply the grounding floor** — once answered (or the user says "proceed anyway" / "don't know"), **stop without writing** if the problem statement, the desired outcome, or at least three grounded checkable acceptance criteria still can't be derived from the input without padding; report what's missing, quoting the thin part. A fabricated story launders guesses into a reviewed-looking artifact — stopping with "too thin: needs X" is the honest outcome. Genuine detail gaps instead become Open questions. -5. **Tag invariants by grepping AGENTS.md** (not from memory — memory drifts; +6. **Tag invariants by grepping AGENTS.md** (not from memory — memory drifts; AGENTS.md is the single source of truth): - extract the idea's domain terms **in English** (translate German first — `Einladung`→invitation, `Rechnung`→invoice — and add synonyms and the verb's @@ -72,20 +131,20 @@ Follow in order. Each step names why it exists. - cite each real match's `## Section` heading and quote the specific bullet line(s); if nothing matches, record "No AGENTS.md invariants matched" — an explicit negative, not a silently empty section. -6. **Assess size** — `chore` / `story` / `epic-needs-splitting` per the template's +7. **Assess size** — `chore` / `story` / `epic-needs-splitting` per the template's calibration, so sizing stays consistent across runs. -7. **Draft the story** from the template below (in your response — nothing on disk +8. **Draft the story** from the template below (in your response — nothing on disk yet). -8. **Present and wait** — show the draft and wait. On requested edits, apply them, - **re-validate the edited draft against every constraint** (six sections, - no-HOW except §4, grounding floor, ≥3 checkable criteria), and re-present. Never - commit unseen or edit-broken text; a user edit can accidentally introduce HOW or - drop a section. -9. **On approval, write and commit** — write the **exact approved text** to the - computed path (see *Writing the story file*), immediately before staging, then - commit via the commit protocol. Writing only after approval means the staged - bytes are, by construction, what the user saw. -10. **Hand off** — name the next step and stop. intake's job ends at capture. +9. **Present and wait** — show the draft and wait. On requested edits, apply them, + **re-validate the edited draft against every constraint** (six sections, the + profile header line, no-HOW except §4, grounding floor, ≥3 checkable criteria), and + re-present. Never commit unseen or edit-broken text; a user edit can accidentally + introduce HOW or drop a section. +10. **On approval, write and commit** — write the **exact approved text** to the + computed path (see *Writing the story file*), immediately before staging, then + commit via the commit protocol. Writing only after approval means the staged + bytes are, by construction, what the user saw. +11. **Hand off** — name the next step and stop. intake's job ends at capture. ## Story template @@ -95,6 +154,11 @@ Write the file with exactly these six `##` sections, in order: # — Story **Date:** YYYY-MM-DD · **Size:** chore | story | epic-needs-splitting +**Risk:** trivial | standard | high · **Security:** none | standard | high · **Validation:** <derived mode> + +<!-- Profile log: omit this whole block until the first change. On the first one: --> +**Profile log:** +- YYYY-MM-DD · axis change · risk ↑ · Gate-B pass 2 finding on the migration path · adds the risk lens set ## 1. Problem statement <What's wrong or missing today, in the user's terms.> @@ -127,6 +191,31 @@ worth — name the suggested split. **Acceptance criteria** describe observable outcomes or constraints, never implementation steps (WHAT is true when done, not how it's built). +**Profile log.** A `**Profile log:**` label directly beneath the profile header, followed +by `-` entries — written **on the first change and never before**, so a story whose +profile never moves carries no empty block. One line per event: date · **event kind** · +**direction** · **reason or trigger** (a finding reference when a finding caused it, plain +prose when none did) · what it changes downstream. The three kinds: + +- `axis change` — names the **axis identity** (`risk`, `security`, or both in one entry + when both move) and a **direction per named axis**, since both may move at once and in + opposite directions: + `- 2026-07-26 · axis change · risk ↑, security ↓ · pass-3 finding: the job truncates a table irreversibly, and the auth surface it appeared to touch is unreachable · adds the risk lens set, drops the security lens set` +- `mode override` — names its own **direction**, because a human may raise or lower the + derived mode. A lowering removes an obligation, so its reason says which one and why: + `- 2026-07-26 · mode override · ↓ · the risk-path verification duplicates the migration rehearsal already run in staging · drops the named verification, keeps the counterfactual check` + + Choosing a **named verification** because no automated test is possible is **not** an + override: it is one of the two routes that satisfy `+check` at the same mode, and it + still owes the counterfactual. Logging it as a lowering would record a mode the header + never moved to. +- `adoption` — has no previous value, so **no direction**: + `- 2026-07-26 · adoption · in-flight story adopting a profile at its plan checkpoint · gates now read this header` + +The log never restates the values — the header is the single writable copy, and git +history already holds what the previous values were. A mode override chosen during intake +confirmation is the first `mode override` event and creates the log. + ## Writing the story file **Path:** `docs/superpowers/stories/YYYY-MM-DD-<topic>-story.md`, where diff --git a/todos.md b/todos.md index f6053d0..e16ca29 100644 --- a/todos.md +++ b/todos.md @@ -131,19 +131,25 @@ That belongs in each product project's own `todos.md` once `/workflow-init` has there, not here: this repo ships the workflow, it does not hold another project's backlog. -- [ ] **P2 + P6 — risk/security profiles, and security sections in the intake, spec and - gate templates.** One story: the profile is what the sections key off, so shipping - the sections without it just adds a heading nobody knows how to fill in. - *Trigger: the first real intake in a product project* — **FIRED.** - Calibration point (infinite-portfolio-canvas, 2026-07-26): 51 Gate-A pass files - across 2 stories — spec 14, plan 14, replan 4, amend 12, a3-spec 7 — in a product - project on a long-lived branch. That is a real risk profile to key sections off, - not a guess about what product projects might need. +- [x] **P2 — risk/security profiles, and the derived validation mode.** Shipped: two + human-confirmed axes in the story header, a mode derived as `max(risk, security)`, + lens sets appended to the §5 gate prompts, and the Gate-B triviality skip narrowed + to effective level 0. Spec: + `docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md`. +- [ ] **P6 — standalone security sections in the intake, spec and gate templates: + DELIBERATELY REJECTED, not shipped.** The profile *is* the heading: a standalone + section would be a second surface to keep in sync with it (the docs-drift class), + and it invites boilerplate-filling on stories where nobody knows what to write. + Security content lives in the spec's decision record and risks discussion and in + `AGENTS.md` invariants; the security lens set is what asks about assets, trust + boundaries, roles, external systems and abuse paths. *Reopens when:* field use shows + high-security content scattering incoherently across specs — that recurrence is the + trigger, not a fresh opinion. - [ ] **P5 light — stable AC-/SEC-IDs in the story and plan templates.** Identifiers that survive from story to plan to review, so an acceptance criterion can be cited - instead of re-described. *Trigger: rides with P2* — the IDs exist to label what P2's - sections produce, so landing them first would ship a numbering scheme with nothing - to number. + instead of re-described. *Trigger: the first story that runs under profiles* — the + IDs exist to label what profiles produce, so the numbering scheme should meet a real + profiled story before it gets a template slot. - [ ] **Upstream-report candidate: `claude plugin update <bare-name>`.** The CLI docs document the bare plugin name as a valid argument for `install`/`uninstall`/`update`