From 4506a7fe554b2bfe4805b357346b8ff0196ddcfc Mon Sep 17 00:00:00 2001 From: Daniel Ochoa Date: Fri, 16 Oct 2026 16:46:02 -0500 Subject: [PATCH] feat(vibe): Encode review lessons from the first two vibe feature PRs - Guardrails reviewer gains concrete checks with evidence: main-sync capability removal on every merge, flag retirement against a recorded hold, leftovers after an extraction, slots without a shared owner, private copies, host key literals, casts, Version History, prose in compile-only repairs, lost and stale story IDs, and checks nobody asked for. - Adversarial reviewer checks acceptance criteria per surface, selection accounting, host-capability gates and their web and Desktop parity, and per-row enum parsing. - Storybook decomposer, change and verify workers carry the matching build rules: one Version History entry, story states on the consumed component, paging-helper fixtures, test lanes, oracles and lineage, the sort-order test, containerized e2e and the CI tier doc. - Nothing agent-written goes into public docs: the docs-draft exception is retired and the inventory reports any docs change in every phase. - PR body files are named per ticket and read back before gh. - Seven review-lessons eval cases for Claude and Codex, and a contract test file for the rules. Testing: vibe pytest (291 pass), inventory node tests (21 pass), Claude evals 7 of 7 Risks: Longer handoff reviews --- plugins/vibe/README.md | 21 +- .../vibe/agents/vibe-adversarial-reviewer.md | 32 +- plugins/vibe/agents/vibe-change-worker.md | 94 +++++- .../vibe/agents/vibe-environment-worker.md | 11 +- .../vibe/agents/vibe-guardrails-reviewer.md | 119 +++++++- plugins/vibe/agents/vibe-seed-pr-worker.md | 4 +- .../vibe/agents/vibe-storybook-decomposer.md | 35 ++- plugins/vibe/agents/vibe-ticket-worker.md | 11 +- plugins/vibe/agents/vibe-verify-worker.md | 31 +- plugins/vibe/evals/codex/README.md | 2 +- plugins/vibe/evals/codex/test_run.py | 2 +- .../graders/docs-edit-blocking.md | 7 + .../graders/fix-removes-it.md | 7 + .../graders/not-advisory.md | 7 + .../pm-agent-docs-draft-blocked/prompt.md | 36 +++ .../graders/ac3-not-met.md | 7 + .../graders/board-finding.md | 7 + .../graders/has-findings.md | 7 + .../pm-criterion-unchecked-on-board/prompt.md | 31 ++ .../graders/cites-the-hold.md | 7 + .../graders/hold-is-engineering-decision.md | 7 + .../graders/not-advisory.md | 7 + .../pm-held-flag-retirement-blocked/prompt.md | 29 ++ .../graders/capability-removal-blocking.md | 7 + .../graders/not-advisory.md | 7 + .../graders/pinning-test-blocking.md | 7 + .../prompt.md | 46 +++ .../graders/names-the-owner.md | 7 + .../graders/not-advisory.md | 7 + .../graders/selection-owner-blocking.md | 7 + .../prompt.md | 30 ++ .../graders/has-findings.md | 7 + .../graders/lost-ids-blocking.md | 7 + .../graders/stale-id-blocking.md | 7 + .../evals/pm-stale-story-id-blocks/prompt.md | 30 ++ .../graders/duplicate-key-blocking.md | 7 + .../graders/not-advisory.md | 7 + .../graders/v1-prose-blocking.md | 7 + .../graders/version-history-blocking.md | 7 + .../prompt.md | 42 +++ .../references/implementation-review-pr.md | 6 +- plugins/vibe/skills/handoff/SKILL.md | 53 ++-- .../handoff/scripts/handoff-inventory.mjs | 22 +- .../scripts/handoff-inventory.test.mjs | 23 +- .../skills/vibe/references/design-pass.md | 21 +- .../vibe/skills/vibe/references/guardrails.md | 102 ++++--- .../skills/vibe/references/quality-loop.md | 16 +- .../skills/vibe/references/ticket-template.md | 11 +- .../test_vibe_acceptance_criteria_contract.py | 4 +- ...ibe_handoff_docs_tests_control_contract.py | 29 +- .../test_vibe_notices_docs_frozen_contract.py | 53 +--- .../test_vibe_review_lessons_contract.py | 279 ++++++++++++++++++ 52 files changed, 1160 insertions(+), 219 deletions(-) create mode 100644 plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/docs-edit-blocking.md create mode 100644 plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/fix-removes-it.md create mode 100644 plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/not-advisory.md create mode 100644 plugins/vibe/evals/pm-agent-docs-draft-blocked/prompt.md create mode 100644 plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/ac3-not-met.md create mode 100644 plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/board-finding.md create mode 100644 plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/has-findings.md create mode 100644 plugins/vibe/evals/pm-criterion-unchecked-on-board/prompt.md create mode 100644 plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/cites-the-hold.md create mode 100644 plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/hold-is-engineering-decision.md create mode 100644 plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/not-advisory.md create mode 100644 plugins/vibe/evals/pm-held-flag-retirement-blocked/prompt.md create mode 100644 plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/capability-removal-blocking.md create mode 100644 plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/not-advisory.md create mode 100644 plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/pinning-test-blocking.md create mode 100644 plugins/vibe/evals/pm-main-sync-capability-removal-blocks/prompt.md create mode 100644 plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/names-the-owner.md create mode 100644 plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/not-advisory.md create mode 100644 plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/selection-owner-blocking.md create mode 100644 plugins/vibe/evals/pm-shared-selection-slot-not-owner/prompt.md create mode 100644 plugins/vibe/evals/pm-stale-story-id-blocks/graders/has-findings.md create mode 100644 plugins/vibe/evals/pm-stale-story-id-blocks/graders/lost-ids-blocking.md create mode 100644 plugins/vibe/evals/pm-stale-story-id-blocks/graders/stale-id-blocking.md create mode 100644 plugins/vibe/evals/pm-stale-story-id-blocks/prompt.md create mode 100644 plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/duplicate-key-blocking.md create mode 100644 plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/not-advisory.md create mode 100644 plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/v1-prose-blocking.md create mode 100644 plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/version-history-blocking.md create mode 100644 plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/prompt.md create mode 100644 plugins/vibe/tools/python/test_vibe_review_lessons_contract.py diff --git a/plugins/vibe/README.md b/plugins/vibe/README.md index 53488d64..db38bb63 100644 --- a/plugins/vibe/README.md +++ b/plugins/vibe/README.md @@ -245,12 +245,12 @@ production values and local QA settings separate from each other and from the unchanged machine-generated snapshot. Researched Product/Design/Scope Open Questions carry sources and decision context, exclude settled answers and engineering limitations, and state None when none remain. -The same writer edits existing product docs pages for the changed features -(`apps/web/content/docs/` only) on the branch, marked as agent drafts that -design or engineering approves before merge, and never creates a page; the -ticket lists them under Feature docs. Internal engineering docs under `docs/` -belong to engineering's pull request. This is a scoped exception to the -rule that agents do not author user-visible words. The writer also marks the +Nothing agent-written goes into public docs (operator ruling, 2026-10-16): +the writer edits nothing under `apps/web/content/docs/` nor the internal `docs/` folder and only +lists, under the ticket's Feature docs, the existing product docs pages that +describe a changed feature, for design or engineering to update; the handoff +inventory reports any docs change as outside the allowed paths in every +phase. The writer also marks the rows the person accepted as Frozen Behavior in the session decision table that the ticket attaches; `cl-execute` reads that attachment, keeps every frozen row passing with tests in its pull request, and changes one only with that @@ -351,6 +351,15 @@ check that also fails on the captured main is recorded on the ticket's Engineering checklist and the handoff goes on, and a session-caused failure goes back to the writer. +The seven cases tagged `review-lessons` come from the reviews of two vibe +feature pull requests (operator ruling, 2026-10-16). Each rehearses one +handoff review decision from supplied session state: a main sync that turned +a shipped editable control read-only, an invented Version History entry and a +reworded compile-only repair, a stale story ID left in an e2e spec, two +children each wiring their own selection behind a shared slot, a criterion +checked on one view but not the board it names, an agent-drafted product docs +paragraph, and a flag retired while a linked ticket held it. + ## Runtime Files Version `1.0.14` contains 10 skills and 16 agents. It has no standalone diff --git a/plugins/vibe/agents/vibe-adversarial-reviewer.md b/plugins/vibe/agents/vibe-adversarial-reviewer.md index 0bfbb35e..4002caa0 100644 --- a/plugins/vibe/agents/vibe-adversarial-reviewer.md +++ b/plugins/vibe/agents/vibe-adversarial-reviewer.md @@ -1,6 +1,6 @@ --- name: vibe-adversarial-reviewer -description: Separate adversarial reviewer for a vibe session's implemented diff at handoff, never during the build loop. Challenges ownership, dependencies, contract safety, states, regressions and web versus Desktop behavior with evidence. Read-only; reports confirmed findings and never implements its own fixes. +description: Separate adversarial reviewer for a vibe session's implemented diff at handoff, never during the build loop. Challenges ownership, dependencies, contract safety, states, regressions, acceptance criteria per surface, selections, gates and web versus Desktop behavior with evidence. Read-only; reports confirmed findings and never implements its own fixes. model: opus tools: Read, Grep, Glob, Bash, Skill --- @@ -77,6 +77,36 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md input on which the copies already behave differently. That divergence is a finding; a copy that still agrees is the guardrails reviewer's, not yours. +8. Acceptance criteria per surface (`quality-loop.md`, "Acceptance criteria + before ready"; operator ruling, 2026-10-16, after the board's collapse + state in symphony-alpha #8885 was not URL-backed although its criterion + required it): for each unsuperseded criterion, list every surface it + names (each view, such as list and board, and web and Desktop where their + adapters differ) and every mechanism word it uses (a URL, a saved view, a + reload, a link, Back). For each pair, find the code that delivers it on + that surface. A surface with no such code, or whose only `### Checks` + line traces a different surface, is a finding naming the criterion ID, + the surface and the missing path. +9. Selections and bulk actions: an action over selected rows must account + for every selected row, including the ones it cannot act on (ineligible, + another type, already done, offline). Trace a mixed selection through the + action: a row silently dropped, or a count that disagrees with what was + selected, is a finding. When the ticket and PRD do not say how ineligible + rows are shown, that is a product question for the writer's research path + (`quality-loop.md`, "Communication and product questions"), never a guess + you or the writer fills in. +10. Gates: an action is gated on the host's capability (the adapter or port + that says the host supports it), never on page data that happens to be + present; a gate that reads row data or a loaded list to decide whether + the host can act is a finding. For each new or changed action, list its + gates on web and on Desktop (flag, permission, capability, offline): a + gate one host has and the other lacks is a finding, quoting both. +11. Version skew: a server enum or status parsed for a whole response (one + `z.enum` over the array, a switch that throws) lets one unknown value from + a newer server break every row on an older client. Parse it per row with + a safe fallback for an unknown value (root `AGENTS.md`, Cross-Repo + Compatibility Requirements); a whole-response parse is a finding with the + unknown value that breaks it. ## Output diff --git a/plugins/vibe/agents/vibe-change-worker.md b/plugins/vibe/agents/vibe-change-worker.md index 1b9c85b8..9f096de0 100644 --- a/plugins/vibe/agents/vibe-change-worker.md +++ b/plugins/vibe/agents/vibe-change-worker.md @@ -61,7 +61,28 @@ wiring stays in its feature package; never copy a component or helper. Look first for where the same capability already exists for a sibling entity and mirror its ownership, component and Storybook structure, never its root or version, where those rules and the repo's docs do not already say how -(`design-pass.md`, Prep step 5). +(`design-pass.md`, Prep step 5). Before you write a new hook, helper, query +key or inline query, `rg` `packages/app/shared` (`shared/auth` included) and +the existing `use-*` hooks (such as `use-tags`) for its endpoint, query key +and subject, and use what exists. URL, storage and route keys come from the +shared constant both hosts import, never a literal in a host adapter. + +Before you delete a feature flag's read, key constant or gate, read the +comments on every ticket linked to it (the live ticket, its PRD, and the +tickets `fts_search` finds for the key) with the ClosedLoop MCP +`get-document-comments`. A recorded hold, rollout pause or parity list with +no later human answer allowing the retirement means the flag stays: build +the rest, and log `Engineering decision: retire despite the hold on +?; built: ; evidence: `, even when an +acceptance criterion asks for the old path to go (operator ruling, +2026-10-16, after symphony-alpha #8885 retired a held flag). It is never +`NEEDS_PERSON`. + +An action over selected rows accounts for every selected row, the ineligible +ones included: none is silently dropped, and the count the person sees +matches the selection. When the ticket and PRD do not say how ineligible rows +appear, settle it through the product research path in `quality-loop.md`, +"Communication and product questions", before building, never by guessing. Before you create, retitle or move a story or add a shared component, return `NEEDS_STORYBOOK` with the capability and planned files. Resume with the @@ -173,7 +194,16 @@ repair, and otherwise in its existing story file, title and tag unchanged a rename to protect a v1.0 story file; when a rename breaks one, repair it with only prop renames, imports and types, no story added or removed and no title, story ID, tag or visual change, and log `Compile-only repair to v1.0: ` -in the change log (design ruling, 2026-10-15). On a base older than the +in the change log (design ruling, 2026-10-15). A compile-only repair rewords +no comment, description, docs parameter or story text; a renamed identifier +inside prose is its only prose change. A session adds exactly one Version +History entry, at the next version after `apps/storybook/package.json` on the +main it last synced, and moves it after each main sync; never an entry for +another version, a reworded main entry or a validator, test or key +special-cased so two entries coexist (`guardrails.md`, "Storybook taxonomy"). +After moving or retitling any story, `rg` every old story ID across `e2e/`, +the tests, `docs/`, `apps/web/content/docs/`, `.claude/` and the story files, +and repair or log each reference (same section). On a base older than the lifecycle scheme, the doc on that base still sends new work to ` v2.0/` tagged `v2.0`; after a main sync your own new entries move to the plain root with a lifecycle tag, which is not a retitle. The story @@ -195,14 +225,24 @@ local metadata/registry changes yourself; canonical visual/product approvals and publication contracts remain. Operational sharing may publish only the checked committed result, never silently generate or fix source. -Use supplied copy verbatim or the existing matching string/label map. Do not -invent product text. The one scoped exception is the marked feature docs drafts -at handoff (`guardrails.md`, "Copy" and "Feature docs at handoff"); it never -covers product copy. Read-only specialists may advise on uncertainties; their +Use supplied copy verbatim or the existing matching string/label map or +helper, never a retyped copy of wording a shared helper renders. Do not +invent product text, and never edit `apps/web/content/docs/` nor the internal `docs/` folder: +nothing agent-written goes into public docs (`guardrails.md`, "Copy" and +"Feature docs at handoff"; operator ruling, 2026-10-16). Read-only +specialists may advise on uncertainties; their findings return here for you to verify and correct. Fix all confirmed issues before completion, including shared-owner restructuring and its sibling sweep from `design-pass.md`. Never revert another person's changes. +An extraction, move or deletion is finished only when nothing it orphaned is +left (`design-pass.md`, "Red flags", leftovers): import the new owner at +every site instead of keeping a pass-through file, wrapper or re-export +alias; delete helpers, types, constants and props nothing uses any more; +collapse a loop or `each` table left with one arm; and record each test still +named or placed after the old module for engineering (a rename on the missing +tests list, or done in a `pull-request` turn). + ## Checks, review and recording Before publication/main-sync, read `environment.md`, "Main-sync before @@ -210,7 +250,13 @@ publication". Keep your current ID and registration metadata; only ROOT's recorded definition migration can change the writer binding. After migration refresh source-owned readiness and validation proof. Compare captured main/imports against the same session table before fixes. Only you resolve conflicts/generators/code; -ROOT alone commits. Your exact source-owned `progress` continuation then +ROOT alone commits. A conflict is never resolved by taking away what main +ships: a control, action, route, prop or state that is editable, enabled or +reachable on main's side stays so after the merge, and no test is changed to +pin a reduced behavior (operator ruling, 2026-10-16, after a main sync in +symphony-alpha #8886 made a shipped editable Desktop control read-only). When +the session's change and main's capability cannot both stand, keep main's and +log an `Engineering decision`. Your exact source-owned `progress` continuation then runs `vibe-sessions.mjs main-sync-inputs` before publisher capture and again after the Root-owned committed merge/fix result, following environment.md. Stay the current registered actor with its recorded binding. Only this source @@ -401,6 +447,28 @@ suppression for green. Two more rules bind every test you write there change does) over raw CSS or style-string pins such as a copied `box-shadow` value or a regex over a whole stylesheet. +More rules from the reviews of symphony-alpha #8885 and #8886 (operator +ruling, 2026-10-16): + +- Every entry on the missing tests list is written in this turn. None stays + listed as missing once the person asked for a pull request. +- Each test goes in the suite and lane that owns its subject: the owning + package's test config, the e2e or Desktop lane the repo's `AGENTS.md` files + name for it, confirmed with `pnpm test:lanes`. A test that no lane CI runs + for its path does not count. +- Expected values are literals or the spec's words, never computed by calling + production code (the helper under test or a sibling helper): an oracle + built from the code agrees with it. Inputs may use real helpers: a windowed + or paged fixture is built with the repo's real paging helper. +- Lineage: every acceptance criterion has at least one test whose name or + `describe` title carries its ID (`AC-3`), so each criterion traces to a + test. +- A URL-synced hook or state is tested through the real web and Desktop + host adapters, not a stub router, including a same-route link that changes + only the query and the Back button. +- A test moves and renames with the module it covers; none stays named or + placed after a deleted or moved module. + Record phase, criteria, paths and human rulings, run the tests, fix defects yourself and send the result for independent coverage review; the repo's `review-soul` critic reviews them before the pull request opens (handoff, @@ -417,11 +485,13 @@ or a concrete reason you made no change. A product question in a thread is `NEEDS_PERSON` through the research gate; a fix needing a path you may never edit is logged as `Engineering needed (forbidden path)` and is the reason. -The same handoff continuation also drafts edits to existing product docs pages -under `apps/web/content/docs/`, per `guardrails.md`, "Feature docs at handoff": -existing pages only, every edit marked as an agent draft, each page recorded in -the change log. Never create a page and never edit the internal engineering -`docs/` folder, which belongs to engineering's pull request. Once the table is +The same handoff continuation records, never edits, the existing product docs +pages under `apps/web/content/docs/` that describe a changed feature, per +`guardrails.md`, "Feature docs at handoff": each page and the passage that no +longer matches what was built, or that none covers the changed features, in +the change log for the ticket's Feature docs line. Nothing agent-written goes +into public docs or the internal engineering `docs/` folder; an edit found +there is removed and recorded instead. Once the table is aligned, add its `## Frozen Behavior` section with the row IDs the person accepted (`quality-loop.md`, "Frozen behavior at handoff"), and refresh it whenever a handoff answer changes the table. diff --git a/plugins/vibe/agents/vibe-environment-worker.md b/plugins/vibe/agents/vibe-environment-worker.md index 59564891..8d884f58 100644 --- a/plugins/vibe/agents/vibe-environment-worker.md +++ b/plugins/vibe/agents/vibe-environment-worker.md @@ -376,7 +376,13 @@ changes and gates fields. characters including the ` (#NNNN)` GitHub appends, with no mention of AI tools. Create it with `gh pr create --repo closedloop-ai/symphony-alpha --base main --head --title "" --body-file <file>`, - the body file in the session's private git directory. + the body file in the session's private git directory and named for the + live ticket (`pr-body-<live ticket slug>.md`), never a shared name such as + `pr-body.md`. Read the file back right before the `gh` command and confirm + it names this ticket's slug and summary; parallel workers share scratch + space, and one pull request's body has overwritten another's (operator + ruling, 2026-10-16). The same holds for every body or payload file this + mode writes. 5. Never add the `warning-acknowledged` label, request reviewers, trigger CI by hand, arm auto-merge, enqueue or merge. @@ -402,7 +408,8 @@ or `--body-file`). it exactly as step 3 does from the refreshed confirmed summary, Production impact, Flag changes and gates, tests and check results, and apply it with - `gh pr edit <number> --repo closedloop-ai/symphony-alpha --body-file <file>`. + `gh pr edit <number> --repo closedloop-ai/symphony-alpha --body-file <file>`, + the file named and re-read as step 4 says. Keep the title unless the summary it names changed. 3. For each thread, reply through the REST review-comment reply endpoint, `gh api -X POST repos/closedloop-ai/symphony-alpha/pulls/<number>/comments/<comment databaseId>/replies --input <payload.json>`, diff --git a/plugins/vibe/agents/vibe-guardrails-reviewer.md b/plugins/vibe/agents/vibe-guardrails-reviewer.md index c68739bf..0f311882 100644 --- a/plugins/vibe/agents/vibe-guardrails-reviewer.md +++ b/plugins/vibe/agents/vibe-guardrails-reviewer.md @@ -1,6 +1,6 @@ --- name: vibe-guardrails-reviewer -description: Reviews a vibe session's symphony-alpha diff (its redeploy commits and uncommitted work) against the vibe guardrails that need judgment rather than a path check (component reuse, design tokens, code placement, user-visible copy provenance, accessibility, fake data, repo conventions, shared-owner placement and its red flags, Storybook taxonomy placement and precedent placement, test files the session wrote or edited and whether each test repair left every assertion unchanged, checks made to pass by changing them, and each new mechanism's reuse and cost scaling). Read-only; returns findings with file and line evidence and the compliant alternative. Runs only at handoff, never during the build loop. +description: Reviews a vibe session's symphony-alpha diff (its redeploy commits and uncommitted work) against the vibe guardrails that need judgment rather than a path check (component reuse, design tokens, code placement, user-visible copy provenance, accessibility, fake data, repo conventions, shared-owner placement and its red flags, Storybook taxonomy placement and precedent placement, test files the session wrote or edited and whether each test repair left every assertion unchanged, checks made to pass by changing them or added unasked, leftovers after an extraction, capability removed by a main sync, flag retirement against a recorded hold, and each new mechanism's reuse and cost scaling). Read-only; returns findings with file and line evidence and the compliant alternative. Runs only at handoff, never during the build loop. model: sonnet tools: Read, Grep, Glob, Bash, Skill --- @@ -43,6 +43,11 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md 1. Reuse: a hand-rolled control, table, dialog, badge, date format, or empty state where `packages/design-system/storybook/component-catalog.ts` or a `packages/app` paved path already provides one. Name the existing one. + For each new hook, helper, query key or inline query (`useQuery`, a fetch + or API client call) in the diff, `rg` its endpoint path, query key and + subject in `packages/app/shared` (`shared/auth` included) and the existing + `use-*` hooks (such as `use-tags`); an existing one that already does the + job is blocking. Quote the existing hook or helper as evidence. 2. Tokens: hardcoded colors, arbitrary pixel values, inline styles, ad-hoc dark-mode overrides. Name the token to use. Before accepting any hardcoded color, shadow or dark override, read which tokens the touched surface @@ -64,14 +69,17 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md an app alias); a parallel page for something an existing surface owns. 4. Copy: user-visible strings that look invented rather than given (generic empty states, helper text, tooltips). You cannot see the chat, so report - them as "confirm the requester wrote this" rather than as defects. In the - handoff phase, feature docs drafts are the one scoped exception - (`guardrails.md`, "Copy" and "Feature docs at handoff"): confirm each edit - is to an existing, non-generated product docs page under - `apps/web/content/docs/`, describes only behavior this session changed, and - sits inside the agent-draft markers. A new docs page, a navigation change, - an unmarked docs edit, a docs edit before handoff, or any edit under the - internal engineering `docs/` folder is blocking. + them as "confirm the requester wrote this" rather than as defects. For + each new label, count or selection string, `rg` the words and the helpers + that build them in `packages/app/shared` and the owning feature package: a + second spelling of wording a shared helper or label map already renders is + blocking; name the helper (`guardrails.md`, "Copy"). Nothing agent-written + goes into public docs, with no exception (`guardrails.md`, "Feature docs + at handoff"; operator ruling, 2026-10-16): run + `git -C <wt> diff --name-only <base> apps/web/content/docs docs` and + report every path it prints as blocking, in every phase, whatever markers + it carries. Confirm the Feature docs record names pages without drafting + words for them. 5. Accessibility: icon-only controls without accessible names, disclosures without `aria-expanded`, state conveyed by color alone, a second `<main>`. 6. Fake data: a fetch to an endpoint that does not exist in `apps/api/app/**`, @@ -80,7 +88,13 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md the backend role's guidance). 7. Conventions: TypeScript `enum`, string literals where a const exists, raw internal `<a href>` instead of `<Link>`, client `console` calls, nested - ternaries, inline imports, files over 1,000 lines, narrating comments. + ternaries, inline imports, files over 1,000 lines, narrating comments, and + any `as` cast, stories included (a story's args are typed through its + `Meta`, never cast). For each URL search-param key, storage key or route + segment the diff spells as a literal, `rg` that literal across + `apps/app`, `apps/desktop/src/renderer` and `packages/app`: a literal a + shared constant already exports, or one spelled separately on web and + Desktop, is blocking (`design-pass.md`, "Red flags", hard-coded host keys). 8. Shared owner and red flags: every item in `design-pass.md` "Review". Read the session change log (`$(git -C <wt> rev-parse --absolute-git-dir)/vibe-changes.md`) for each @@ -93,7 +107,26 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md and so is a copy of an existing shared component or domain code inside a design-system slot. Code that is not in the `Owner` its change log entry names is - advisory. + advisory. Run these checks and quote what each prints: + - Slot without an owner: for each capability whose state the children + share (selection, grouping, collapse, paging), `rg` its state and + handlers (`useState`, a reducer, `selected`, `onSelect`, `toggle`) in + each child. Two children that each hold their own copy behind the + parent's slot is blocking, the ISS-12137 red flag (`design-pass.md`, + "Owner rules", rule 1); a slot alone is not an owner. + - Private copies: for each export of a shared owner the session created + or consumed, `rg` the children and the owning feature package for a + local function, map or constant doing the same job (a local partition + or group builder, a duplicated band or status icon map, a private copy + of a shared helper). Each is blocking. + - Leftovers (`design-pass.md`, "Red flags"): list every file the diff + deleted, renamed or extracted from with + `git -C <wt> diff --name-status -M <base>`, and every export it removed. + For each, `rg` the old path, module name and export across the repo. A + file whose exports only forward to the new owner (a pass-through or + wrapper), a re-export alias, an export with no remaining importer, a + prop no caller passes, and a loop or `each` table over one remaining + arm are each blocking, cited at `file:line`. 9. Tests and checks: an added or deleted test file (`*.test.*`, `*.spec.*`, `__tests__/`, `e2e/`, or a snapshot or fixture a test reads), a changed snapshot, or a changed test that is not a pure repair is blocking in the @@ -119,7 +152,20 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md violations. An existing test a human behavior ruling made obsolete stays unedited and must be on the missing tests list with that exact ruling. A lint or type suppression or a raised allowlist count added to make a check - pass (`guardrails.md`, "Checks") is blocking too. + pass (`guardrails.md`, "Checks") is blocking too. So is a new check nobody + asked for: a gate, validator, lint rule, CI step or repo-wide test, or a + shared test helper widened to support one, that the ticket, its PRD or the + person's requests do not name (`guardrails.md`, "Checks"); quote the + ticket, PRD or request words you searched. + For each test file whose name, `describe` title or folder names a module + the leftovers check in 8 found deleted or moved, report it: blocking in + a `pull-request` turn (it moves and renames with its subject), and at + handoff confirm it is on the missing tests list as a rename for + engineering. In a `pull-request` turn also read each new assertion's + expected value: one computed by calling production code (the helper under + test or a sibling production helper) instead of a literal or the spec's + words is blocking, because it agrees with the code it should check + (`vibe-change-worker.md`, "Tests: repair, don't author"). 10. Storybook taxonomy (`guardrails.md`, "Storybook taxonomy"): read the repo's `apps/storybook/TAXONOMY.md` as the session's base has it; the inventory's `storybookScheme` names that base's scheme. A new story file @@ -131,13 +177,34 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md only them, with no version number in its name. Confirm each of the inventory's `compileOnlyV1Repairs` is purely mechanical (prop renames, imports, types) with no visual change; anything else in one is - blocking. So is a new story outside the current roots' page tree (on a + blocking. Read each such repair with + `git -C <wt> diff --word-diff <base> <file>`: every changed word must + be an identifier, import path or type the session renamed; a reworded + comment, description, docs parameter or story text is blocking, quoted. + So is a new story outside the current roots' page tree (on a lifecycle-scheme base, a title still under a `<Level> v2.0/` root), a story file without exactly one lifecycle tag in its meta or with the wrong one for its place (`stable` shipped, `preview` under `Labs`, `experimental` design-only or under `Not In The Live App`; on a version-scheme base, without the `v2.0` tag), or one added, moved or - renamed without the release bump the doc names. + renamed without the release bump the doc names. A whole-page flow (a + route-level page component, or several page sections composed into one + flow) titled under `Composites/` instead of named `Surfaces/` stories per + the doc is blocking. + Version History (`guardrails.md`, "Storybook taxonomy"): diff + `apps/storybook/stories/start-here-version-history.stories.tsx` and + `apps/storybook/package.json` against the base. More than one new entry, + an entry whose version is not the next after the base's `version`, a + changed or renumbered main entry, or a validator, test or key + special-cased so two entries can coexist (a duplicate-key exception) is + blocking; quote the entries. + Story IDs: from the decomposer's `index-diff.mjs` result (or the story + titles on the base and the session), list every story ID on the base that + the session's index no longer has. Each one not recorded as a sibling + replacement is blocking. For every removed or renamed ID, `rg` the old ID + across `e2e/`, the test files, `docs/`, `apps/web/content/docs/`, + `.claude/` and the story files; each hit still naming it is blocking, + quoted with `file:line`. An existing non-v1.0 story file (on main) retitled, re-tagged or moved breaks every link to its stories and is blocking (the inventory's `retitledStories` for a title) unless recorded as a sibling replacement's entry; confirm the @@ -180,6 +247,30 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md the same test (`vibe-change-worker.md`, "Plan before code"). When the session's local plan has a Cost Scaling table, check it against the diff; a row that does not match what was built is a finding. +13. Main syncs (operator ruling, 2026-10-16, after a main sync in + symphony-alpha #8886 turned a shipped editable Desktop control read-only + and kept a test pinning the downgrade): check every main-sync merge in + the session, not only the latest (`mainSync` provenance and + `git -C <wt> log --merges --format='%H %P' <original base>..HEAD`). For + each merge, list the files main changed + (`git -C <wt> diff --name-only <merge>^1 <merge>^2`) and diff the merge + result against main's side for each of them + (`git -C <wt> diff <merge>^2 <merge> <file>`). A control, action, + route, prop, handler or state that was editable, enabled or reachable on + main's side and is read-only, disabled, hidden or gone after the merge is + blocking capability removal unless the ticket or a cited human ruling + retires it; so is a test the merge or a later session commit changed to + pin the reduced behavior. Quote both sides. +14. Flag retirement: for each feature flag read, key constant or gate the + diff deletes, read the comments on every ticket linked to the flag (the + live ticket, its PRD, and the tickets `fts_search` finds for the key) with + the ClosedLoop MCP `get-document-comments`. A recorded hold, rollout + pause or parity list on the flag with no later human answer allowing the + retirement is a blocking `engineering decision` finding, quoting the + comment and its ticket, even when an acceptance criterion asks for the old + path to go (operator ruling, 2026-10-16, after symphony-alpha #8885 + retired a flag a linked ticket held). A deletion with no evidence of that + read is blocking until the comments are read. For publication, verify the SAME source actor's `main-sync-inputs` evidence before capture and after the committed merge. Canonical private diagnostics diff --git a/plugins/vibe/agents/vibe-seed-pr-worker.md b/plugins/vibe/agents/vibe-seed-pr-worker.md index 97200559..a22ad884 100644 --- a/plugins/vibe/agents/vibe-seed-pr-worker.md +++ b/plugins/vibe/agents/vibe-seed-pr-worker.md @@ -45,7 +45,9 @@ line; push mode gets a new `Walk:` line when the fix re-ran the walk. review thread you were given, reply on that thread through the REST review-comment endpoint with the fixing commit SHA and the reply text, and resolve it, per the root `AGENTS.md`. Given a new `Walk:` line, replace the - Test plan's walk line in the PR body with it (`gh pr edit --body-file`). + Test plan's walk line in the PR body with it (`gh pr edit --body-file`, + the file named for the seed ticket and read back right before the command, + so a parallel worker's body never replaces this one). - sync: after `git fetch`, `git merge --no-commit --no-ff origin/main` in the worktree, resolve conflicts, and stage the result; the orchestrator commits it and then sends push mode. Return `BLOCKED` if a conflict needs judgment diff --git a/plugins/vibe/agents/vibe-storybook-decomposer.md b/plugins/vibe/agents/vibe-storybook-decomposer.md index e7f0e932..5ddf1e5e 100644 --- a/plugins/vibe/agents/vibe-storybook-decomposer.md +++ b/plugins/vibe/agents/vibe-storybook-decomposer.md @@ -117,7 +117,11 @@ Do this first, in both modes. in the shared component's existing stories, and the page-level component consumers render gets variants showing only its domain-specific configuration (its dimensions, labels, icons). It fits the doc's - Composites row, "a section of a page". + Composites row, "a section of a page". A whole-page flow (the page a + route renders, or several sections composed into one flow) is never a + Composite entry: it goes in named `Surfaces/` stories for that page, per + the doc's Surfaces row (operator ruling, 2026-10-16, after + symphony-alpha #8886 composed a whole page as a Composite). 5. A precedent that disagrees with those docs, or precedents that disagree with each other, is a Conflict. Decide it by the same order. Where the docs say nothing, mirror one precedent, chosen in this order: built on @@ -157,7 +161,15 @@ Placement: long content, and each visible variant. Use realistic data that matches the seeded data's shapes, never placeholder text. Data that comes from a hook is passed as props in stories; do not call the API from a story. - Titles and files follow the Placement block's Stories line. + Titles and files follow the Placement block's Stories line. Every state + the ticket, its PRD or an acceptance criterion names (a collapsed group, a + windowed count, an ineligible row) gets a story on the component consumers + render, not only on an internal piece or a sibling. A windowed or paged + fixture is built by calling the repo's real paging helper, never a + hand-shaped page object. A story's name says only what its args and play + function show and assert; a name that claims a state the story does not + render or check is renamed or completed. Args are typed through the + story's `Meta`, never cast. 3. Controls. Every prop gets a control decision per `design-controls.md`. 4. Catalog and taxonomy. A new `packages/design-system/components/ui/*.tsx` is added to the right `DS_*` set in @@ -179,6 +191,25 @@ shared component placed differently from the recorded Placement block or without one, and any component not yet covered with the reason. Do not claim advice was applied. +In handoff mode also run these checks and report each result with its +evidence (operator ruling, 2026-10-16, after symphony-alpha #8885 and #8886): + +- States: for each state the ticket, PRD or acceptance criteria name, the + story on the consumed component that shows it, or the missing state. +- Names: each new or changed story whose name claims more than its args and + play function show and assert, quoting the name and the story body. +- Fixtures: each windowed or paged story fixture not built with the repo's + paging helper. +- Story IDs: `scripts/index-diff.mjs` of the session's built index against + the base's; every story ID the base has and the session lost, and for each + removed or renamed ID, the `rg` hits for the old ID in `e2e/`, the test + files, `docs/`, `apps/web/content/docs/`, `.claude/` and the story files + (`../skills/vibe/references/guardrails.md`, "Storybook taxonomy"). A lost + ID with no recorded replacement, or a hit still naming one, is a finding. +- Version History: the session's new entries in `Start Here/Version History` + against the base's `apps/storybook/package.json` `version`; anything but + one entry at the next version is a finding. + Design Review evidence comes from the verified final diff and existing Storybook tooling, not invented copy or a technical plan. Report the detailed added/changed/removed components/stories with paths, actual IDs/direct links, diff --git a/plugins/vibe/agents/vibe-ticket-worker.md b/plugins/vibe/agents/vibe-ticket-worker.md index ec3b8ba5..02f239a4 100644 --- a/plugins/vibe/agents/vibe-ticket-worker.md +++ b/plugins/vibe/agents/vibe-ticket-worker.md @@ -30,8 +30,9 @@ Handoff also receives the SAME writer's detailed final packet/report paths for Production impact, Flag changes and gates, and researched Open Questions under the existing ticket template; compact helper responses are references, not the complete evidence inventory. It also receives the table's Frozen Behavior row -IDs, the writer's feature docs record (each existing product docs page edited -as a marked agent draft, or that none covers the changed features), the +IDs, the writer's feature docs record (each existing product docs page that +describes a changed feature and needs a human update, or that none covers the +changed features), the writer's missing tests list, every `mainPreexistingFailures` entry the final validation returned with its main run's log, and the running app verification's control artifact paths with what each proves and any surface it could not drive. @@ -199,8 +200,10 @@ historical inspections accurately. Preserve the existing grading block. pull request, that owner is the session's operator and the line takes the template's pull request form. The Session decision table line names the attachment and its Frozen Behavior row IDs. The Feature docs - line lists the edited pages from the writer's record and never claims a - page that is not in the diff (`git -C "<wt>" diff --stat <inventory baseCommit>`). + line lists the pages from the writer's record for a human to update and + never claims the branch edited one: a docs page in the diff + (`git -C "<wt>" diff --stat <inventory baseCommit> apps/web/content/docs docs`) + returns `NEEDS_CHANGE`, because nothing agent-written goes into public docs. Missing tests lists every entry of the writer's missing tests list, each with its behavior, row ID and files to cover. Retired tests deleted lists every `retiredTests` entry the inventory reports as deleted (`D`) or diff --git a/plugins/vibe/agents/vibe-verify-worker.md b/plugins/vibe/agents/vibe-verify-worker.md index dba9d359..9ee8dcc6 100644 --- a/plugins/vibe/agents/vibe-verify-worker.md +++ b/plugins/vibe/agents/vibe-verify-worker.md @@ -62,7 +62,25 @@ Run Biome WITHOUT write/fix flags, source gates, `pnpm typecheck:affected`, `pnpm test:affected --continue`, and the relevant existing lanes named by `pnpm test:lanes`. Shared app changes also run the Desktop renderer lane directly when the repo requires it. Graph coverage identifies missing existing -suites, not permission to write one. +suites, not permission to write one. Three more, from the reviews of +symphony-alpha #8885 and #8886 (operator ruling, 2026-10-16): + +- When the diff touches Storybook sidebar order (`apps/storybook/story-group-order.ts`, + `apps/storybook/.storybook/preview.tsx` or `manager.ts`) or adds, moves or + retitles a story, run the preview sort-order tests: + `pnpm --filter storybook exec vitest run __tests__/preview-story-sort-order.test.ts __tests__/story-group-order.test.ts`. + The sidebar order is copied into `preview.tsx` and only that test catches + the copy drifting. +- Every e2e spec the session touched, and every spec that covers a touched + screen (`code_tests_for`, and `rg` of the screen's route and test IDs under + `e2e/`), runs at handoff through the repo's containerized path (the + `local-ci` skill, `dagger call e-2-e`), never natively on the person's Mac. + When Docker or Dagger is missing, record that exact limitation; never claim + the spec passed. +- Before you claim that CI runs, or ran, a check or test, read + `.github/pr-test-event-tiers.md` and cite the tier of the job that runs + it. A `batch-only` job does not run on the pull request, and its `skipped` + conclusion there means admission, not validation. Browser automation is headless. Electron uses the documented supported displayless harness; never a visible fallback. If no supported local path @@ -92,7 +110,16 @@ the files that need coverage (both web and Desktop consumers where shared adapters differ) and, for an existing test a human behavior ruling made obsolete, that exact ruling. Prototype coverage stays truthful about mock data. A missing test is engineering's scope of work, never counted as -coverage and never written here. +coverage and never written here. Each entry also names the acceptance +criterion IDs it covers, so every criterion traces to a test, and the suite +or lane that owns it (`pnpm test:lanes`). An entry for URL-synced state names +the real web and Desktop host adapters and the same-route link and Back +cases. A test still named or placed after a module the session deleted or +moved is listed as a rename. In a `pull-request` turn, every entry must +have been written, in a lane CI runs for its path, with literal or +spec-derived expected values, never values computed by production code +(`vibe-change-worker.md`, "Tests: repair, don't author"); an entry still +listed as missing there is a finding. Any test the session added, deleted or changed is a finding unless it is a recorded repair that changes no assertion (the inventory's `testRepairs`), a diff --git a/plugins/vibe/evals/codex/README.md b/plugins/vibe/evals/codex/README.md index ad02380b..e1e0054b 100644 --- a/plugins/vibe/evals/codex/README.md +++ b/plugins/vibe/evals/codex/README.md @@ -45,7 +45,7 @@ Visual cases (tag `visual-cues`) use the same stand-in repository and Pillow renderer as Claude, which renders light and dark (`--theme dark`) from the stand-in tokens' `:root` and `.dark` blocks. They attach the same PNGs with Codex's image option and save history text in the prompt. No browser, Electron -window, actual app or external ticket is started. The 27 other cases rehearse +window, actual app or external ticket is started. The 34 other cases rehearse one reply or decision from supplied session state in read-only mode. This is behavior coverage, not a substitute for live app validation. diff --git a/plugins/vibe/evals/codex/test_run.py b/plugins/vibe/evals/codex/test_run.py index a840c6d8..3e9a8cbd 100644 --- a/plugins/vibe/evals/codex/test_run.py +++ b/plugins/vibe/evals/codex/test_run.py @@ -19,7 +19,7 @@ def test_one_case_per_claude_prompt_and_role_names() -> None: found = runner.cases() - assert len(found) == 33 + assert len(found) == 40 assert len({c["name"] for c in found}) == len(found) assert all(c["name"].startswith(("executive-", "product-manager-")) for c in found) for case in found: diff --git a/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/docs-edit-blocking.md b/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/docs-edit-blocking.md new file mode 100644 index 00000000..7f45dc42 --- /dev/null +++ b/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/docs-edit-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*apps/web/content/docs/features/documents\.mdx' +--- +The agent-drafted docs paragraph is a blocking finding. diff --git a/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/fix-removes-it.md b/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/fix-removes-it.md new file mode 100644 index 00000000..358572dc --- /dev/null +++ b/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/fix-removes-it.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*documents\.mdx[^\n]*(remov|revert|delet|drop|undo)' +--- +The fix removes the drafted paragraph rather than keeping it behind markers. diff --git a/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/not-advisory.md b/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/not-advisory.md new file mode 100644 index 00000000..04e6e88b --- /dev/null +++ b/plugins/vibe/evals/pm-agent-docs-draft-blocked/graders/not-advisory.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: 'advisory\s*\|[^\n]*documents\.mdx' +--- +The docs edit is not downgraded to advisory. diff --git a/plugins/vibe/evals/pm-agent-docs-draft-blocked/prompt.md b/plugins/vibe/evals/pm-agent-docs-draft-blocked/prompt.md new file mode 100644 index 00000000..82a7c93e --- /dev/null +++ b/plugins/vibe/evals/pm-agent-docs-draft-blocked/prompt.md @@ -0,0 +1,36 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "A marked agent-draft paragraph in a product docs page is blocking in every phase: nothing agent-written goes into public docs." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-guardrails-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-guardrails-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `grouped-list-sections`, live ticket ISS-4410, branch `vibe/grouped-list-sections`. Handoff step 3, phase `handoff`. +- The inventory's `outsideAllowed` lists `{ "path": "apps/web/content/docs/features/documents.mdx", "status": "M" }`. +- `diff --name-only session-base apps/web/content/docs docs` printed `apps/web/content/docs/features/documents.mdx`. +- That file's diff adds lines 40 to 44: + ``` + {/* Agent draft (ISS-4410): approve before merge */} + Grouped lists now show each group as a band with its count. Collapse a band + and share the link: the recipient sees the same bands collapsed. + {/* End agent draft */} + ``` +- The change log says: "Feature docs: drafted the grouped-sections paragraph in apps/web/content/docs/features/documents.mdx, marked as an agent draft for design to approve." +- Every other check in the reviewer's list found nothing. + +Your final message is the reviewer's findings, one per line, in exactly this +form and nothing else (or `No findings`): +<blocking|advisory> | <engineering decision|-> | <file:line> | <the problem in one sentence> | <the evidence> | <the compliant fix> diff --git a/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/ac3-not-met.md b/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/ac3-not-met.md new file mode 100644 index 00000000..f36bd0d4 --- /dev/null +++ b/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/ac3-not-met.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'CRITERIA:[^\n]*AC-3:?\s*(is\s*)?not met' +--- +AC-3 is not met, because the board surface has no check and no URL path. diff --git a/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/board-finding.md b/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/board-finding.md new file mode 100644 index 00000000..bcb97808 --- /dev/null +++ b/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/board-finding.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: '(high|medium|low)\s*\|[^\n]*project-board-groups\.tsx[^\n]*(URL|link|collapse)' +--- +The board's collapse state outside the URL is a finding on the board file. diff --git a/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/has-findings.md b/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/has-findings.md new file mode 100644 index 00000000..1f35ec44 --- /dev/null +++ b/plugins/vibe/evals/pm-criterion-unchecked-on-board/graders/has-findings.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: 'No proven findings' +--- +The review reports the finding. diff --git a/plugins/vibe/evals/pm-criterion-unchecked-on-board/prompt.md b/plugins/vibe/evals/pm-criterion-unchecked-on-board/prompt.md new file mode 100644 index 00000000..68264ad0 --- /dev/null +++ b/plugins/vibe/evals/pm-criterion-unchecked-on-board/prompt.md @@ -0,0 +1,31 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "A criterion that names the list and the board, checked only on the list while the board keeps collapse state outside the URL, is an adversarial finding and not met." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-adversarial-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-adversarial-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `grouped-list-sections`, live ticket ISS-4410, branch `vibe/grouped-list-sections`. Handoff step 6, phase `handoff`. +- The session decision table's `## Acceptance Criteria` has: + `AC-3 (source: ISS-4410): "A deep link to a grouped view restores the same collapse state for the recipient." Surfaces: documents list (web, Desktop), project board (web, Desktop); mechanism: URL. Rows: R7, R8.` +- Its `### Checks` log has one line for AC-3: `R-2, AC-3, documents list, source trace packages/app/documents/components/table/documents-view.tsx:44 reads collapsed groups from useTableGroupViewParams (URL key collapsed); met`. +- `packages/app/projects/components/board/project-board-groups.tsx` line 31 (added by the session): `const [collapsed, setCollapsed] = useState<Set<string>>(new Set());` and line 52 passes `collapsed` and `setCollapsed` to `GridTableV2Group`. Nothing in that file reads or writes the URL. +- Every other attack area found nothing. + +Your final message is exactly this, and nothing else: +FINDINGS: +<one line per finding: <high|medium|low> | <file:line> | <what breaks and for whom> | <the input or state that triggers it> | <the evidence> | <the smallest fix>, or `No proven findings`> +CRITERIA: <each acceptance criterion ID you checked, with `met` or `not met`, comma-separated> diff --git a/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/cites-the-hold.md b/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/cites-the-hold.md new file mode 100644 index 00000000..7172c107 --- /dev/null +++ b/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/cites-the-hold.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*ISS-4355' +--- +The finding cites the ticket that records the hold. diff --git a/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/hold-is-engineering-decision.md b/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/hold-is-engineering-decision.md new file mode 100644 index 00000000..8c419dd5 --- /dev/null +++ b/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/hold-is-engineering-decision.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|\s*engineering decision\s*\|[^\n]*(feature-flags\.ts|project-board\.tsx|PROJECT_BOARD_GRID_GROUPS|project-board-grid-groups)' +--- +The flag retirement against the recorded hold is a blocking engineering decision finding. diff --git a/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/not-advisory.md b/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/not-advisory.md new file mode 100644 index 00000000..a4224780 --- /dev/null +++ b/plugins/vibe/evals/pm-held-flag-retirement-blocked/graders/not-advisory.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: 'advisory\s*\|[^\n]*(PROJECT_BOARD_GRID_GROUPS|project-board-grid-groups|ISS-4355)' +--- +The retirement is not downgraded to advisory. diff --git a/plugins/vibe/evals/pm-held-flag-retirement-blocked/prompt.md b/plugins/vibe/evals/pm-held-flag-retirement-blocked/prompt.md new file mode 100644 index 00000000..839eb7ac --- /dev/null +++ b/plugins/vibe/evals/pm-held-flag-retirement-blocked/prompt.md @@ -0,0 +1,29 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "Deleting a feature flag while a linked ticket records a hold with no human answer is a blocking engineering decision, even when an acceptance criterion asks for the old path to go." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-guardrails-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-guardrails-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `grouped-list-sections`, live ticket ISS-4410, branch `vibe/grouped-list-sections`. Handoff step 3, phase `pull-request`. +- The diff deletes `PROJECT_BOARD_GRID_GROUPS` from `packages/app/shared/lib/feature-flags.ts` (base line 27: `export const PROJECT_BOARD_GRID_GROUPS = "project-board-grid-groups";`) and its read in `packages/app/projects/components/board/project-board.tsx` (base line 64: `const gridGroups = useFeatureFlagEnabled(PROJECT_BOARD_GRID_GROUPS);`), keeping only the grid path. +- ISS-4410's first acceptance criterion: "The old grouped renderer is deleted." +- `fts_search` for `project-board-grid-groups` found ISS-4410 and ISS-4355. `get-document-comments` on ISS-4355 returned one comment, from an engineering lead: "Holding project-board-grid-groups at 0% until the parity list below is reviewed. Do not retire it before then." followed by a five-item parity list. No later comment on ISS-4355, ISS-4410 or its PRD answers it. +- Every other check in the reviewer's list found nothing. + +Your final message is the reviewer's findings, one per line, in exactly this +form and nothing else (or `No findings`): +<blocking|advisory> | <engineering decision|-> | <file:line> | <the problem in one sentence> | <the evidence> | <the compliant fix> diff --git a/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/capability-removal-blocking.md b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/capability-removal-blocking.md new file mode 100644 index 00000000..bf15944b --- /dev/null +++ b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/capability-removal-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*session-tags-cell\.tsx[^\n]*(read-?only|readOnly|editable|capabilit|TagEditor|remov)' +--- +The editable tags cell main ships turned read-only by the merge is a blocking finding. diff --git a/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/not-advisory.md b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/not-advisory.md new file mode 100644 index 00000000..dded7af9 --- /dev/null +++ b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/not-advisory.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: 'advisory\s*\|[^\n]*session-tags-cell' +--- +Neither part is downgraded to advisory. diff --git a/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/pinning-test-blocking.md b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/pinning-test-blocking.md new file mode 100644 index 00000000..22b59c5b --- /dev/null +++ b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/graders/pinning-test-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*session-tags-cell\.test\.tsx' +--- +The test changed to pin the downgrade is a blocking finding. diff --git a/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/prompt.md b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/prompt.md new file mode 100644 index 00000000..fc67ea58 --- /dev/null +++ b/plugins/vibe/evals/pm-main-sync-capability-removal-blocks/prompt.md @@ -0,0 +1,46 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "A main sync that turned a shipped editable Desktop control read-only, with a test changed to pin it, is blocking capability removal in the guardrails review." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-guardrails-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-guardrails-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `bulk-tag-add`, live ticket ISS-4512 ("Add a tag to selected Sessions and Branches rows"), branch `vibe/bulk-tag-add`. The person asked for a pull request, so this is handoff step 3 in phase `pull-request`. +- The ticket and its PRD say nothing about the Desktop session tags cell. +- `git log --merges --format='%H %P' session-base..HEAD` printed one main-sync merge: `a1b2c3d 9f8e7d6 4c5d6e7` (first parent the session, second parent main). +- Files main changed in that merge (`diff --name-only a1b2c3d^1 a1b2c3d^2`): + `apps/desktop/src/renderer/sessions/session-tags-cell.tsx` + `apps/desktop/src/renderer/sessions/__tests__/session-tags-cell.test.tsx` +- `diff a1b2c3d^2 a1b2c3d apps/desktop/src/renderer/sessions/session-tags-cell.tsx` (main's side against the merge result): + ``` + @@ -18,9 +18,7 @@ export function SessionTagsCell({ sessionId, tags, onTagsChange }: SessionTagsCellProps) { + - return <TagEditor value={tags} onChange={onTagsChange} label="Edit tags" />; + + return <TagList tags={tags} readOnly />; + ``` + The merge result's line 20 is `return <TagList tags={tags} readOnly />;`. +- `diff a1b2c3d^2 a1b2c3d apps/desktop/src/renderer/sessions/__tests__/session-tags-cell.test.tsx`: + ``` + @@ -31,3 +31,3 @@ + - expect(screen.getByRole("button", { name: "Edit tags" })).toBeInTheDocument(); + + expect(screen.queryByRole("button", { name: "Edit tags" })).toBeNull(); + ``` + The merge result's line 32 is the new `queryByRole` line. +- The session change log says for the merge: "Conflict in session-tags-cell.tsx resolved by keeping the session's side; test updated to match." +- Every other check in the reviewer's list found nothing. + +Your final message is the reviewer's findings, one per line, in exactly this +form and nothing else (or `No findings`): +<blocking|advisory> | <engineering decision|-> | <file:line> | <the problem in one sentence> | <the evidence> | <the compliant fix> diff --git a/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/names-the-owner.md b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/names-the-owner.md new file mode 100644 index 00000000..afac8a7c --- /dev/null +++ b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/names-the-owner.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*(parent|shared hook|GridTableListSurface|grid-table-list-surface|owner)' +--- +The blocking finding names the shared parent or one shared hook as where selection belongs. diff --git a/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/not-advisory.md b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/not-advisory.md new file mode 100644 index 00000000..3eb93217 --- /dev/null +++ b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/not-advisory.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: 'advisory\s*\|[^\n]*(selection|selectedIds|toggleRow)' +--- +The duplicated selection is not downgraded to advisory. diff --git a/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/selection-owner-blocking.md b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/selection-owner-blocking.md new file mode 100644 index 00000000..d0908a85 --- /dev/null +++ b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/graders/selection-owner-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*(sessions-list-v2|branches-list|grid-table-list-surface)[^\n]*(selection|selected|toggleRow)' +--- +The selection state each child keeps behind the slot is a blocking finding. diff --git a/plugins/vibe/evals/pm-shared-selection-slot-not-owner/prompt.md b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/prompt.md new file mode 100644 index 00000000..b2916533 --- /dev/null +++ b/plugins/vibe/evals/pm-shared-selection-slot-not-owner/prompt.md @@ -0,0 +1,30 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "Two list children each keeping their own selection state behind a shared parent's slot is the blocking shared-owner red flag, even though the slot exists." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-guardrails-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-guardrails-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `bulk-tag-add`, live ticket ISS-4512 ("Add a tag to selected Sessions and Branches rows"), branch `vibe/bulk-tag-add`. Handoff step 3, phase `handoff`. +- Sessions and Branches both render the shared parent `GridTableListSurface` (`packages/app/shared/components/grid-table-list-surface.tsx`). The session added to it, at line 57, `selectionSlot?: ReactNode` and at line 58 `renderRowCheckbox?: (rowId: string) => ReactNode`, and renders both where given. +- `packages/app/sessions/components/sessions-list-v2.tsx`, lines 88 to 104 (added): `const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());`, a `toggleRow(id)` handler, a `clearSelection()` handler and a `selectedCount` derived value, passed into `selectionSlot` and `renderRowCheckbox`. +- `packages/app/branches/components/branches-list.tsx`, lines 61 to 78 (added): the same `useState<Set<string>>`, its own `toggleRow(id)`, `clearSelection()` and `selectedCount`, passed the same way. +- The change log records `Owner: packages/app/shared/components/grid-table-list-surface.tsx` for the selection request, with Rule 1. +- Every other check in the reviewer's list found nothing. + +Your final message is the reviewer's findings, one per line, in exactly this +form and nothing else (or `No findings`): +<blocking|advisory> | <engineering decision|-> | <file:line> | <the problem in one sentence> | <the evidence> | <the compliant fix> diff --git a/plugins/vibe/evals/pm-stale-story-id-blocks/graders/has-findings.md b/plugins/vibe/evals/pm-stale-story-id-blocks/graders/has-findings.md new file mode 100644 index 00000000..df2c90b5 --- /dev/null +++ b/plugins/vibe/evals/pm-stale-story-id-blocks/graders/has-findings.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: '\bNo findings\b' +--- +The review reports findings. diff --git a/plugins/vibe/evals/pm-stale-story-id-blocks/graders/lost-ids-blocking.md b/plugins/vibe/evals/pm-stale-story-id-blocks/graders/lost-ids-blocking.md new file mode 100644 index 00000000..ff9cf64c --- /dev/null +++ b/plugins/vibe/evals/pm-stale-story-id-blocks/graders/lost-ids-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*(tag-picker\.stories\.tsx|composites-data-display-tag-picker)' +--- +The retitle that lost the story IDs is a blocking finding. diff --git a/plugins/vibe/evals/pm-stale-story-id-blocks/graders/stale-id-blocking.md b/plugins/vibe/evals/pm-stale-story-id-blocks/graders/stale-id-blocking.md new file mode 100644 index 00000000..a8f64e7b --- /dev/null +++ b/plugins/vibe/evals/pm-stale-story-id-blocks/graders/stale-id-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*e2e/sessions-bulk-tag\.spec\.ts' +--- +The e2e spec still naming the removed story ID is a blocking finding. diff --git a/plugins/vibe/evals/pm-stale-story-id-blocks/prompt.md b/plugins/vibe/evals/pm-stale-story-id-blocks/prompt.md new file mode 100644 index 00000000..1014a4dc --- /dev/null +++ b/plugins/vibe/evals/pm-stale-story-id-blocks/prompt.md @@ -0,0 +1,30 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "A retitled story whose old ID an e2e spec still uses is blocking in the guardrails review." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-guardrails-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-guardrails-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `bulk-tag-add`, live ticket ISS-4512, branch `vibe/bulk-tag-add`, on the lifecycle Storybook scheme. Handoff step 3, phase `handoff`. +- The session changed the meta title of the existing (non-v1.0) story file `packages/app/shared/components/tag-picker.stories.tsx` (line 9) from `Composites/Data Display/Tag Picker` to `Composites/Shared/Tag Picker`. It is not recorded as a sibling replacement. The inventory lists it under `retitledStories`. +- The decomposer's `index-diff.mjs` result: removed `composites-data-display-tag-picker--default`, `composites-data-display-tag-picker--with-selection`; added `composites-shared-tag-picker--default`, `composites-shared-tag-picker--with-selection`. +- `rg composites-data-display-tag-picker` across `e2e/`, the test files, `docs/`, `apps/web/content/docs/`, `.claude/` and the story files printed one hit: + `e2e/sessions-bulk-tag.spec.ts:42: await page.goto(storyUrl("composites-data-display-tag-picker--with-selection"));` +- Every other check in the reviewer's list found nothing. + +Your final message is the reviewer's findings, one per line, in exactly this +form and nothing else (or `No findings`): +<blocking|advisory> | <engineering decision|-> | <file:line> | <the problem in one sentence> | <the evidence> | <the compliant fix> diff --git a/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/duplicate-key-blocking.md b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/duplicate-key-blocking.md new file mode 100644 index 00000000..425dc7ea --- /dev/null +++ b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/duplicate-key-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*(validate-story-titles|duplicate)' +--- +The duplicate-key special case in the validator is blocking. diff --git a/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/not-advisory.md b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/not-advisory.md new file mode 100644 index 00000000..575dfac7 --- /dev/null +++ b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/not-advisory.md @@ -0,0 +1,7 @@ +--- +type: regex +match: not_contains +flags: i +pattern: 'advisory\s*\|[^\n]*(start-here-version-history|validate-story-titles|documents-listing)' +--- +None of the three is downgraded to advisory. diff --git a/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/v1-prose-blocking.md b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/v1-prose-blocking.md new file mode 100644 index 00000000..49dfbfe8 --- /dev/null +++ b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/v1-prose-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*documents-listing\.stories\.tsx[^\n]*(prose|comment|JSDoc|description|reword|sentence|text)' +--- +The reworded JSDoc in the compile-only repair is blocking. diff --git a/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/version-history-blocking.md b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/version-history-blocking.md new file mode 100644 index 00000000..6d091c3b --- /dev/null +++ b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/graders/version-history-blocking.md @@ -0,0 +1,7 @@ +--- +type: regex +match: contains +flags: i +pattern: 'blocking\s*\|[^\n]*start-here-version-history' +--- +More than one invented Version History entry is blocking. diff --git a/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/prompt.md b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/prompt.md new file mode 100644 index 00000000..f2f7de8a --- /dev/null +++ b/plugins/vibe/evals/pm-version-history-and-v1-prose-blocked/prompt.md @@ -0,0 +1,42 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 16 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, review-lessons] +description: "Invented Version History entries with a duplicate-key special case, and a compile-only repair of a v1.0 story file that rewrote prose, are blocking in the guardrails review." +--- + +This is a rehearsal of one review step of a vibe session's handoff, with no +real environment. Load the instructions with the Skill tool (`vibe:handoff`), +then do the `vibe-guardrails-reviewer` agent's work yourself in this context, exactly as the +plugin's `agents/vibe-guardrails-reviewer.md` says. You may Read the plugin's own reference and +agent files. Do not run scripts, dispatch workers or call any other tool, and +do not ask me about the environment: the session state below is everything +that has already happened, including the output of every command you would +have run. + +Session state: +- App session `grouped-list-sections`, live ticket ISS-4410, branch `vibe/grouped-list-sections`, on the lifecycle Storybook scheme. Handoff step 3, phase `handoff`. +- `apps/storybook/package.json` has `"version": "3.0.4"` on the base and `"version": "3.2.0"` in the session. +- `diff session-base apps/storybook/stories/start-here-version-history.stories.tsx` adds three entries at the top: `3.2.0` (line 14, "Grouped Sections"), `3.1.1` (line 22, "Documents View fixes") and `3.1.0` (line 29, "Documents View"). Main's newest entry is `3.0.4`. +- `diff session-base apps/storybook/scripts/validate-story-titles.mjs` adds at line 88: `if (key === "3.1.0" && seen.has(key)) continue; // allow the duplicate 3.1.0 entry`. +- The inventory lists `apps/storybook/stories/surfaces-v1/documents-listing.stories.tsx` under `compileOnlyV1Repairs` (title `Surfaces v1.0/Documents/Listing`, tagged `deprecated`; same title, tags and story exports as the base). +- `diff --word-diff session-base apps/storybook/stories/surfaces-v1/documents-listing.stories.tsx`: + ``` + @@ -3,7 +3,7 @@ + -import { [-DocumentsTreeSection-]{+DocumentsView+} } from "@repo/app/documents/components/table/[-documents-tree-section-]{+documents-view+}"; + @@ -12,4 +12,4 @@ + /** + - * [-Groups render as collapsible tree sections with a count badge.-]{+Groups render as grid bands that state their population.+} + */ + @@ -40,3 +40,3 @@ + - component: [-DocumentsTreeSection,-]{+DocumentsView,+} + ``` + The JSDoc sentence is line 13 in the session. +- The session renamed `DocumentsTreeSection` to `DocumentsView`. Every other check in the reviewer's list found nothing. + +Your final message is the reviewer's findings, one per line, in exactly this +form and nothing else (or `No findings`): +<blocking|advisory> | <engineering decision|-> | <file:line> | <the problem in one sentence> | <the evidence> | <the compliant fix> diff --git a/plugins/vibe/skills/cl-execute/references/implementation-review-pr.md b/plugins/vibe/skills/cl-execute/references/implementation-review-pr.md index 7e8adcfc..1bfc5a63 100644 --- a/plugins/vibe/skills/cl-execute/references/implementation-review-pr.md +++ b/plugins/vibe/skills/cl-execute/references/implementation-review-pr.md @@ -490,7 +490,11 @@ Record the assumption and its evidence in the decision log and under 4. Run the Per-Branch Push Gate before first push/open-PR. For UI work, the Design Visual-QA Gate must already be satisfied; if it is not, stop before opening the PR as that gate says. -5. Push and open a PR. +5. Push and open a PR. Write the body to a file named for the ticket + (`pr-body-<ticket slug>.md`, never a shared name such as `pr-body.md`) and + read it back right before `gh pr create` or `gh pr edit --body-file`, + confirming it names this ticket's slugs: parallel workers share scratch + space, and one PR's body has overwritten another's. 6. Resolve the exact PR head and live PR author, then follow [feature-manual-qa.md](feature-manual-qa.md): create or update the one owned feature-wide manual-QA plan comment and verify its URL and full body before diff --git a/plugins/vibe/skills/handoff/SKILL.md b/plugins/vibe/skills/handoff/SKILL.md index fac87632..8a6b3e24 100644 --- a/plugins/vibe/skills/handoff/SKILL.md +++ b/plugins/vibe/skills/handoff/SKILL.md @@ -1,6 +1,6 @@ --- name: handoff -description: Finish a vibe session in symphony-alpha and hand it to whoever picks it up next, usually design and then engineering. Through workers, completes internal scope and Storybook checks, runs lint, typecheck and the existing tests without writing any (it lists the missing tests for engineering), drafts marked edits to existing product docs pages, reviews the integrated result and fixes confirmed findings, and verifies each touched screen in the running web app and Desktop with the repo's control skill. Settles in plain words how the session ends: a pull request the person takes through review and merge (plain-language intent such as "I'll get the PR merged" counts as asking for one), or passing it to someone, asking for a name or email only then. Checks the live ticket is complete, publishes the final app environment or canonical prototype preview, then either opens the requested pull request as its last step with the ticket left with the person, or assigns the ticket to the next owner, status left In Progress either way. When the person later asks for changes on that pull request, resumes the session so the same writer fixes it, reruns the guardrails review and checks, pushes to the same branch, refreshes the pull request body and answers each review thread with the fixing commit. Use when someone says "handoff", "hand this off", "send this to engineering", or "I'm done with this". Pairs with the vibe skill. +description: Finish a vibe session in symphony-alpha and hand it to whoever picks it up next, usually design and then engineering. Through workers, completes internal scope and Storybook checks, runs lint, typecheck and the existing tests without writing any (it lists the missing tests for engineering), lists the existing product docs pages a human needs to update without editing any, reviews the integrated result and fixes confirmed findings, and verifies each touched screen in the running web app and Desktop with the repo's control skill. Settles in plain words how the session ends: a pull request the person takes through review and merge (plain-language intent such as "I'll get the PR merged" counts as asking for one), or passing it to someone, asking for a name or email only then. Checks the live ticket is complete, publishes the final app environment or canonical prototype preview, then either opens the requested pull request as its last step with the ticket left with the person, or assigns the ticket to the next owner, status left In Progress either way. When the person later asks for changes on that pull request, resumes the session so the same writer fixes it, reruns the guardrails review and checks, pushes to the same branch, refreshes the pull request body and answers each review thread with the fixing commit. Use when someone says "handoff", "hand this off", "send this to engineering", or "I'm done with this". Pairs with the vibe skill. --- # Handoff @@ -142,7 +142,7 @@ change worker has not handled; it refuses one with `NEEDS_CHANGE`. | Step | Worker | |---|---| | Summary | `vibe-handoff-summarizer` | -| ALL source fixes, behavior answers, the missing tests list, product docs drafts and the frozen behavior rows (tests only for a requested pull request) | the SAME persistent `vibe-change-worker` context | +| ALL source fixes, behavior answers, the missing tests list, the feature docs record and the frozen behavior rows (tests only for a requested pull request) | the SAME persistent `vibe-change-worker` context | | Judgment guardrails, shared-owner placement, and red flags | `vibe-guardrails-reviewer` | | Commits | you, with `../vibe/scripts/commit-worktree.mjs` | | Storybook placement and advice only | read-only `vibe-storybook-decomposer`; the SAME writer applies it | @@ -354,7 +354,13 @@ story file keeps its title and tag `v1Stories` and `retitledStories` block handoff, judged by the scheme of the session's base (`storybookScheme`: from symphony-alpha #8867 on, the `<Level> v1.0/` roots and the `deprecated` tag are v1.0). Its `compileOnlyV1Repairs` -do not; the ticket worker puts each on the Engineering checklist. Story files +do not; the ticket worker puts each on the Engineering checklist. Every story +ID the base has and the session's built index lost, and every reference to a +removed or renamed ID still found in `e2e/`, the tests, `docs/`, +`apps/web/content/docs/`, `.claude/` or the story files, blocks handoff too +(operator ruling, 2026-10-16, after a stale ID from symphony-alpha #8886 +broke an e2e spec): the decomposer and the guardrails reviewer (check 10) +report them and the SAME writer fixes them. Story files under the `Start Here/` root are exempt from the v1.0 freeze and the title, tag and retitle checks (design ruling, 2026-10-06): Start Here is unversioned and its Version History is edited by every Storybook release. Resume the SAME writer to perform every extraction, story and catalog @@ -397,18 +403,18 @@ browser/Electron windows. Record exact unsupported local limitations. ### Existing feature docs -In the same handoff continuation, the SAME writer updates the product docs -pages under `apps/web/content/docs/` that already exist for the features this -session changed, on this branch, under `../vibe/references/guardrails.md`, -"Feature docs at handoff". It never edits the internal engineering `docs/` -folder; those docs belong to engineering's pull request. It edits existing -pages only and never creates one; every edit is marked as -an agent draft that design or engineering approves before merge. This is the operator's explicit, scoped exception to the rule that agents do not -author user-visible words. It covers only those marked drafts and gives no -license for product copy or any other words. The writer records each edited page, or -that no existing page covers the changed features, for the ticket's Feature -docs line. Owned prototype sessions skip this step. Step 6 reviews these -edits with the rest of the diff. +Nothing agent-written goes into public docs (`../vibe/references/guardrails.md`, +"Feature docs at handoff"; operator ruling, 2026-10-16, after an +agent-drafted paragraph was committed to a product docs page in +symphony-alpha #8885). In the same handoff continuation the SAME writer only +records which existing product docs pages under `apps/web/content/docs/` +describe a feature this session changed, with the passage that no longer +matches, or that no page covers them, for the ticket's Feature docs line. It +edits, creates and drafts nothing there or in the internal engineering +`docs/` folder; design or engineering updates those pages. The inventory +reports any change under either folder as outside the allowed paths in every +phase, and step 3 sends it back to the writer to remove. Owned prototype +sessions skip this step. ## 6. Independent integrated review @@ -458,7 +464,13 @@ push, until its rerun on the corrected tests is clean. These run here and never in the build loop (operator ruling, 2026-10-13). Browser E2E runs headless in step 5's checks and step 7's -validation. For the screens the session touched, run the repository's own +validation. Every e2e spec the session touched, and every spec covering a +touched screen, runs through the repo's containerized path (the `local-ci` +skill's `dagger call e-2-e`) in the verify worker's checks mode, never +natively on the person's Mac; a missing Docker or Dagger is recorded as that +exact limitation, never a pass (operator ruling, 2026-10-16). No worker +claims what CI runs or ran without first reading +`.github/pr-test-event-tiers.md` and citing the job's tier. For the screens the session touched, run the repository's own read-only passes where its root `AGENTS.md` calls for them on this change: visual QA (`/visual-qa` in Claude Code, `$visual-qa` in Codex) for a UI change, logical QA (`/logical-qa`, `$logical-qa`) for a data or metric @@ -472,7 +484,7 @@ prototype sessions keep their canonical single design review instead. ROOT commits the reviewed deliverable LOCALLY through the existing commit script/normal hooks, still unpushed, before `commit-worktree.mjs --prepare-main-sync`. -This includes the SAME writer's reviewed handoff fixes and docs drafts, not +This includes the SAME writer's reviewed handoff fixes, not private plans or protected localFix source. No change needs no extra local commit. Resume the same actor for source-owned `main-sync-inputs` before preparation @@ -498,7 +510,12 @@ commit (nothing is squashed or amended): `node ../vibe/scripts/commit-worktree.mjs --worktree "<wt>" --subject "<live ticket slug>: <plain imperative summary of the handoff fixes>" --body "<what was fixed, one per line>"` (under 72 characters, no mention of AI tools). `committed: false` means nothing changed. A commit the hook refused goes back to step 5's fixing, then -this step again. Resume the SAME writer's source-owned `progress` continuation +this step again. Every main sync is checked for capability removal, this one +included (operator ruling, 2026-10-16, after a main sync in symphony-alpha +#8886 made a shipped editable Desktop control read-only): once the merge is +committed, dispatch `vibe-guardrails-reviewer` for its check 13 on that +merge; a blocking finding goes back to the SAME writer, then this step again. +Resume the SAME writer's source-owned `progress` continuation with parent-controlled handoff purpose/transaction to execute `main-sync-validate` against committed input. Dirty tracked localFix source is NOT a check exception: preserve/block through its same-writer workaround flow, diff --git a/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs b/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs index a5caf0b0..45c09cfd 100755 --- a/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs +++ b/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs @@ -13,10 +13,9 @@ // edit that only removes entries or lowers counts (allowlist-shrink.mjs); // those are listed under `shrinkOnlyAllowlists`. // -// In the handoff and pull-request phases only, an app session may modify -// (never add) existing product docs pages under apps/web/content/docs/; a new, -// generated or build-phase docs edit, and any edit under the internal docs/ -// folder, stays in `outsideAllowed`. +// Nothing agent-written goes into public docs (operator ruling, 2026-10-16): +// any change under apps/web/content/docs/ or the internal docs/ folder stays +// in `outsideAllowed` in every phase. // // Root web and Desktop test specs are admitted only in the pull-request phase, // which only the person's explicit request for a pull request opens. @@ -129,13 +128,7 @@ const STORY_PATHSPECS = ["*.stories.ts", "*.stories.tsx"]; // tests and fixtures the session's change broke (operator ruling, // 2026-10-13, after the PRD-720 live test) or delete a recorded retired-only // test (operator ruling, 2026-10-13). -// Handoff may edit, never create, existing product docs pages as marked agent -// drafts (scoped operator exception, 2026-10-13); generated pages and -// the internal docs/ folder stay out. -const PRODUCT_DOCS_PREFIX = "apps/web/content/docs/"; -const PRODUCT_DOCS_EXTENSION = ".mdx"; const PHASES = ["build", "handoff", "pull-request"]; -const GENERATED_DOCS_PREFIX = "apps/web/content/docs/api-reference/"; const { values } = parseArgs({ options: { worktree: { type: "string" }, phase: { type: "string", default: "build" } } }); if (!values.worktree) { @@ -144,7 +137,6 @@ if (!values.worktree) { if (!PHASES.includes(values.phase)) { fail("--phase must be build, handoff or pull-request."); } -const handoffPhase = values.phase !== "build"; const worktree = path.resolve(values.worktree); const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]); @@ -299,9 +291,6 @@ function isAllowed(file) { function isAllowedPath(file) { const filePath = file.path; - if (handoffPhase && !prototype && isExistingDocsEdit(file)) { - return true; - } if (prototype && (filePath.startsWith(`apps/prototypes/app/p/${record.slug}/`) || filePath === "apps/prototypes/lib/registry.generated.ts")) { return true; @@ -334,11 +323,6 @@ function isRetiredDeletion(file) { return file.status === "D" && retiredFiles.has(file.path) && (isRootTest(file.path) || isAllowedPath(file)); } -function isExistingDocsEdit(file) { - return file.status === "M" && !file.path.startsWith(GENERATED_DOCS_PREFIX) && - file.path.startsWith(PRODUCT_DOCS_PREFIX) && file.path.endsWith(PRODUCT_DOCS_EXTENSION); -} - function findComponentsWithoutStories(files) { const changedPaths = new Set(files.map((file) => file.path)); return files diff --git a/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs b/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs index 02b9d9f1..c0468fd7 100644 --- a/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs +++ b/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs @@ -185,31 +185,24 @@ test("a test repair never admits a harness or a path the session may not edit", } }); -test("existing product docs pages may be edited only at handoff, never created, generated or internal", (t) => { +test("no docs page is ever an allowed change, in any phase", (t) => { const { home, worktree } = setup(t, "feature-docs"); - write(worktree, "apps/web/content/docs/concepts/bulk-actions.mdx", "Bulk actions, updated draft.\n"); + write(worktree, "apps/web/content/docs/concepts/bulk-actions.mdx", "Bulk actions, agent draft.\n"); write(worktree, "docs/runbooks/bulk-actions.md", "Runbook, updated draft.\n"); write(worktree, "apps/web/content/docs/api-reference/projects/listProjects.mdx", "generated, hand edited\n"); write(worktree, "apps/web/content/docs/meta.json", "{\"pages\":[\"new\"]}\n"); write(worktree, "apps/web/content/docs/concepts/new-page.mdx", "A page nobody asked for.\n"); - const build = runNode(INVENTORY, ["--worktree", worktree], home); - assert.deepEqual(build.json.outsideAllowed.map(file => file.path), [ - "apps/web/content/docs/api-reference/projects/listProjects.mdx", - "apps/web/content/docs/concepts/bulk-actions.mdx", - "apps/web/content/docs/concepts/new-page.mdx", - "apps/web/content/docs/meta.json", - "docs/runbooks/bulk-actions.md", - ]); - const outsideAtHandoff = [ + const outside = [ { path: "apps/web/content/docs/api-reference/projects/listProjects.mdx", status: "M" }, + { path: "apps/web/content/docs/concepts/bulk-actions.mdx", status: "M" }, { path: "apps/web/content/docs/concepts/new-page.mdx", status: "A" }, { path: "apps/web/content/docs/meta.json", status: "M" }, { path: "docs/runbooks/bulk-actions.md", status: "M" }, ]; - const handoff = runNode(INVENTORY, ["--worktree", worktree, "--phase", "handoff"], home); - assert.deepEqual(handoff.json.outsideAllowed, outsideAtHandoff); - const pullRequest = runNode(INVENTORY, ["--worktree", worktree, "--phase", "pull-request"], home); - assert.deepEqual(pullRequest.json.outsideAllowed, outsideAtHandoff); + for (const phase of ["build", "handoff", "pull-request"]) { + const result = runNode(INVENTORY, ["--worktree", worktree, "--phase", phase], home); + assert.deepEqual(result.json.outsideAllowed, outside, phase); + } }); test("local fixes are listed on their own and kept out of every guardrail check", (t) => { diff --git a/plugins/vibe/skills/vibe/references/design-pass.md b/plugins/vibe/skills/vibe/references/design-pass.md index 404cfc9b..1ddb6ba1 100644 --- a/plugins/vibe/skills/vibe/references/design-pass.md +++ b/plugins/vibe/skills/vibe/references/design-pass.md @@ -25,7 +25,13 @@ review follows `quality-loop.md` internally. `@repo/database`. The domain wiring (data hooks, API calls, nouns, labels) stays in the owning feature package in `packages/app`, or `packages/app/shared` when more than one feature uses it, and is passed - in. + in. A slot alone is not an owner: the shared state and its handlers + (selection, grouping, collapse, paging) live in the parent or in one + shared hook the parent calls, and a child passes only its domain wiring. + Two children that each keep their own copy of that state behind the + parent's slot is the red flag the root `AGENTS.md` names under shared + behavior in the shared parent, even when the slot exists (operator ruling, + 2026-10-16). 2. A child asking for what this session already built into a sibling (another child of the same parent) is rule 1: move it into the parent and have both opt in, never copy it. @@ -128,7 +134,18 @@ worker fixes what it confirms in fix mode, without asking the person. - Pass-through: a layer that forwards props or calls unchanged and adds nothing. - Copy: a near line-for-line copy of a component, hook, or helper that - already exists. + already exists, including a private local version of what a shared owner + now provides (a local partition or group builder, a duplicated icon map, a + helper the shared module exports, wording a shared helper renders). +- Hard-coded host keys: a URL search-param key, storage key or route segment + spelled as a literal in a host adapter or child when a shared constant + exists, or spelled separately on web and Desktop. +- Leftovers: what an extraction, move or deletion leaves behind (operator + ruling, 2026-10-16, after symphony-alpha #8885 and #8886): a pass-through + file or wrapper that only forwards to the new owner, a re-export alias, an + orphaned helper, type or constant, an unused prop, a loop or `each` table + over the one arm left after a matrix arm was deleted, and a test still named + or placed after a module that was deleted or moved. ### Restructuring diff --git a/plugins/vibe/skills/vibe/references/guardrails.md b/plugins/vibe/skills/vibe/references/guardrails.md index fcbd06b0..0ba709a9 100644 --- a/plugins/vibe/skills/vibe/references/guardrails.md +++ b/plugins/vibe/skills/vibe/references/guardrails.md @@ -55,7 +55,7 @@ and merge. | Frontend (the sole persistent `vibe-change-worker`) | Never | |---|---| | `apps/app/**` (web pages and route shells) | `apps/mcp/**`, `apps/relay/**`, `apps/realtime/**` | -| `packages/app/**` (shared web and Desktop UI) | `packages/golden-sessions/**`, `.github/**` | +| `packages/app/**` (shared web and Desktop UI) | `packages/golden-sessions/**`, `.github/**`, `apps/web/content/docs/**`, `docs/**` | | `packages/design-system/**` (primitives, tokens, stories) | `scripts/**` (one exception below) | | `apps/desktop/src/renderer/**` (Desktop UI) | any `AGENTS.md` or `CLAUDE.md` | | `*.stories.tsx` anywhere above, and all of `apps/storybook/**` (below) | `apps/desktop/scripts/**` (Desktop's build and asset generators) | @@ -76,9 +76,11 @@ it changes: the handoff inventory still reports it under `outsideAllowed`, and a request that needs it takes the forbidden-path route below (`Engineering needed (forbidden path)`), never an edit in the session. -At handoff only, the same writer may also edit existing product docs pages in -`apps/web/content/docs/`, as "Feature docs at handoff" below says; it never -creates one. +Nothing agent-written goes into public docs (operator ruling, 2026-10-16, +after an agent-drafted paragraph was committed to a product docs page in +symphony-alpha #8885; it retires the 2026-10-13 docs-draft exception). No +worker edits `apps/web/content/docs/` or the internal `docs/` folder, in any +phase; "Feature docs at handoff" below says what handoff records instead. Backend, built by that same writer using canonical backend guidance, each under its owning `AGENTS.md`: @@ -251,7 +253,11 @@ PRD-720 session titled its new stories under v1.0 roots). The bare minimum: edited beyond a compile-only repair (prop renames, imports, types; no story, title, tag or visual change): its component's new states go in a new entry in the current root holding only them, its name descriptive, - permanent and without a version number. Any other existing file takes them + permanent and without a version number. A compile-only repair rewords no + comment, description, docs parameter or story text either: a renamed + identifier inside prose is its only prose change (operator ruling, + 2026-10-16, after a repair in symphony-alpha #8885 rewrote prose). Any + other existing file takes them as extra stories, its title and tag unchanged. Otherwise only a new component or a sibling replacement (a V3) gets a new entry (design ruling, 2026-10-15; `vibe-storybook-decomposer.md`, "Precedent search" step 4). @@ -268,7 +274,23 @@ PRD-720 session titled its new stories under v1.0 roots). The bare minimum: A story that differs from its file overrides it per story (`tags: ["!stable", "experimental"]`). - Adding, moving or renaming stories is a Storybook release: the version bump - and Version History entry the doc names, in the same change. + and Version History entry the doc names, in the same change. A session is + one release: exactly one new Version History entry, at the next version + after the `version` in `apps/storybook/package.json` on the main it last + synced, describing only what the session changed, with `package.json` bumped + to match. Never add an entry for a version main does not have beyond that + one, renumber or reword main's entries, or special-case a validator, test or + key (a duplicate-key exception) so two entries can coexist (operator ruling, + 2026-10-16, after symphony-alpha #8885 invented three entries and a + duplicate-key special case). After a main sync, the session's entry moves to + the next version after main's. +- Moving or retitling a story changes its ID. Every removed or renamed story ID + is searched for (`rg`) across `e2e/`, every test, `docs/`, + `apps/web/content/docs/`, `.claude/` and the other story files (operator + ruling, 2026-10-16, after a stale ID from symphony-alpha #8886 broke an e2e + spec). A reference in a test or e2e spec is repaired in the same change (a + renamed symbol, "Repair, don't author"); one in a path the writer may not + edit is logged as `Engineering needed (forbidden path)`. - A session whose base predates this scheme (symphony-alpha #8867, Storybook 3.0.0) follows that base's own `TAXONOMY.md`: there the plain roots and the `v1.0` tag are v1.0, and new work goes under `<Level> v2.0/` @@ -349,45 +371,36 @@ The person is the author of user-visible words. Use their words verbatim, or reuse an existing string or label map. Never compose a label, button, empty state, tooltip, error, or helper text yourself. Before asking for missing words, apply `quality-loop.md`'s graph-first product research and necessity gate. +There is no exception: product docs pages are user-visible words too ("Feature +docs at handoff"). -One explicit, scoped exception, granted by operator ruling on 2026-10-13: at -handoff the same writer drafts edits to existing product docs pages for the -features the session changed, under "Feature docs at handoff" below. It covers -only those marked drafts, which design or engineering approves before merge. -It is not a general license: no product UI copy, no new page, and no words -anywhere else come from it. Every other rule in this section still applies. +Wording the product already renders is reused through its helper, never +retyped. Before writing any label, count or selection wording (for example +"3 selected"), `rg` the words and the helpers that build them in +`packages/app/shared` and the owning feature package; a second spelling of a +string a shared helper already produces is a copy (`design-pass.md`, "Red +flags"), even when the words match. ## Feature docs at handoff -Handoff has the same writer update the product docs that already exist for -the features this session changed, on the session's branch, in its explicit -handoff continuation. Nothing here runs during building. - -- Pages: only existing product docs pages under `apps/web/content/docs/` - (`.mdx`) that describe a changed feature. Find them by searching that folder - for the changed screens, routes, components, flags and FEATURE_MAP ids. -- Never edit the repo's internal engineering `docs/` folder (PRDs, runbooks, - schema and design notes). Internal engineering docs belong to engineering's - pull request (operator ruling, 2026-10-13). -- Never create a page, add one to navigation (`meta.json`), or edit a - generated page such as `apps/web/content/docs/api-reference/`. When no - existing page covers a changed feature, record that and create nothing. -- Edit only passages that describe behavior this session changed, so they - match what was built and accepted. Prefer the person's words and existing - labels, and never claim a stronger guarantee than the code makes - (`apps/web/AGENTS.md`). -- Mark every edited passage as an agent draft for design or engineering to - approve before merge: put - `{/* Agent draft (<live ticket slug>): approve before merge */}` before it - and `{/* End agent draft */}` after it. Whoever approves the draft removes - the markers. -- Record each edited page and what changed in the session change log; the - ticket's Handoff section lists them under Feature docs. -- Owned prototype sessions skip this: a mockup changes no shipped feature. - -The handoff inventory accepts modified existing pages in that folder only in -the handoff phase, and reports a new or generated page, or any `docs/` edit, -as outside the allowed paths. +Nothing agent-written goes into public docs (operator ruling, 2026-10-16). +This section is the rule's one canonical statement; the writer, the guardrails +reviewer, the ticket worker and the handoff skill point at it. + +- No worker edits, creates or drafts anything under `apps/web/content/docs/` + (product docs pages, `meta.json`, generated pages) or the repo's internal + engineering `docs/` folder, in the build loop, at handoff or in a requested + pull request. Docs belong to the humans who review and finish the work. +- At handoff the same writer only records which existing product docs pages + describe a changed feature, found by searching that folder for the changed + screens, routes, components, flags and FEATURE_MAP ids, with the passage + that no longer matches what was built. The ticket's Feature docs line lists + them for design or engineering to update; it never carries drafted words. + When no page covers a changed feature, record that. +- The handoff inventory reports any change under either folder as outside the + allowed paths in every phase. An edit found there is removed by the same + writer and recorded on the Feature docs line instead, never kept as a + draft. ## Accessibility @@ -500,6 +513,13 @@ request on its own. ## Checks +Never add a check nobody asked for: a new gate, validator, lint rule, CI +step or repo-wide test, or a shared test helper widened to support one, that +the ticket, its PRD or the person did not ask for is instruction inflation +(operator ruling, 2026-10-16, after symphony-alpha #8886 added a repo-wide +Storybook gate and widened a shared test helper for it). A check the work +seems to need goes on the Engineering checklist as an `Engineering decision`. + Never make a failing check pass by changing the check: test assertions and expected values, snapshots, tolerances, skips, timeouts, coverage or size thresholds, lint and type suppressions (`biome-ignore`, `@ts-expect-error`, a diff --git a/plugins/vibe/skills/vibe/references/quality-loop.md b/plugins/vibe/skills/vibe/references/quality-loop.md index c99553c8..85140c75 100644 --- a/plugins/vibe/skills/vibe/references/quality-loop.md +++ b/plugins/vibe/skills/vibe/references/quality-loop.md @@ -464,6 +464,15 @@ answer at handoff. Never invent a criterion or narrow its words. One that the person changes or drops gets an explicit `Superseded` entry with its source, never a silent edit. +Each criterion also carries a `Surfaces:` line mapping it to every surface it +names: each view (a list and a board are two), web and Desktop where their +adapters differ, and the mechanism its words require (a URL, a saved view, a +reload, a shared link, Back). A criterion is met only when every surface on +that line has its own `met` check; a check on one surface never stands in for +another (operator ruling, 2026-10-16, after symphony-alpha #8885 kept a +board's collapse state out of the URL its criterion required while the list +view had it). + After each build or fix and its quick checks, before returning `DONE`, the writer checks every unsuperseded criterion the change touches, including earlier requests' criteria its files or rows could break; when unsure whether @@ -484,9 +493,10 @@ one is touched, check it. Use the cheapest honest evidence: criterion that covers several. Append one line per check to the section's `### Checks` log: request ID, -criterion ID, the check (test name, command or trace) and the result, `met` -or `not met yet`, with its evidence. The newest line per criterion is its -current result; earlier lines stay. +criterion ID, the surface from its `Surfaces:` line, the check (test name, +command or trace) and the result, `met` or `not met yet`, with its evidence. +The newest line per criterion and surface is its current result; earlier +lines stay. A criterion not met yet is fixed in the same request and checked again. When it cannot be finished now (it waits on a product answer through the diff --git a/plugins/vibe/skills/vibe/references/ticket-template.md b/plugins/vibe/skills/vibe/references/ticket-template.md index a508345a..e2baa343 100644 --- a/plugins/vibe/skills/vibe/references/ticket-template.md +++ b/plugins/vibe/skills/vibe/references/ticket-template.md @@ -190,10 +190,11 @@ subagent that worked on this.> accepted, which engineering keeps passing with tests in its pull request and changes only with that person's explicit OK> - Feature docs: <each existing product docs page under - `apps/web/content/docs/` edited on this branch for the changed features, - with what changed; every edit is marked as an agent draft that design or - engineering approves before merge, and no page is created. Internal - engineering docs under `docs/` are left to engineering's pull request>, or + `apps/web/content/docs/` that describes a changed feature, with the passage + that no longer matches what was built, for design or engineering to update. + Nothing agent-written goes into public docs, so no page is edited on this + branch. Internal engineering docs under `docs/` are left to engineering's + pull request>, or "None: no existing product docs page describes the changed features" - Missing tests (engineering scope): <every test the session still needs, which engineering writes in its pull request: the behavior and stable row @@ -276,7 +277,7 @@ Yes / no, plus one line when it's a no: 3. Storybook is right. Every new or changed component has stories in the right place, and the controls are usable. (Design) 4. Extended the existing pattern. For example, behavior the children of a shared parent share built once in that parent so each gets it, not one copy per child. 5. Backend is wired correctly. Route, service, shared types, Zod and migration follow our layering, and nothing is left on a stub. (Eng) -6. No invented copy. Every user-visible string came from a person or an existing label. The only exception is the marked agent drafts in existing product docs pages, which need approval before merge. +6. No invented copy. Every user-visible string came from a person or an existing label, and nothing agent-written went into product docs pages. 7. What we had to fix before merge. A count and a short list. (Eng, filled in at merge) ``` diff --git a/plugins/vibe/tools/python/test_vibe_acceptance_criteria_contract.py b/plugins/vibe/tools/python/test_vibe_acceptance_criteria_contract.py index c78fe5f8..8ef5807d 100644 --- a/plugins/vibe/tools/python/test_vibe_acceptance_criteria_contract.py +++ b/plugins/vibe/tools/python/test_vibe_acceptance_criteria_contract.py @@ -81,9 +81,9 @@ def test_each_touched_criterion_is_checked_with_cheap_concrete_evidence_before_d def test_each_check_records_criterion_check_and_result() -> None: text = rule() assert "Append one line per check to the section's `### Checks` log" in text - assert "request ID, criterion ID, the check (test name, command or trace) and the result" in text + assert "request ID, criterion ID, the surface from its `Surfaces:` line, the check (test name, command or trace) and the result" in text assert "`met` or `not met yet`, with its evidence" in text - assert "earlier lines stay" in text + assert "The newest line per criterion and surface is its current result; earlier lines stay" in text def test_an_unmet_criterion_is_fixed_or_reported_never_claimed() -> None: diff --git a/plugins/vibe/tools/python/test_vibe_handoff_docs_tests_control_contract.py b/plugins/vibe/tools/python/test_vibe_handoff_docs_tests_control_contract.py index 8377cbfa..c7c69b02 100644 --- a/plugins/vibe/tools/python/test_vibe_handoff_docs_tests_control_contract.py +++ b/plugins/vibe/tools/python/test_vibe_handoff_docs_tests_control_contract.py @@ -1,8 +1,8 @@ """Contract checks for the 2026-10-13 operator handoff follow-ups. -1. Handoff docs drafts cover only existing product docs pages under - apps/web/content/docs/; the internal engineering docs/ folder belongs to - engineering's pull request. +1. The internal engineering docs/ folder belongs to engineering's pull + request. Since the 2026-10-16 operator ruling no worker edits product docs + pages either; handoff only lists the ones a human needs to update. 2. No worker writes or edits tests in vibe or handoff. Handoff runs the existing tests, fixes the session's own code, and lists the missing tests on the ticket as engineering's scope of work. Only the person's explicit request @@ -56,31 +56,26 @@ def plugin_prose() -> dict[str, str]: return {str(path.relative_to(PLUGIN_ROOT)): path.read_text() for path in paths} -def test_handoff_docs_drafts_cover_only_existing_product_docs_pages() -> None: +def test_handoff_lists_docs_pages_and_leaves_internal_docs_to_engineering() -> None: docs = section(GUARDRAILS.read_text(), "## Feature docs at handoff") - assert "only existing product docs pages under `apps/web/content/docs/`" in docs - assert "Never edit the repo's internal engineering `docs/` folder" in docs - assert "Internal engineering docs belong to engineering's pull request" in docs - assert "or any `docs/` edit, as outside the allowed paths" in docs - assert "`docs/` (`.md`)" not in docs + assert "Nothing agent-written goes into public docs (operator ruling, 2026-10-16)" in docs + assert "the repo's internal engineering `docs/` folder" in docs + assert "only records which existing product docs pages describe a changed feature" in docs step = section(HANDOFF_SKILL.read_text(), "### Existing feature docs") - assert "product docs pages under `apps/web/content/docs/`" in step - assert "never edits the internal engineering `docs/` folder" in step - assert "belong to engineering's pull request" in step + assert "internal engineering `docs/` folder" in step + assert "edits, creates and drafts nothing there" in step writer = flat(agent("vibe-change-worker")) - assert "never edit the internal engineering `docs/` folder, which belongs to engineering's pull request" in writer - assert "any edit under the internal engineering `docs/` folder is blocking" in flat(agent("vibe-guardrails-reviewer")) + assert "records, never edits, the existing product docs pages under `apps/web/content/docs/`" in writer feature_docs = section(TICKET_TEMPLATE.read_text(), "## Handoff") - assert "existing product docs page under `apps/web/content/docs/`" in feature_docs + assert "existing product docs page under `apps/web/content/docs/` that describes a changed feature" in feature_docs assert "Internal engineering docs under `docs/` are left to engineering's pull request" in feature_docs for name, text in plugin_prose().items(): assert "or `docs/`" not in text, name assert "internal docs under `docs/`" not in text, name - def test_the_inventory_has_no_internal_docs_allowance() -> None: inventory = INVENTORY.read_text() - assert 'const PRODUCT_DOCS_PREFIX = "apps/web/content/docs/";' in inventory + assert "Nothing agent-written goes into public docs (operator ruling, 2026-10-16)" in inventory assert '{ prefix: "docs/"' not in inventory assert "HANDOFF_DOCS" not in inventory diff --git a/plugins/vibe/tools/python/test_vibe_notices_docs_frozen_contract.py b/plugins/vibe/tools/python/test_vibe_notices_docs_frozen_contract.py index 8446ff2e..8fed1dc7 100644 --- a/plugins/vibe/tools/python/test_vibe_notices_docs_frozen_contract.py +++ b/plugins/vibe/tools/python/test_vibe_notices_docs_frozen_contract.py @@ -4,9 +4,9 @@ stories without waiting; reuse-only changes skip the notice. 2. A named reference product is researched into a bulleted behavior list that the person sees while the build keeps going. -3. Handoff drafts edits to existing product docs pages only (never the - internal docs/ folder), marked for design or engineering approval, under a - scoped exception to the copy rule. +3. Retired by the 2026-10-16 operator ruling: handoff no longer drafts + product docs edits; test_vibe_review_lessons_contract.py pins the rule that + replaced it. 4. Handoff marks the rows the person accepted as frozen in the attached session decision table, and cl-execute keeps them passing with tests. """ @@ -103,53 +103,6 @@ def test_a_reference_product_is_researched_into_a_behavior_list_the_build_keeps_ assert "list of behaviors to build" in flat(agent("vibe-change-worker")) -def test_feature_docs_edit_only_existing_pages_as_marked_drafts_at_handoff() -> None: - docs = section(GUARDRAILS.read_text(), "## Feature docs at handoff") - for required in ("only existing product docs pages", "`apps/web/content/docs/`", - "Never edit the repo's internal engineering `docs/` folder", - "Never create a page", "`meta.json`", "`apps/web/content/docs/api-reference/`", - "create nothing", "behavior this session changed", - "agent draft for design or engineering to approve before merge", - "{/* Agent draft (<live ticket slug>): approve before merge */}", - "{/* End agent draft */}", "Owned prototype sessions skip this"): - assert required in docs, required - assert "`<!-- -->`" not in docs - step = section(HANDOFF_SKILL.read_text(), "### Existing feature docs") - for required in ("SAME writer", "already exist", "never creates one", "agent draft", - "approves before merge", "Feature docs line"): - assert required in step, required - handoff = section(TICKET_TEMPLATE.read_text(), "## Handoff") - assert "- Feature docs:" in handoff - assert "no page is created" in handoff - assert "the writer's feature docs record" in flat(HANDOFF_SKILL.read_text()) - assert "Feature docs line lists the edited pages" in section(agent("vibe-ticket-worker"), "## Handoff mode") - - -def test_the_docs_exception_is_scoped_wherever_the_copy_rule_is_referenced() -> None: - copy = section(GUARDRAILS.read_text(), "## Copy") - for required in ("One explicit, scoped exception", "granted by operator ruling", "2026-10-13", - "existing product docs pages", "approves before merge", - "not a general license", "no product UI copy", "no new page"): - assert required in copy, required - assert "scoped exception" in section(HANDOFF_SKILL.read_text(), "### Existing feature docs") - assert "gives no license for product copy" in section(HANDOFF_SKILL.read_text(), "### Existing feature docs") - assert "never covers product copy" in flat(agent("vibe-change-worker")) - reviewer = flat(agent("vibe-guardrails-reviewer")) - assert "feature docs drafts are the one scoped exception" in reviewer - assert ("A new docs page, a navigation change, an unmarked docs edit, a docs edit before handoff, " - "or any edit under the internal engineering `docs/` folder is blocking") in reviewer - grading = section(TICKET_TEMPLATE.read_text(), "## Grading") - assert "The only exception is the marked agent drafts in existing product docs pages" in grading - - -def test_the_inventory_admits_existing_docs_edits_only_in_the_handoff_phase() -> None: - inventory = INVENTORY.read_text() - assert 'file.status === "M"' in inventory - assert '"apps/web/content/docs/api-reference/"' in inventory - assert 'const handoffPhase = values.phase !== "build";' in inventory - assert "handoffPhase && !prototype && isExistingDocsEdit(file)" in inventory - - def test_handoff_freezes_the_accepted_rows_in_the_attached_session_table() -> None: frozen = section(QUALITY.read_text(), "### Frozen behavior at handoff") for required in ("`## Frozen Behavior`", "stable row ID of every row the person accepted", diff --git a/plugins/vibe/tools/python/test_vibe_review_lessons_contract.py b/plugins/vibe/tools/python/test_vibe_review_lessons_contract.py new file mode 100644 index 00000000..1c2263bd --- /dev/null +++ b/plugins/vibe/tools/python/test_vibe_review_lessons_contract.py @@ -0,0 +1,279 @@ +"""Contract checks for the 2026-10-16 operator ruling on the reviews of the first +two vibe feature pull requests in symphony-alpha (#8885 grouped list sections, +#8886 bulk add tag). + +Each lesson lands in the agent or reference section that owns the behavior. +Where a rule already existed and was not followed, the owner now carries a +concrete check with evidence. The docs-draft exception of 2026-10-13 is +retired: nothing agent-written goes into public docs. +""" + +from __future__ import annotations + +from pathlib import Path + +from vibe_contract_text import agent, flat, section + +PLUGIN_ROOT = Path(__file__).resolve().parents[2] +REFERENCES = PLUGIN_ROOT / "skills" / "vibe" / "references" +GUARDRAILS = REFERENCES / "guardrails.md" +DESIGN_PASS = REFERENCES / "design-pass.md" +QUALITY = REFERENCES / "quality-loop.md" +TICKET_TEMPLATE = REFERENCES / "ticket-template.md" +HANDOFF_SKILL = PLUGIN_ROOT / "skills" / "handoff" / "SKILL.md" +INVENTORY = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "handoff-inventory.mjs" +CL_EXECUTE_PR = PLUGIN_ROOT / "skills" / "cl-execute" / "references" / "implementation-review-pr.md" +RULING = "operator ruling, 2026-10-16" + + +def assert_all(text: str, required: tuple[str, ...]) -> None: + for phrase in required: + assert phrase in text, phrase + + +def reviewer_check(number: int) -> str: + """One numbered check of the guardrails reviewer, flattened.""" + text = section(agent("vibe-guardrails-reviewer"), "## Check, for added or changed lines only") + start = text.index(f" {number}. ") + following = text.find(f" {number + 1}. ", start) + return text[start:] if following == -1 else text[start:following] + + +def test_main_sync_capability_removal_is_a_reviewer_check_on_every_merge() -> None: + check = reviewer_check(13) + assert_all(check, ( + RULING, + "check every main-sync merge in the session, not only the latest", + "--merges", + "diff the merge result against main's side", + "is read-only, disabled, hidden or gone after the merge is blocking capability removal", + "so is a test the merge or a later session commit changed to pin the reduced behavior", + "Quote both sides.", + )) + writer = flat(agent("vibe-change-worker")) + assert "A conflict is never resolved by taking away what main ships" in writer + assert "no test is changed to pin a reduced behavior" in writer + handoff = flat(HANDOFF_SKILL.read_text()) + assert "Every main sync is checked for capability removal, this one included" in handoff + assert "dispatch `vibe-guardrails-reviewer` for its check 13 on that merge" in handoff + + +def test_version_history_is_one_entry_at_the_next_version_and_checked() -> None: + rule = section(GUARDRAILS.read_text(), "## Storybook taxonomy") + assert_all(rule, ( + "A session is one release: exactly one new Version History entry, at the next version", + "special-case a validator, test or key (a duplicate-key exception) so two entries can coexist", + "After a main sync, the session's entry moves to the next version after main's.", + )) + check = reviewer_check(10) + assert_all(check, ( + "`apps/storybook/stories/start-here-version-history.stories.tsx`", + "More than one new entry, an entry whose version is not the next after the base's `version`", + "special-cased so two entries can coexist (a duplicate-key exception) is blocking", + )) + assert "Version History: the session's new entries" in flat(agent("vibe-storybook-decomposer")) + + +def test_a_compile_only_repair_rewords_no_prose_and_is_read_word_by_word() -> None: + rule = section(GUARDRAILS.read_text(), "## Storybook taxonomy") + assert "A compile-only repair rewords no comment, description, docs parameter or story text either" in rule + check = reviewer_check(10) + assert "`git -C <wt> diff --word-diff <base> <file>`" in check + assert "a reworded comment, description, docs parameter or story text is blocking" in check + assert "A compile-only repair rewords no comment, description, docs parameter or story text" in flat(agent("vibe-change-worker")) + + +def test_lost_and_stale_story_ids_are_searched_and_block() -> None: + rule = section(GUARDRAILS.read_text(), "## Storybook taxonomy") + assert "Every removed or renamed story ID is searched for (`rg`) across `e2e/`" in rule + check = reviewer_check(10) + assert_all(check, ( + "list every story ID on the base that the session's index no longer has", + "`rg` the old ID across `e2e/`, the test files, `docs/`, `apps/web/content/docs/`, `.claude/` and the story files", + "each hit still naming it is blocking", + )) + assert "`scripts/index-diff.mjs` of the session's built index against the base's" in flat(agent("vibe-storybook-decomposer")) + assert "every reference to a removed or renamed ID still found in `e2e/`" in flat(HANDOFF_SKILL.read_text()) + + +def test_a_slot_alone_is_not_a_shared_owner() -> None: + rules = section(DESIGN_PASS.read_text(), "## Owner rules") + assert_all(rules, ( + "A slot alone is not an owner", + "(selection, grouping, collapse, paging) live in the parent or in one shared hook", + "Two children that each keep their own copy of that state behind the parent's slot", + )) + check = reviewer_check(8) + assert_all(check, ( + "Slot without an owner", + "Two children that each hold their own copy behind the parent's slot is blocking, the ISS-12137 red flag", + "Private copies", + "a duplicated band or status icon map", + )) + + +def test_each_acceptance_criterion_maps_to_every_surface_it_names() -> None: + rule = section(QUALITY.read_text(), "## Acceptance criteria before ready") + assert_all(rule, ( + "Each criterion also carries a `Surfaces:` line", + "a list and a board are two", + "A criterion is met only when every surface on that line has its own `met` check", + )) + adversary = flat(agent("vibe-adversarial-reviewer")) + assert_all(adversary, ( + "Acceptance criteria per surface", + "A surface with no such code, or whose only `### Checks` line traces a different surface, is a finding", + )) + + +def test_nothing_agent_written_goes_into_public_docs() -> None: + docs = section(GUARDRAILS.read_text(), "## Feature docs at handoff") + assert_all(docs, ( + f"Nothing agent-written goes into public docs ({RULING}).", + "No worker edits, creates or drafts anything under `apps/web/content/docs/`", + "it never carries drafted words", + "reports any change under either folder as outside the allowed paths in every phase", + )) + copy = section(GUARDRAILS.read_text(), "## Copy") + assert "There is no exception" in copy + assert "scoped exception" not in copy + table = next(line for line in GUARDRAILS.read_text().splitlines() if line.startswith("| `packages/app/**`")) + assert "`apps/web/content/docs/**`" in table.split("|")[2] + assert "report every path it prints as blocking, in every phase, whatever markers it carries" in reviewer_check(4) + inventory = INVENTORY.read_text() + assert "isExistingDocsEdit" not in inventory + assert "PRODUCT_DOCS_PREFIX" not in inventory + for path in (GUARDRAILS, HANDOFF_SKILL, TICKET_TEMPLATE, PLUGIN_ROOT / "README.md"): + assert "Agent draft (<live ticket slug>)" not in path.read_text(), path.name + for name in ("vibe-change-worker", "vibe-guardrails-reviewer", "vibe-ticket-worker"): + text = flat(agent(name)) + assert "marked agent draft" not in text, name + assert "feature docs drafts are the one scoped exception" not in text, name + + +def test_flag_retirement_reads_linked_ticket_comments_and_a_hold_needs_a_human() -> None: + check = reviewer_check(14) + assert_all(check, ( + "read the comments on every ticket linked to the flag", + "`get-document-comments`", + "A recorded hold, rollout pause or parity list on the flag with no later human answer", + "is a blocking `engineering decision` finding", + "even when an acceptance criterion asks for the old path to go", + )) + writer = flat(agent("vibe-change-worker")) + assert "Before you delete a feature flag's read, key constant or gate, read the comments on every ticket linked to it" in writer + assert "means the flag stays" in writer + + +def test_leftovers_after_an_extraction_are_a_red_flag_with_a_concrete_check() -> None: + flags = section(DESIGN_PASS.read_text(), "### Red flags") + assert_all(flags, ( + "Leftovers: what an extraction, move or deletion leaves behind", + "a re-export alias", + "a loop or `each` table over the one arm left after a matrix arm was deleted", + "a test still named or placed after a module that was deleted or moved", + "Hard-coded host keys", + )) + check = reviewer_check(8) + assert "`git -C <wt> diff --name-status -M <base>`" in check + assert "a re-export alias, an export with no remaining importer, a prop no caller passes" in check + assert "An extraction, move or deletion is finished only when nothing it orphaned is left" in flat(agent("vibe-change-worker")) + + +def test_reviewer_flags_checks_nobody_asked_for_and_test_oracles_from_production_code() -> None: + assert "Never add a check nobody asked for" in section(GUARDRAILS.read_text(), "## Checks") + check = reviewer_check(9) + assert_all(check, ( + "So is a new check nobody asked for", + "a shared test helper widened to support one", + "one computed by calling production code", + )) + + +def test_pull_request_tests_follow_the_review_rules() -> None: + writer = flat(agent("vibe-change-worker")) + assert_all(writer, ( + "Every entry on the missing tests list is written in this turn.", + "Each test goes in the suite and lane that owns its subject", + "Expected values are literals or the spec's words, never computed by calling production code", + "a windowed or paged fixture is built with the repo's real paging helper", + "every acceptance criterion has at least one test whose name or `describe` title carries its ID", + "tested through the real web and Desktop host adapters", + "a same-route link that changes only the query and the Back button", + )) + verify = flat(agent("vibe-verify-worker")) + assert_all(verify, ( + "names the acceptance criterion IDs it covers", + "the same-route link and Back cases", + "an entry still listed as missing there is a finding", + )) + + +def test_verify_runs_sort_order_tests_containerized_e2e_and_reads_ci_tiers() -> None: + verify = flat(agent("vibe-verify-worker")) + assert_all(verify, ( + "__tests__/preview-story-sort-order.test.ts", + "runs at handoff through the repo's containerized path (the `local-ci` skill, `dagger call e-2-e`)", + "read `.github/pr-test-event-tiers.md` and cite the tier", + )) + handoff = flat(HANDOFF_SKILL.read_text()) + assert "`dagger call e-2-e`" in handoff + assert "`.github/pr-test-event-tiers.md`" in handoff + + +def test_storybook_owner_checks_states_names_fixtures_and_surfaces() -> None: + decomposer = flat(agent("vibe-storybook-decomposer")) + assert_all(decomposer, ( + "A whole-page flow (the page a route renders, or several sections composed into one flow) is never a Composite entry", + "gets a story on the component consumers render", + "built by calling the repo's real paging helper", + "A story's name says only what its args and play function show and assert", + "never cast", + )) + + +def test_adversary_checks_selections_gates_and_per_row_enums() -> None: + adversary = flat(agent("vibe-adversarial-reviewer")) + assert_all(adversary, ( + "must account for every selected row, including the ones it cannot act on", + "a product question for the writer's research path", + "gated on the host's capability", + "a gate one host has and the other lacks is a finding", + "Parse it per row with a safe fallback for an unknown value", + )) + assert "An action over selected rows accounts for every selected row" in flat(agent("vibe-change-worker")) + + +def test_reuse_and_wording_checks_grep_shared_owners_first() -> None: + assert "`shared/auth` included) and the existing `use-*` hooks (such as `use-tags`)" in reviewer_check(1) + assert "a second spelling of wording a shared helper or label map already renders is blocking" in reviewer_check(4) + check = reviewer_check(7) + assert "any `as` cast, stories included" in check + assert "`rg` that literal across `apps/app`, `apps/desktop/src/renderer` and `packages/app`" in check + writer = flat(agent("vibe-change-worker")) + assert "`rg` `packages/app/shared` (`shared/auth` included) and the existing `use-*` hooks (such as `use-tags`)" in writer + + +def test_pr_body_files_are_named_per_ticket_and_read_back_before_gh() -> None: + environment = section(agent("vibe-environment-worker"), "## Pull-request mode") + assert_all(environment, ( + "named for the live ticket (`pr-body-<live ticket slug>.md`), never a shared name such as `pr-body.md`", + "Read the file back right before the `gh` command", + "the file named and re-read as step 4 says", + )) + cl_execute = flat(CL_EXECUTE_PR.read_text()) + assert "(`pr-body-<ticket slug>.md`, never a shared name such as `pr-body.md`)" in cl_execute + assert "read it back right before `gh pr create` or `gh pr edit --body-file`" in cl_execute + assert "read back right before the command" in flat(agent("vibe-seed-pr-worker")) + + +def test_new_text_uses_no_emdashes_or_double_hyphen_dashes() -> None: + for name in ("vibe-guardrails-reviewer", "vibe-adversarial-reviewer", "vibe-change-worker", + "vibe-verify-worker", "vibe-storybook-decomposer", "vibe-environment-worker", + "vibe-ticket-worker"): + text = agent(name) + assert "—" not in text, name + assert " -- " not in text, name + for path in (GUARDRAILS, DESIGN_PASS, QUALITY, HANDOFF_SKILL, TICKET_TEMPLATE): + assert "—" not in path.read_text(), path.name + assert " -- " not in path.read_text(), path.name