From b1625247eb154b4efa0548e0eed9e22334bb4f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:49:15 +0200 Subject: [PATCH 1/8] docs(intake): add risk-security-validation-profiles story --- ...risk-security-validation-profiles-story.md | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md 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..0c046ac --- /dev/null +++ b/docs/superpowers/stories/2026-07-26-risk-security-validation-profiles-story.md @@ -0,0 +1,91 @@ +# 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), trivial legitimizes the documented skip +with a recorded reason. 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 domain triggers — auth, permissions, payments, + migrations, data deletion, public APIs, personal data, supply chain — not by + unaided judgement. +- [ ] 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; at `trivial` the documented skip is available with its reason recorded. +- [ ] 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: P2+P6 closed, P5 light's trigger re-pointed at + "the first story that runs under profiles". +- [ ] Scope holds: nothing under `plugins/dev-workflow/hooks/` changes, and no new + script is added. + +## 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. From d676b040fab6dfd1eec49495ad44641cb3e20c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Sun, 26 Jul 2026 17:11:44 +0200 Subject: [PATCH 2/8] docs(spec): risk, security, and validation profiles design --- ...isk-security-validation-profiles-design.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md 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..4770f01 --- /dev/null +++ b/docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md @@ -0,0 +1,172 @@ +# 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 switching any gate off. + +## 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**; the validation mode is **derived**, never asked | A third question invites an answer inconsistent with the first two | — | +| 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 upgrade mechanism (§6). Adoption allowed, retrofit not required | +| D3 | `trivial` does **not** relax Gate A's floor or 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 validation mode governs **evidence the author must produce**; risk governs **questions the reviewer asks** | Orthogonal levers. Had the mode also steered the gates it would be a second name for risk | — | +| D6 | Profiles never switch a gate off and never change the 3-pass floor | The mandatory-gates promise is the product; an economics lever that can disable review is not an economics lever | — | + +Rejected, for the record: the mode as a **gate schedule** (full / reduced / minimal) — +it would let profiles switch gates off, the exact promise D3 protects; and the mode as +**artifact depth** (full spec vs. design note vs. story-only) — "depth" is not checkable, +and prose review never converges on it. A **spec-header echo** of the profile was +considered and 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:** +``` + +**Risk** answers *what does this break if it is wrong*. `high` is **proposed whenever +the story hits a named trigger**: auth, permissions, payments, migrations, data +deletion, public APIs, personal data, supply chain. The list is domain vocabulary, not +stack vocabulary, so it stays in the shipped skill (invariant 10 holds); a project that +needs more triggers adds them to its own `AGENTS.md`, not to the skill. + +**Security relevance** answers *does this touch assets, trust boundaries, roles, or +external systems*. `none` is a real answer and the common one; it must not read as an +admission of carelessness, or every story drifts to `standard`. + +**Confirmation and the default.** Intake proposes all three values with a one-line +reason each; the human confirms or corrects; the story is not written until they do — +the existing one-round pause governs. If the human says "proceed anyway" or does not +know, the recorded value is **the agent's proposal and never lower than it**. That is +invariant 2 ("loose in the firing direction") applied to intake: a redundant lens costs +a paragraph, a missing one costs a review. + +## 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+test` | battery + a test that fails without the change | +| 2 | `battery+test+verification` | battery + tests + a **named** verification | +| security `high` | `+ abuse-path check` | added to the mode above | + +`max()` is deliberate at the corner: a change judged `trivial` that sits 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. + +**"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 does not satisfy any mode. The entry travels in the Gate-B call and the +PR body — the commit message body in projects that never open PRs — so no new file and +no new story section is introduced to hold it. + +**Forward-compatible by design.** Today `named verification` and `abuse-path check` +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 story +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. +- **risk `trivial`** → nothing appended. It unlocks the *existing* Gate-B triviality + skip, which now requires a recorded reason in the story rather than unrecorded + judgement. + +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. The coverage rule is unchanged too: Codex reports every finding with +severity and confidence; the filtering stays downstream. + +## 6. Upgrades (and how they interact with the floor) + +Any pass may reveal that the profile was set too low. Then: + +1. The story header value is **corrected**, and a line is **appended** beneath it: date · + old → new · the finding that triggered it · what it changes downstream. +2. The edit is **committed docs-only at the next natural commit point**. An uncommitted + upgrade exists on one machine's disk, while the profile is input to the gates. +3. The new profile takes effect **at the next pass**, mechanically, because the prompt + re-reads the header rather than remembering a value quoted in an earlier pass. + +**Downgrades** are possible but never automatic: human confirmation, same appended 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**, which the fresh header read guarantees mechanically. 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. + +## 7. Compatibility and scope + +A story with **no profile line behaves exactly as today**: standard intensity, no lens +sets appended, no triviality skip available. That is what makes D2 free — nothing in +flight breaks, and adoption is a one-line edit through the §6 mechanism whenever a +story wants it. + +**Surfaces touched:** `plugins/dev-workflow/skills/intake/SKILL.md` (proposal step + +header line 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), `todos.md` (P2+P6 closed; P5 light's trigger re-pointed at the first story that +runs under profiles), CHANGELOG + plugin version bump (invariant 12). + +**Non-goals:** no hook change (the hook counts passes and does not need to know +profiles); no new script; no new scaffolded file; no standalone security section in any +template — security content lives in the spec's existing sections and in AGENTS.md +invariants, and the security lens set is the mechanism that forces those concerns to be +addressed there. 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 and 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; it changes nothing about what either gate + compares. From 8db4eeb4d8056bbb4279fc2834c80267f6d838c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Sun, 26 Jul 2026 17:52:02 +0200 Subject: [PATCH 3/8] docs(spec): profiles design through Gate A (8 passes, clean at 8) Spec revised across eight Codex Gate-A passes (21/10/13/10/8/5/3/1 findings; last four Blocker-free). Story AC 9 amended in the same commit: the design splits the P2+P6 todos row rather than closing it whole, since P6's standalone-section half was deliberately rejected. --- ...isk-security-validation-profiles-design.md | 418 ++++++++++++++---- ...risk-security-validation-profiles-story.md | 8 +- 2 files changed, 338 insertions(+), 88 deletions(-) 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 index 4770f01..4e8c859 100644 --- 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 @@ -19,49 +19,93 @@ 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 switching any gate off. +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**; the validation mode is **derived**, never asked | A third question invites an answer inconsistent with the first two | — | -| 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 upgrade mechanism (§6). Adoption allowed, retrofit not required | -| D3 | `trivial` does **not** relax Gate A's floor or 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 | +| 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 validation mode governs **evidence the author must produce**; risk governs **questions the reviewer asks** | Orthogonal levers. Had the mode also steered the gates it would be a second name for risk | — | -| D6 | Profiles never switch a gate off and never change the 3-pass floor | The mandatory-gates promise is the product; an economics lever that can disable review is not an economics lever | — | +| 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 D3 protects; and the mode as +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. A **spec-header echo** of the profile was -considered and rejected under D4. +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:** +**Risk:** trivial | standard | high · **Security:** none | standard | high · **Validation:** ``` -**Risk** answers *what does this break if it is wrong*. `high` is **proposed whenever -the story hits a named trigger**: auth, permissions, payments, migrations, data -deletion, public APIs, personal data, supply chain. The list is domain vocabulary, not -stack vocabulary, so it stays in the shipped skill (invariant 10 holds); a project that -needs more triggers adds them to its own `AGENTS.md`, not to the skill. - -**Security relevance** answers *does this touch assets, trust boundaries, roles, or -external systems*. `none` is a real answer and the common one; it must not read as an -admission of carelessness, or every story drifts to `standard`. - -**Confirmation and the default.** Intake proposes all three values with a one-line -reason each; the human confirms or corrects; the story is not written until they do — -the existing one-round pause governs. If the human says "proceed anyway" or does not -know, the recorded value is **the agent's proposal and never lower than it**. That is -invariant 2 ("loose in the firing direction") applied to intake: a redundant lens costs -a paragraph, a missing one costs a review. +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 @@ -72,89 +116,291 @@ top of whichever mode applies. | effective | mode | the author must show, before Gate B | |---|---|---| | 0 | `battery` | the project's quality battery green | -| 1 | `battery+test` | battery + a test that fails without the change | -| 2 | `battery+test+verification` | battery + tests + a **named** verification | -| security `high` | `+ abuse-path check` | added to the mode above | +| 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. -`max()` is deliberate at the corner: a change judged `trivial` that sits on +**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 does not satisfy any mode. The entry travels in the Gate-B call and the -PR body — the commit message body in projects that never open PRs — so no new file and -no new story section is introduced to hold it. - -**Forward-compatible by design.** Today `named verification` and `abuse-path check` -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. +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 story +satisfies its own mode and suffix** with its own named evidence entry, 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 story -header fresh at each pass, and append the lens set(s) the current values call for.** +`/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. -- **risk `trivial`** → nothing appended. It unlocks the *existing* Gate-B triviality - skip, which now requires a recorded reason in the story rather than unrecorded - judgement. - -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. The coverage rule is unchanged too: Codex reports every finding with -severity and confidence; the filtering stays downstream. - -## 6. Upgrades (and how they interact with the floor) - -Any pass may reveal that the profile was set too low. Then: - -1. The story header value is **corrected**, and a line is **appended** beneath it: date · - old → new · the finding that triggered it · what it changes downstream. -2. The edit is **committed docs-only at the next natural commit point**. An uncommitted - upgrade exists on one machine's disk, while the profile is input to the gates. -3. The new profile takes effect **at the next pass**, mechanically, because the prompt - re-reads the header rather than remembering a value quoted in an earlier pass. - -**Downgrades** are possible but never automatic: human confirmation, same appended line. +- **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 the recorded reason for §5's + pre-existing Gate-B triviality skip; it creates no skip of its own. + +**The skip keys on the effective level, never on risk alone.** §5's Gate-B triviality +skip becomes available only when `max(risk, security)` is 0 — risk `trivial` *and* +security `none`. 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 story is still mode +`battery` by derivation, so the battery still runs and its entry still lands in the commit +body before the commit. 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** — unparseable line, a value outside the + enums, two profile blocks, a citation that resolves to nothing → stop and surface the + cause. 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**, which the fresh header read guarantees mechanically. 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. +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**: standard intensity, no lens -sets appended, no triviality skip available. That is what makes D2 free — nothing in -flight breaks, and adoption is a one-line edit through the §6 mechanism whenever a -story wants it. - -**Surfaces touched:** `plugins/dev-workflow/skills/intake/SKILL.md` (proposal step + -header line 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), `todos.md` (P2+P6 closed; P5 light's trigger re-pointed at the first story that -runs under profiles), CHANGELOG + plugin version bump (invariant 12). +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 and no +commit-body 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. 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/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 (the hook counts passes and does not need to know profiles); no new script; no new scaffolded file; no standalone security section in any -template — security content lives in the spec's existing sections and in AGENTS.md -invariants, and the security lens set is the mechanism that forces those concerns to be -addressed there. 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. +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 @@ -162,11 +408,11 @@ what the IDs would number. 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 and lens sets stay stack-neutral; project vocabulary belongs - in that project's own files. +- **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; it changes nothing about what either gate - compares. + 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 index 0c046ac..62b2523 100644 --- 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 @@ -50,8 +50,12 @@ knowing what each item risks. - [ ] 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: P2+P6 closed, P5 light's trigger re-pointed at - "the first story that runs under profiles". +- [ ] `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: nothing under `plugins/dev-workflow/hooks/` changes, and no new script is added. From 3feb31878071df6fb150bfe8667cde39f09f21a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:39:17 +0200 Subject: [PATCH 4/8] docs(plan): implementation plan for risk/security/validation profiles --- ...07-26-risk-security-validation-profiles.md | 528 ++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md 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..5cb2d7d --- /dev/null +++ b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md @@ -0,0 +1,528 @@ +# 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). No hook change, no new script, no new +scaffolded file. + +**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. + +**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` — Flow step 6 (line 75), 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 proposal as Flow step 6** + +Renumber the existing steps 6–10 to 7–11. Insert as the new step 6, immediately after the +"Tag invariants by grepping AGENTS.md" step: + +```markdown +6. **Propose the profile, and let the human settle it** — two axes and a derived + validation mode, all three confirmed before the story is written: + - **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. + + Propose all three with one line of reason each **inside the single question round** + of 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; a + correction may set any value, up or down. "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. +``` + +- [ ] **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:** +``` + +- [ ] **Step 3: Document the profile log after the size calibration block** + +Insert after the "**Acceptance criteria** describe observable outcomes…" paragraph +(line 128): + +```markdown +**Profile log.** A `**Profile log:**` label followed by `-` entries, written **on the +first change and never before** — a story whose profile never moves carries no empty +block. One line per event: date · **event kind** (`axis change` | `mode override` | +`adoption`) · **axis identity** on an axis change (`risk`, `security`, or both in one +entry when both move) · **direction per named axis** (both may move at once and in +opposite directions) · **reason or trigger** (a finding reference when a finding caused +it, plain prose when none did) · what it changes downstream. + +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.** They are 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** (unparseable line, a value outside the enums, + two profile blocks, a citation resolving to nothing) → **stop and surface the cause**. + 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 keys on the effective level, never on risk alone.** For a +profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* +security `none` — and the reason goes in the story's profile log. **A skip removes the +review, never the evidence:** the battery still runs and its entry still lands in the +commit body. In a cycle citing several stories, **every** cited story must be +skip-eligible. An **unprofiled** story keeps exactly today's judgement-based skip and owes +no mode-derived evidence. + +**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. + +**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 evidence entry** for a profiled story — 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** + +Run: + +```bash +diff <(sed -n '/^### Profiles — how much review this story gets$/,/^### Mechanics (reference)$/p' CLAUDE.md) \ + <(sed -n '/^### Profiles — how much review this story gets$/,/^### Mechanics (reference)$/p' plugins/dev-workflow/commands/workflow-init.md) +``` + +Expected: no output. Any output is drift — reconcile before committing. Both files +changing is not evidence that they agree; this diff 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 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. +``` + +- [ ] **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`, the sentence beginning "The `intake` skill turns it into a +story: problem, outcome, ≥3 checkable acceptance criteria, which `AGENTS.md` invariants it +touches." — replace with: + +```markdown +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, 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, and that profile is +what decides which extra questions the review gates ask. +``` + +- [ ] **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. + +- [ ] **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 to effective level 0 for profiled stories, 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 the evidence entry, so the final commit + body is its durable record. +- Unprofiled stories behave exactly as before, including today's judgement-based skip. +``` + +- [ ] **Step 3: Run the full battery** + +Run (the AGENTS.md quality row, verbatim): + +```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 +``` + +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`). + +- [ ] **Step 4: Snapshot for Gate B** + +```bash +git add -A && git commit -m "WIP: profiles — snapshot for Gate B" +git log --oneline -6 +``` + +Note the SHA of the commit **before** Task 1's first WIP commit — that is `baseSha`. + +- [ ] **Step 5: 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` from Step 4, and an `additionalContext` that carries: the +spec path, the story path, "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. Write `-dispositions.md` per pass. + +This story's own profile, applied to itself: risk `standard` (prompt artifacts, no named +domain or effect trigger), security `none`, so mode `battery` — the battery in Step 3 is +the evidence, and its entry goes in the closing commit body per Step 6. + +- [ ] **Step 6: Close the cycle with one real commit** + +```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 triviality +skip to effective level 0. Unprofiled stories are unaffected. + +Validation: mode `battery` (risk standard, security none). +Evidence: full battery green — shellcheck, hook tests, check-invariants, +check-version-bump, claude plugin validate — run at , exit 0. +EOF +``` + +- [ ] **Step 7: 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. From dbe505867c47ba169f8aecea0a34867be30ffa80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:51:44 +0200 Subject: [PATCH 5/8] docs(plan): plan through Gate A (3 passes, clean at 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan revised across three Gate-A passes (12/7/0 findings, 5 Blockers). Spec §5 read-case 3 updated in the same commit: only the latest mode override after the latest axis change can explain a mode/axes mismatch, since an axis change voids prior overrides. --- ...07-26-risk-security-validation-profiles.md | 263 +++++++++++++----- ...isk-security-validation-profiles-design.md | 17 +- 2 files changed, 206 insertions(+), 74 deletions(-) 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 index 5cb2d7d..f175fe2 100644 --- a/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md +++ b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md @@ -52,8 +52,10 @@ 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` — Flow step 6 (line 75), the story - template (lines 94–120), and the calibration block after it (lines 122–128) +- 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. @@ -63,14 +65,16 @@ commit via `git reset --soft` + commit. `**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 proposal as Flow step 6** +- [ ] **Step 1: Insert the profile assessment as a new Flow step 3, BEFORE the question round** -Renumber the existing steps 6–10 to 7–11. Insert as the new step 6, immediately after the -"Tag invariants by grepping AGENTS.md" step: +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 -6. **Propose the profile, and let the human settle it** — two axes and a derived - validation mode, all three confirmed before the story is written: +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 @@ -91,13 +95,21 @@ Renumber the existing steps 6–10 to 7–11. Insert as the new step 6, immediat 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. - Propose all three with one line of reason each **inside the single question round** - of 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; a - correction may set any value, up or down. "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. + 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; a correction may set any value, up or down. + "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** @@ -113,25 +125,38 @@ 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** -Insert after the "**Acceptance criteria** describe observable outcomes…" paragraph -(line 128): +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 followed by `-` entries, written **on the -first change and never before** — a story whose profile never moves carries no empty -block. One line per event: date · **event kind** (`axis change` | `mode override` | -`adoption`) · **axis identity** on an axis change (`risk`, `security`, or both in one -entry when both move) · **direction per named axis** (both may move at once and in -opposite directions) · **reason or trigger** (a finding reference when a finding caused -it, plain prose when none did) · what it changes downstream. +**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: + `- 2026-07-26 · mode override · ↓ · no automated check is possible for prompt text · verification replaces the counterfactual test` +- `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. +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** @@ -214,19 +239,32 @@ filter, the file-first findings protocol and the clean-final-pass rule are uncha 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** (unparseable line, a value outside the enums, - two profile blocks, a citation resolving to nothing) → **stop and surface the cause**. - Falling back to the lighter behaviour on a malformed profile would under-review exactly - the stories most likely to have one. +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` recorded after the latest + `axis change`**, moving in a direction compatible with the current value, can explain + such a mismatch — an axis change voids every prior override, so an older one resolves + nothing. 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 keys on the effective level, never on risk alone.** For a profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* security `none` — and the reason goes in the story's profile log. **A skip removes the review, never the evidence:** the battery still runs and its entry still lands in the -commit body. In a cycle citing several stories, **every** cited story must be -skip-eligible. An **unprofiled** story keeps exactly today's judgement-based skip and owes +commit body. An **unprofiled** story keeps exactly today's judgement-based skip and owes no mode-derived evidence. +**A cycle citing several stories** aggregates along separate dimensions, never through one +winning mode: the **battery runs once** for the cycle; **each cited story satisfies its own +mode and suffix**, with its own named evidence 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 @@ -292,17 +330,27 @@ diverge. 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** +- [ ] **Step 5: Verify the two copies agree — BOTH halves of the paired edit** -Run: +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 -diff <(sed -n '/^### Profiles — how much review this story gets$/,/^### Mechanics (reference)$/p' CLAUDE.md) \ - <(sed -n '/^### Profiles — how much review this story gets$/,/^### Mechanics (reference)$/p' plugins/dev-workflow/commands/workflow-init.md) +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 evidence entry\*\*/,/second home to keep in sync\./p' CLAUDE.md) +M_TMPL=$(sed -n '/\*\*The closing message carries the 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: no output. Any output is drift — reconcile before committing. Both files -changing is not evidence that they agree; this diff is. +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** @@ -379,19 +427,32 @@ git commit -m "WIP: split P2+P6 in the backlog, re-point P5 light" - [ ] **Step 1: Update the getting-started intake paragraph** -In `docs/getting-started.md`, the sentence beginning "The `intake` skill turns it into a -story: problem, outcome, ≥3 checkable acceptance criteria, which `AGENTS.md` invariants it -touches." — replace with: +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 -a voice transcript — German is fine). The `intake` skill turns it into a story: +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, and that profile is -what decides which extra questions the review gates ask. +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`: @@ -403,13 +464,17 @@ Run: `grep -rn "story\b" README.md docs/coding-workflow.md docs/architecture.md 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. 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 - -" +git commit -m "WIP: docs audit for profiles" ``` --- @@ -447,45 +512,96 @@ Insert as the newest entry, matching the file's existing style (no dates): - Unprofiled stories behave exactly as before, including today's judgement-based skip. ``` -- [ ] **Step 3: Run the full battery** +- [ ] **Step 3: Snapshot FIRST — the version bump must be committed before the battery runs** -Run (the AGENTS.md quality row, verbatim): +`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: ```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 +git add -A && 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`). +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 4: Snapshot for Gate B** +- [ ] **Step 5: Write the evidence entry into the WIP body, before the first Gate-B call** + +The spec makes the evidence a **precondition** of the call, and requires the call to quote +the prepared entry rather than compose its own. Amend the snapshot so its body carries it — +story **path** and named evidence, and **no profile values**: ```bash -git add -A && git commit -m "WIP: profiles — snapshot for Gate B" -git log --oneline -6 +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." ``` -Note the SHA of the commit **before** Task 1's first WIP commit — that is `baseSha`. +**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 5: Gate B, minimum 3 passes, file protocol** +- [ ] **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` from Step 4, and an `additionalContext` that carries: the -spec path, the story path, "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. Write `-dispositions.md` per pass. +`reviewType: full`, `baseSha: `, and an `additionalContext` that carries: the spec +path, the story path, **the exact evidence entry from Step 5 quoted verbatim**, "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: -This story's own profile, applied to itself: risk `standard` (prompt artifacts, no named -domain or effect trigger), security `none`, so mode `battery` — the battery in Step 3 is -the evidence, and its entry goes in the closing commit body per Step 6. +```bash +git add # never -A blindly; check `git status` first +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`. + +**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 6: Close the cycle with one real commit** +- [ ] **Step 7: Close the cycle with one real commit** + +The closing message must carry the **same evidence entry** the final clean pass validated +(§5 Mechanics), the docs-audit verdicts from Task 4, and **no profile values** — the story +header is their single writable copy: ```bash -git reset --soft +git reset --soft git commit -F - <<'EOF' feat(intake,§5): risk, security, and validation profiles @@ -493,13 +609,20 @@ 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 triviality skip to effective level 0. Unprofiled stories are unaffected. -Validation: mode `battery` (risk standard, security none). -Evidence: full battery green — shellcheck, hook tests, check-invariants, -check-version-bump, claude plugin validate — run at , exit 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. + +Docs audit: EOF ``` -- [ ] **Step 7: Open the PR** +The evidence 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 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 index 4e8c859..dc28d9a 100644 --- 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 @@ -287,10 +287,19 @@ they need different answers: 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** — unparseable line, a value outside the - enums, two profile blocks, a citation that resolves to nothing → stop and surface the - cause. Falling back to the lighter behaviour on a *malformed* profile would - under-review exactly the stories most likely to have one. +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` recorded after the + latest `axis change`**, moving in a direction compatible with the current value, can: + an axis change voids every prior override (§4), so an older one resolves nothing. + 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 From 03901f38ab79e5a2b24a6e22235b100241aacb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:18:54 +0200 Subject: [PATCH 6/8] =?UTF-8?q?feat(intake,=C2=A75):=20risk,=20security,?= =?UTF-8?q?=20and=20validation=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two human-confirmed axes in the story header — risk (trivial|standard|high, via named domain and effect triggers) and security relevance (none|standard|high) — plus a validation mode derived as max(risk, security). The axes append review lens sets at the gates; the mode sets what evidence the author owes before Gate B. Different questions, not more passes: the 3-pass floor, the Blocker/Major filter and the file-first findings protocol are unchanged, and no new way to skip a gate is added. The Gate-B triviality skip narrows for a profiled story to effective level 0 and still requires the change itself to be trivial. Unprofiled stories behave exactly as before. 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 (123 assertions), check-version-bump + suite (36 assertions), claude plugin validate --strict; exit 0. The story is unprofiled, so no mode-derived evidence entry is owed. Docs audit: getting-started.md and coding-workflow.md updated (profile, its two halves, and the narrowed skip); README.md unchanged — its intake row is a capability summary, not a field list, and the flow shape is unaffected; architecture.md unchanged — it describes layout, not story fields; MANIFEST.md unchanged — an inventory of the frozen source-files/ seed. Scope-guard waiver (granted during Gate B pass 11, reasoning in the plan's decision record): one string in hooks/codex-gate.sh. Its below-floor reminder asserted a skip rule the narrowing made false, at the moment an author decides whether to skip. The edit removes a rule statement and defers to the policy file, so the hook reads no profile and gains no machinery. Gates: Gate A spec clean at pass 8 (21/10/13/10/8/5/3/1), Gate A plan clean at pass 3 (12/7/0), Gate B clean at pass 16 on both branches. --- CLAUDE.md | 110 +++++++++++ docs/coding-workflow.md | 23 ++- docs/getting-started.md | 23 ++- ...07-26-risk-security-validation-profiles.md | 177 ++++++++++++++---- ...isk-security-validation-profiles-design.md | 21 ++- .../dev-workflow/.claude-plugin/plugin.json | 2 +- plugins/dev-workflow/CHANGELOG.md | 30 +++ .../commands/process-pr-review.md | 19 +- .../dev-workflow/commands/workflow-init.md | 110 +++++++++++ plugins/dev-workflow/hooks/codex-gate.sh | 2 +- plugins/dev-workflow/skills/intake/SKILL.md | 121 ++++++++++-- todos.md | 28 +-- 12 files changed, 576 insertions(+), 90 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c620b13..db431a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -251,6 +251,111 @@ 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 keys on the effective level, never on risk alone.** For a +profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* +security `none` — and **its reason is recorded in the commit body**, beside the evidence +entry. Not in the profile log: that log records profile *changes*, and a skip changes no +profile value. **A skip removes the review, never the evidence:** the battery still runs and its entry still lands in the +commit body. An **unprofiled** story keeps exactly today's judgement-based skip and owes +no mode-derived evidence. + +**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 +375,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..a88caad 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,19 @@ 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 and the reason is recorded. **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..a78f3e1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,7 +11,14 @@ 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 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, 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 +75,11 @@ 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: what a `trivial` +profile unlocks is the **Gate-B** skip and only at effective level 0 (risk `trivial` +*and* security `none`), with the battery still owed and Gate A's floor unchanged; a +story with no profile keeps the prior judgement-based Gate-B skip. 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 +96,11 @@ 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 the skip reason 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 index f175fe2..9d5c85c 100644 --- a/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md +++ b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md @@ -42,6 +42,18 @@ this section. 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 @@ -103,9 +115,36 @@ Then extend the question-round step (now step 4) so it asks them. Its current te 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; a correction may set any value, up or down. + 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 @@ -149,8 +188,14 @@ prose when none did) · what it changes downstream. The three kinds: 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: - `- 2026-07-26 · mode override · ↓ · no automated check is possible for prompt text · verification replaces the counterfactual test` + 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` @@ -220,8 +265,8 @@ only when security is `high`). The **story header is the single writable copy** 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.** They are separate levers: -one aims the reviewer, the other obliges the author. +**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, @@ -243,30 +288,34 @@ filter, the file-first findings protocol and the clean-final-pass rule are uncha 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` recorded after the latest - `axis change`**, moving in a direction compatible with the current value, can explain - such a mismatch — an axis change voids every prior override, so an older one resolves - nothing. 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. + `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 keys on the effective level, never on risk alone.** For a profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* -security `none` — and the reason goes in the story's profile log. **A skip removes the -review, never the evidence:** the battery still runs and its entry still lands in the +security `none` — and **its reason is recorded in the commit body**, beside the evidence +entry. Not in the profile log: that log records profile *changes*, and a skip changes no +profile value. **A skip removes the review, never the evidence:** the battery still runs and its entry still lands in the commit body. An **unprofiled** story keeps exactly today's judgement-based skip and owes no mode-derived evidence. **A cycle citing several stories** aggregates along separate dimensions, never through one -winning mode: the **battery runs once** for the cycle; **each cited story satisfies its own -mode and suffix**, with its own named evidence entry; the **lens sets are unioned** across +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` = 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 @@ -284,6 +333,20 @@ re-review and before the cycle-closing amend** — a fix changes the diff even w 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 @@ -311,8 +374,9 @@ Find (line 266–267): Insert a sentence at the end of that bullet's paragraph, after "…produced no fixes.": ```markdown - **The closing message carries the evidence entry** for a profiled story — the amend - replaces the WIP message wholesale, so an entry written only into the WIP body is + **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. ``` @@ -339,8 +403,8 @@ 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 evidence entry\*\*/,/second home to keep in sync\./p' CLAUDE.md) -M_TMPL=$(sed -n '/\*\*The closing message carries the evidence entry\*\*/,/second home to keep in sync\./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; } @@ -465,10 +529,24 @@ Expected: a short list to read; most will be accurate as-is because they describ 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. 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. +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** @@ -507,7 +585,8 @@ Insert as the newest entry, matching the file's existing style (no dates): skip narrowed to effective level 0 for profiled stories, 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 the evidence entry, so the final commit +- §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. ``` @@ -542,9 +621,16 @@ 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** -The spec makes the evidence a **precondition** of the call, and requires the call to quote -the prepared entry rather than compose its own. Amend the snapshot so its body carries it — -story **path** and named evidence, and **no profile values**: +**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) @@ -566,9 +652,11 @@ a re-run before every re-review and before the close. Amending keeps the message 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 story path, **the exact evidence entry from Step 5 quoted verbatim**, "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 +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. @@ -596,9 +684,10 @@ sets, no mode-derived evidence. Running it profiled would mean adopting a profil - [ ] **Step 7: Close the cycle with one real commit** -The closing message must carry the **same evidence entry** the final clean pass validated -(§5 Mechanics), the docs-audit verdicts from Task 4, and **no profile values** — the story -header is their single writable copy: +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 @@ -610,7 +699,7 @@ from them; §5 appends lens sets per axis and narrows the Gate-B triviality skip to effective level 0. Unprofiled stories are unaffected. 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), +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. @@ -618,9 +707,15 @@ Docs audit: — Story **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 ## 1. Problem statement @@ -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--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 `.** The CLI docs document the bare plugin name as a valid argument for `install`/`uninstall`/`update` From 5201d9b1f68a00b8aeceacf4aed99ae90bce1a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:00:09 +0200 Subject: [PATCH 7/8] fix(pr-review): profile-aware skip in process-pr-review, and artifact consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten bot findings from PR #15 (CodeRabbit 9, Greptile 1), all validated directly rather than by triage subagent — the PR edits instruction-bearing paths, which the command's step-0 precheck routes to manual validation. Greptile's P1 was the substantive one: `process-pr-review` resolved "cited story paths" without saying where citations live, so a PR whose story path sits in a commit body — where this workflow puts it — fell into the "no story cited" branch and took the unprofiled judgement call. It now reads the PR body and the commit bodies in the range. Also from the bots: triviality judged by behavioural effect rather than line count; a stop for an unresolvable cited profile; the per-profiled-story evidence entry named in both skip summaries; getting-started no longer implying the derived mode is directly editable; the hook-scope waiver propagated into the spec non-goal and story AC 10; `process-pr-review` added to the surfaces lists; `git add -A` removed from the plan's snapshot step; and story AC 2 and AC 6 amended to the shipped rules, reasons inline. Gate B on these fixes ran 9 passes (4/1/5/3/2/2/4/1/0), clean on both branches at 9. Those passes found that the two-condition skip rule and the profiled/unprofiled evidence contract were each stated incompletely at several sites; both are now explicit at their source in §5 and consistent across the spec, plan, story, command and docs. One finding dismissed with reason: the version-bump Blocker measured the range against an intermediate commit. Invariant 12 is per-pull-request and CI compares against origin/$BASE_REF; against main the check is green, because this PR bumps 0.6.0 -> 0.7.0. Both branches later confirmed the dismissal. Verification: full battery green on the tree this commit records — shellcheck (6 files), hook tests, check-invariants + suite (123 assertions), check-version-bump + suite (36 assertions), claude plugin validate --strict; exit 0. The story is unprofiled, so no mode-derived evidence entry is owed. --- CLAUDE.md | 17 +++--- docs/coding-workflow.md | 3 +- docs/getting-started.md | 16 +++-- ...07-26-risk-security-validation-profiles.md | 60 ++++++++++++++----- ...isk-security-validation-profiles-design.md | 40 ++++++++----- ...risk-security-validation-profiles-story.md | 33 +++++++--- plugins/dev-workflow/CHANGELOG.md | 11 ++-- .../commands/process-pr-review.md | 29 +++++---- .../dev-workflow/commands/workflow-init.md | 17 +++--- 9 files changed, 152 insertions(+), 74 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index db431a3..6b1d766 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -293,13 +293,16 @@ filter, the file-first findings protocol and the clean-final-pass rule are uncha 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 keys on the effective level, never on risk alone.** For a -profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* -security `none` — and **its reason is recorded in the commit body**, beside the evidence -entry. Not in the profile log: that log records profile *changes*, and a skip changes no -profile value. **A skip removes the review, never the evidence:** the battery still runs and its entry still lands in the -commit body. An **unprofiled** story keeps exactly today's judgement-based skip and owes -no mode-derived evidence. +**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 diff --git a/docs/coding-workflow.md b/docs/coding-workflow.md index a88caad..b145332 100644 --- a/docs/coding-workflow.md +++ b/docs/coding-workflow.md @@ -127,7 +127,8 @@ 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 and the reason is recorded. **Explanatory** +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 diff --git a/docs/getting-started.md b/docs/getting-started.md index a78f3e1..0ec38c9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,7 +13,9 @@ 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, 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 +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 @@ -75,10 +77,11 @@ 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 travel lighter, within limits: what a `trivial` -profile unlocks is the **Gate-B** skip and only at effective level 0 (risk `trivial` -*and* security `none`), with the battery still owed and Gate A's floor unchanged; a -story with no profile keeps the prior judgement-based Gate-B skip. The caution bias is +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 @@ -99,7 +102,8 @@ smallest that matches your intent: 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 the skip reason in the commit body. Gate A is not + 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). 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 index 9d5c85c..de2b06a 100644 --- a/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md +++ b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md @@ -12,8 +12,10 @@ 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). No hook change, no new script, no new -scaffolded file. +§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 @@ -298,13 +300,16 @@ filter, the file-first findings protocol and the clean-final-pass rule are uncha 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 keys on the effective level, never on risk alone.** For a -profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* -security `none` — and **its reason is recorded in the commit body**, beside the evidence -entry. Not in the profile log: that log records profile *changes*, and a skip changes no -profile value. **A skip removes the review, never the evidence:** the battery still runs and its entry still lands in the -commit body. An **unprofiled** story keeps exactly today's judgement-based skip and owes -no mode-derived evidence. +**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 @@ -441,8 +446,8 @@ Replace the whole `- [ ] **P2 + P6 — risk/security profiles…**` row (lines 1 ```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 triviality skip narrowed - to effective level 0. Spec: + 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 @@ -582,7 +587,8 @@ Insert as the newest entry, matching the file's existing style (no dates): **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 to effective level 0 for profiled stories, and the author's evidence + 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 @@ -598,7 +604,15 @@ Tasks 1–2's plugin edits, so running the battery with the bump still uncommitt check for a bump that exists on disk. Snapshot first, then measure: ```bash -git add -A && git commit -m "WIP: profiles — snapshot for Gate B (0.7.0)" +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 commit -m "WIP: profiles — snapshot for Gate B (0.7.0)" git log --oneline -8 ``` @@ -666,7 +680,9 @@ uncommitted fix sits outside `baseSha..HEAD`, where Gate B would return clean on pre-fix diff while the closing commit carried unreviewed changes: ```bash -git add # never -A blindly; check `git status` first +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 @@ -675,6 +691,18 @@ git commit --amend --no-edit # folds the fix into the active WIP snapsh `--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 @@ -695,8 +723,8 @@ 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 triviality -skip to effective level 0. Unprofiled stories are unaffected. +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), 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 index 2e8d530..fc4b38a 100644 --- 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 @@ -256,13 +256,15 @@ for.** 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 the recorded reason for §5's - pre-existing Gate-B triviality skip; it creates no skip of its own. +- **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 keys on the effective level, never on risk alone.** §5's Gate-B triviality +**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` — and **its reason is recorded in the commit body**, beside the evidence -entry. Not in the profile log: that log records *profile changes*, and a skip changes no +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 @@ -270,9 +272,11 @@ obligations, `+abuse-path` among them at security `high`, and a skip keyed on ri 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 story is still mode -`battery` by derivation, so the battery still runs and its entry still lands in the commit -body before the commit. Otherwise the profile would quietly become an evidence-off path +**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 @@ -371,9 +375,11 @@ written into the spec that is supposed to prevent it. 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 and no -commit-body 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. That is what makes D2 free — nothing in flight breaks, +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 @@ -391,7 +397,11 @@ assets, trust boundaries, roles, external systems and abuse paths; a spec that h 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/skills/intake/SKILL.md` (proposal step, +**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), @@ -411,8 +421,10 @@ check that the two §5 copies say the same thing after the edit — both files c 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 (the hook counts passes and does not need to know -profiles); no new script; no new scaffolded file; no standalone security section in any +**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 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 index 62b2523..9b11ee2 100644 --- 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 @@ -21,8 +21,9 @@ generic questions as a trivial one. Nothing records what a story risks, so CLAUD 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), trivial legitimizes the documented skip -with a recorded reason. Different questions, not more identical passes. Why: profiles +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. @@ -32,9 +33,13 @@ knowing what each item risks. - [ ] 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 domain triggers — auth, permissions, payments, - migrations, data deletion, public APIs, personal data, supply chain — not by - unaided judgement. +- [ ] 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, @@ -43,7 +48,14 @@ knowing what each item risks. 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; at `trivial` the documented skip is available with its reason recorded. + 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. @@ -56,8 +68,13 @@ knowing what each item risks. 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: nothing under `plugins/dev-workflow/hooks/` changes, and no new - script is added. +- [ ] 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 diff --git a/plugins/dev-workflow/CHANGELOG.md b/plugins/dev-workflow/CHANGELOG.md index d524622..a8d4539 100644 --- a/plugins/dev-workflow/CHANGELOG.md +++ b/plugins/dev-workflow/CHANGELOG.md @@ -37,16 +37,19 @@ AGENTS.md invariant 12 carries the complete list. 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 triviality skip narrows** for a profiled story to effective level 0, and - removes the review but never the evidence. No new way to skip a gate is added. +- **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` decides the skip from the story profile**, not from the fix's - size alone, with explicit no-story, one-story and several-stories branches. +- **`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 diff --git a/plugins/dev-workflow/commands/process-pr-review.md b/plugins/dev-workflow/commands/process-pr-review.md index 181d68e..ced1a7b 100644 --- a/plugins/dev-workflow/commands/process-pr-review.md +++ b/plugins/dev-workflow/commands/process-pr-review.md @@ -142,17 +142,24 @@ be ambiguous, which is not an instruction. 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** — a one-liner, a comment, naming — the pre-existing judgement, - unchanged by profiles; **and** - - **every cited story is eligible.** Resolve the cited story path(s) first: 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; a PR citing **no** story — the - common shape for a review-fix PR — is §5's unprofiled case. With several cited - stories, each must be eligible on its own; one eligible story does not carry the rest. - - A skip removes the review and never the evidence: run the battery, and record the skip - reason in the commit body — plus one evidence entry per cited **profiled** story, and - none for an unprofiled one. + - 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 435b935..497d1d5 100644 --- a/plugins/dev-workflow/commands/workflow-init.md +++ b/plugins/dev-workflow/commands/workflow-init.md @@ -468,13 +468,16 @@ filter, the file-first findings protocol and the clean-final-pass rule are uncha 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 keys on the effective level, never on risk alone.** For a -profiled story it is available only when `max(risk, security)` is 0 — risk `trivial` *and* -security `none` — and **its reason is recorded in the commit body**, beside the evidence -entry. Not in the profile log: that log records profile *changes*, and a skip changes no -profile value. **A skip removes the review, never the evidence:** the battery still runs and its entry still lands in the -commit body. An **unprofiled** story keeps exactly today's judgement-based skip and owes -no mode-derived evidence. +**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 From 9531f6b3a8e5572700f9001c65cacfbedbffc6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:04:55 +0200 Subject: [PATCH 8/8] docs(plan): guard the initial snapshot staging against pre-existing dirt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up finding from CodeRabbit on the fix head: Task 5 Step 3 staged its explicit path list wholesale, folding any pre-existing edits in those files into the WIP snapshot. Step 6's fix loop gained that guard at Gate-B pass 7; the initial snapshot never did. Step 3 now states the same rule — if a listed path was already dirty before the cycle, stage only this cycle's hunks or stop and ask — and verifies with git diff --cached --name-only and git diff --cached before committing. Gate B: clean on both branches at pass 1, the documented early exit for a zero-finding pass. check-invariants green. --- .../plans/2026-07-26-risk-security-validation-profiles.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 index de2b06a..c78c9ad 100644 --- a/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md +++ b/docs/superpowers/plans/2026-07-26-risk-security-validation-profiles.md @@ -601,7 +601,10 @@ Insert as the newest entry, matching the file's existing style (no dates): `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: +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 @@ -612,6 +615,8 @@ git add plugins/dev-workflow/skills/intake/SKILL.md \ 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 ```