From 20ea65eea60bf4ff0732581f9c6bfa9bb4ac843d Mon Sep 17 00:00:00 2001 From: Daniel Ochoa Date: Fri, 16 Oct 2026 11:50:15 -0500 Subject: [PATCH 1/2] fix(vibe): Judge Storybook stories by the lifecycle scheme of the session's base symphony-alpha #8867 (ISS-12610) made the plain roots current and moved the old ones to v1.0 with a deprecated tag. The handoff inventory read every plain-root story as frozen v1.0, so a synced session was blocked. - Read the scheme from the base's story-group-order.ts: the lifecycle scheme takes its frozen tag and v1.0 root suffix from StorybookLifecycle and storybookBaselineVersion; an older base keeps the v1.0/v2.0 rules. - Keep the freeze, the compile-only repair, the retitle check against the synced base and the Start Here exemption on both schemes. - Translate the placement rules in guardrails, the decomposer, the writer, the reviewer, design pass, handoff and README. --- plugins/vibe/README.md | 21 +- plugins/vibe/agents/vibe-change-worker.md | 18 +- .../vibe/agents/vibe-guardrails-reviewer.md | 22 +- plugins/vibe/agents/vibe-primitive-worker.md | 4 +- .../vibe/agents/vibe-storybook-decomposer.md | 37 +-- plugins/vibe/evals/codex/README.md | 2 +- plugins/vibe/evals/codex/test_run.py | 2 +- .../graders/keeps-deprecated-file-frozen.md | 6 + .../graders/lifecycle-tag.md | 5 + .../graders/no-deprecated-or-version-tag.md | 5 + .../graders/title-has-no-version.md | 5 + .../graders/title-in-current-root.md | 5 + .../prompt.md | 29 ++ .../graders/no-deprecated-or-version-tag.md | 5 + .../graders/not-a-retitle.md | 6 + .../graders/plain-root-same-path.md | 5 + .../graders/stable-tag.md | 5 + .../prompt.md | 28 ++ plugins/vibe/skills/handoff/SKILL.md | 4 +- ...andoff-inventory-storybook-scheme.test.mjs | 261 ++++++++++++++++++ .../handoff/scripts/handoff-inventory.mjs | 99 +++---- .../scripts/handoff-inventory.test.mjs | 3 + .../handoff/scripts/storybook-taxonomy.mjs | 124 +++++++++ .../skills/vibe/references/design-pass.md | 5 +- .../vibe/skills/vibe/references/guardrails.md | 55 ++-- ...st_vibe_start_here_unversioned_contract.py | 16 +- ...e_storybook_existing_component_contract.py | 70 +++-- .../test_vibe_storybook_placement_contract.py | 10 +- .../test_vibe_storybook_taxonomy_contract.py | 79 +++++- ...st_vibe_v1_compile_only_repair_contract.py | 8 +- 30 files changed, 768 insertions(+), 176 deletions(-) create mode 100644 plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/keeps-deprecated-file-frozen.md create mode 100644 plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/lifecycle-tag.md create mode 100644 plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/no-deprecated-or-version-tag.md create mode 100644 plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-has-no-version.md create mode 100644 plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-in-current-root.md create mode 100644 plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/prompt.md create mode 100644 plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/no-deprecated-or-version-tag.md create mode 100644 plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/not-a-retitle.md create mode 100644 plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/plain-root-same-path.md create mode 100644 plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/stable-tag.md create mode 100644 plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/prompt.md create mode 100644 plugins/vibe/skills/handoff/scripts/handoff-inventory-storybook-scheme.test.mjs create mode 100644 plugins/vibe/skills/handoff/scripts/storybook-taxonomy.mjs diff --git a/plugins/vibe/README.md b/plugins/vibe/README.md index f50711ee..de34f1e9 100644 --- a/plugins/vibe/README.md +++ b/plugins/vibe/README.md @@ -156,15 +156,22 @@ and Storybook skill (`.claude/skills/storybook/`, whose placement workflows and index scripts it uses) where they say how and mirroring that precedent's structure, never its root or version, where they do not; the story files the writer matches are the same capability's, not the same folder's. Stories cover the components consumers -render, not internal render pieces. A v1.0 story file is never edited beyond -a compile-only repair, so new states of its component go in a new v2.0 entry -holding only them. New states +render, not internal render pieces. New stories go in the current roots +(`Composites/`, `Surfaces/` and the other plain levels) with one lifecycle +tag: `stable`, `preview` under Labs, or `experimental` for design-only work. +A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`) is +never edited beyond a compile-only repair, so new states of its component go +in a new entry in the current root holding only them. New states of any other existing component go in its existing story file under the same title and tag, since a retitle breaks every link to its stories; handoff blocks any other edit of a v1.0 story file except a compile-only repair, and a retitle unless it is a sibling replacement's entry. 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). The writer records it in its local plan; +retitle checks (design ruling, 2026-10-06). Handoff judges story files by +the scheme of the session's base, read from that base's +`apps/storybook/story-group-order.ts`, so a session still based before +symphony-alpha #8867 keeps the `v1.0`/`v2.0` rules until it syncs main, and a +sync is never blamed for the renames it imported. The writer records it in its local plan; a conflict goes on the live ticket by audience, design conflicts under Design Review and everything else on the Engineering checklist, never to the person. @@ -314,6 +321,12 @@ cd plugins claude plugin eval . --eval-dir vibe/evals --tag visual-cues --scaffold --allow-tools Bash Write Edit --model opus --runs 3 -j 4 --no-publish --ablation none ``` +The two cases tagged `storybook` rehearse placement on symphony-alpha's +lifecycle scheme (#8867): new states for a component whose story file is +deprecated go in a new entry in a current root, and after a main sync a +session's own ` v2.0/` entry moves to the plain root with a lifecycle +tag without counting as a retitle. + ## Runtime Files Version `1.0.14` contains 10 skills and 16 agents. It has no standalone diff --git a/plugins/vibe/agents/vibe-change-worker.md b/plugins/vibe/agents/vibe-change-worker.md index 814e69b1..ed261946 100644 --- a/plugins/vibe/agents/vibe-change-worker.md +++ b/plugins/vibe/agents/vibe-change-worker.md @@ -133,16 +133,22 @@ for a second source author. For a missing primitive use the read-only steward spec and build its complete states/stories yourself. Keep Storybook collected locations, catalog and taxonomy correct; all resulting source edits are yours. Read the repo's `apps/storybook/TAXONOMY.md` and follow it: every new story -goes in a v2.0 root, in its page tree, tagged `v2.0`, with the Storybook -release bump it requires, never in a frozen v1.0 root; new states of an -existing component go in a new v2.0 entry holding only them when its story -file is v1.0, which you never edit beyond a compile-only repair, and otherwise -in its existing story file, title and tag unchanged +goes in a current root (the plain level, such as `Composites/`), in its page +tree, with exactly one lifecycle tag (`stable` for a shipped surface, +`preview` under `Labs`, `experimental` for a design-only or not-in-the-app +entry) and the Storybook release bump it requires, never in a frozen v1.0 +root; new states of an existing component go in a new entry in the current +root holding only them when its story file is v1.0 (under a ` v1.0/` +root or tagged `deprecated`), which you never edit beyond a compile-only +repair, and otherwise in its existing story file, title and tag unchanged (`../skills/vibe/references/guardrails.md`, "Storybook taxonomy"). Never avoid 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). The story +in the change log (design ruling, 2026-10-15). 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 files you match are the same capability's, not the same folder's (same section). diff --git a/plugins/vibe/agents/vibe-guardrails-reviewer.md b/plugins/vibe/agents/vibe-guardrails-reviewer.md index 847eded0..afffa3b5 100644 --- a/plugins/vibe/agents/vibe-guardrails-reviewer.md +++ b/plugins/vibe/agents/vibe-guardrails-reviewer.md @@ -108,15 +108,23 @@ closedloop-graph is required, per `../skills/vibe/references/closedloop-graph.md lint or type suppression or a raised allowlist count added to make a check pass (`guardrails.md`, "Checks") is blocking too. 10. Storybook taxonomy (`guardrails.md`, "Storybook taxonomy"): read the - repo's `apps/storybook/TAXONOMY.md`. A new story file titled - under or tagged for a frozen v1.0 root, or any edit or move of a story - file that is v1.0 on main (the inventory's `v1Stories`, reason `frozen`), - is blocking; that component's new states belong in a new v2.0 entry - holding only them, with no version number in its name. Confirm each of + 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 + titled under or tagged for a frozen v1.0 root (a ` v1.0/` root or + the `deprecated` tag; on a version-scheme base, a plain root or the + `v1.0` tag), or any edit or move of a story file that is v1.0 on main + (the inventory's `v1Stories`, reason `frozen`), is blocking; that + component's new states belong in a new entry in the current root holding + 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 v2.0 story outside its page tree, without the `v2.0` - tag, or added, moved or renamed without the release bump the doc names. + blocking. So is a new story outside the current roots' page tree (on a + lifecycle-scheme base, a title still under a ` 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. 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 diff --git a/plugins/vibe/agents/vibe-primitive-worker.md b/plugins/vibe/agents/vibe-primitive-worker.md index 82442168..d1109de2 100644 --- a/plugins/vibe/agents/vibe-primitive-worker.md +++ b/plugins/vibe/agents/vibe-primitive-worker.md @@ -49,8 +49,8 @@ skill (`.claude/skills/storybook/SKILL.md`, `references/gotchas.md`, case the screen needs, with one story per variant, size, and state. 3. Stories in a collected location with controls, and a `DS_*` taxonomy entry for a new `packages/design-system/components/ui/*.tsx`. No tests. - Place them per the repo's `apps/storybook/TAXONOMY.md`: a v2.0 root, the - `v2.0` tag and the release bump, never a frozen v1.0 root + Place them per the repo's `apps/storybook/TAXONOMY.md`: a current root, + one lifecycle tag and the release bump, never a frozen v1.0 root (`guardrails.md`, "Storybook taxonomy"). 4. `pnpm --filter storybook catalog:sync`, `pnpm --filter storybook validate:catalog`, `pnpm --filter storybook test`, and Biome on your files, until all pass. diff --git a/plugins/vibe/agents/vibe-storybook-decomposer.md b/plugins/vibe/agents/vibe-storybook-decomposer.md index bbc2a935..e7f0e932 100644 --- a/plugins/vibe/agents/vibe-storybook-decomposer.md +++ b/plugins/vibe/agents/vibe-storybook-decomposer.md @@ -47,9 +47,11 @@ taxonomy"). For placement, use that skill's placement pieces: Read `apps/storybook/AGENTS.md` for where stories must live to be collected, and the repo's -`apps/storybook/TAXONOMY.md`, which places every new story: a v2.0 root, its -page tree, the `v2.0` tag and the release bump, never a frozen v1.0 root -(`../skills/vibe/references/guardrails.md`, "Storybook taxonomy"). An +`apps/storybook/TAXONOMY.md`, which places every new story: a current root, +its page tree, one lifecycle tag and the release bump, never a frozen v1.0 +root (`../skills/vibe/references/guardrails.md`, "Storybook taxonomy"). Read +that doc as the session's base has it: on a base older than the lifecycle +scheme it still places new work under ` v2.0/` with the `v2.0` tag. An existing component's story file keeps its title and tag ("Precedent search", step 4). Read the same session decision table and affected/interacting row IDs. Use @@ -78,16 +80,18 @@ Do this first, in both modes. 3. Report everything found, not a pick: the component that provides the capability and its path; how each host consumes it (a prop, a hook, a wrapper, its own copy); and every existing story that exercises it, with - its title, story names, version tag and file. + its title, story names, lifecycle tag and file. 4. Decide in this order. First (design ruling, 2026-10-15): a change to an existing component (new states or variants, an extra column action, a new footer) never retitles, re-tags or moves its existing story file. Storybook builds story IDs from the title path, so a retitle breaks every - link to those stories. A v1.0 story file is frozen and never edited, not - even to add a story: the new states go in a new v2.0 entry for that - component holding only the new states, its name descriptive, permanent, - per `apps/storybook/TAXONOMY.md` and without a version number, and the - v1.0 stories stay where they are. The one exception is a compile-only + link to those stories. A v1.0 story file (under a ` v1.0/` root or + tagged `deprecated`) is frozen and never edited, not even to add a story: + the new states go in a new entry for that component in the current root + holding only the new states, its name descriptive, permanent, per + `apps/storybook/TAXONOMY.md` and without a version number, with the + lifecycle tag its place in the tree takes, and the v1.0 stories stay where + they are. The one exception is a compile-only repair after a rename elsewhere: only mechanical changes (prop renames, imports, types), no story added or removed, no title, story ID or tag change and no visual change, since the freeze protects what its stories @@ -98,25 +102,26 @@ Do this first, in both modes. new states as extra stories, its title, tag and existing story names unchanged. Otherwise only a new component, or a sibling replacement such as a V3, gets a new - entry and v2.0 placement. This outranks reading a touched story file as - taking the v2.0 version. Then (operator ruling, 2026-10-14): where + entry and a place in the current roots. This outranks reading a touched + story file as moving to the current roots or taking a new lifecycle tag. + Then (operator ruling, 2026-10-14): where the repo's `apps/storybook/TAXONOMY.md` or `.claude/skills/storybook/` already say how, follow them; where they do not, mirror the precedent: the same owner, the same way of consuming it, the component that carries its stories, how granular they are and which states get variants. Mirroring copies - structure only, never location or version: the root, version tag and v2.0 + structure only, never location or lifecycle: the root, lifecycle tag and tree position always come from `apps/storybook/TAXONOMY.md`, and a precedent in a v1.0 root never puts new work in v1.0. Either way, stories document the components consumers actually render, one story per state, never internal render pieces: a shared capability's generic mechanics stay 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 v2.0 + configuration (its dimensions, labels, icons). It fits the doc's Composites row, "a section of a page". 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 - the same providing component, then already in the v2.0 tree, then most + the same providing component, then already in the current roots, then most recently changed (the latest commit on main to its story file); name the others. Report each side with its paths and titles, and its audience, `design` or @@ -131,9 +136,9 @@ precedent path, it follows. ``` Placement: - Capability: -- Precedents: , or none found +- Precedents: , or none found - Owner: , per or mirroring -- Stories: , root and tag per , structure per or mirroring ; an existing file's title and tag unchanged, a v1.0 file not edited beyond a compile-only repair (step 4) +- Stories: , root and tag per , structure per or mirroring ; an existing file's title and tag unchanged, a v1.0 file not edited beyond a compile-only repair (step 4) - Conflict: , or none ``` diff --git a/plugins/vibe/evals/codex/README.md b/plugins/vibe/evals/codex/README.md index 9fb328f2..911e18c3 100644 --- a/plugins/vibe/evals/codex/README.md +++ b/plugins/vibe/evals/codex/README.md @@ -44,7 +44,7 @@ The cheap grader never decides routing, file correctness or reference evidence. Visual cases use the same stand-in repository and Pillow renderer as Claude. 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 22 other cases rehearse one reply from supplied session state in read-only +The 24 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. The console reports each case and failure. Private artifacts include input, diff --git a/plugins/vibe/evals/codex/test_run.py b/plugins/vibe/evals/codex/test_run.py index 5f759153..529abb86 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) == 26 + assert len(found) == 28 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-storybook-new-states-for-deprecated-component/graders/keeps-deprecated-file-frozen.md b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/keeps-deprecated-file-frozen.md new file mode 100644 index 00000000..9fd3113e --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/keeps-deprecated-file-frozen.md @@ -0,0 +1,6 @@ +--- +type: regex +match: contains +flags: i +pattern: 'EDIT_EXISTING_FILE:\s*no\b' +--- diff --git a/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/lifecycle-tag.md b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/lifecycle-tag.md new file mode 100644 index 00000000..6b70fef8 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/lifecycle-tag.md @@ -0,0 +1,5 @@ +--- +type: regex +match: contains +pattern: 'TAGS:[^\n]*\b(stable|preview)\b' +--- diff --git a/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/no-deprecated-or-version-tag.md b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/no-deprecated-or-version-tag.md new file mode 100644 index 00000000..5ddd6400 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/no-deprecated-or-version-tag.md @@ -0,0 +1,5 @@ +--- +type: regex +match: not_contains +pattern: 'TAGS:[^\n]*(deprecated|v[12]\.0)' +--- diff --git a/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-has-no-version.md b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-has-no-version.md new file mode 100644 index 00000000..f851cafd --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-has-no-version.md @@ -0,0 +1,5 @@ +--- +type: regex +match: not_contains +pattern: 'TITLE:[^\n]*v[12]\.0' +--- diff --git a/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-in-current-root.md b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-in-current-root.md new file mode 100644 index 00000000..50005028 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/graders/title-in-current-root.md @@ -0,0 +1,5 @@ +--- +type: regex +match: contains +pattern: 'TITLE:\s*["''`]?Composites/' +--- diff --git a/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/prompt.md b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/prompt.md new file mode 100644 index 00000000..81d4901d --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-new-states-for-deprecated-component/prompt.md @@ -0,0 +1,29 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 12 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, storybook] +--- + +This is a rehearsal of one Storybook placement decision in a vibe session, with +no real environment. Load the instructions with the Skill tool (`vibe:vibe`) +and decide exactly as those instructions say. 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. + +Session state: +- App session `documents-grouped-rows`, seeded data, live ticket ISS-4400, branch `vibe/documents-grouped-rows`. +- The session's base is symphony-alpha main after #8867 (Storybook 3.0.0, the lifecycle scheme): its `apps/storybook/story-group-order.ts` exports `StorybookLifecycle` with `Deprecated: "deprecated"` and `storybookBaselineVersion = "v1.0"`. +- The person asked: "group the rows on the Documents list by status, with a collapsible header per group". The writer built it on the shared Grid Table. +- The Grid Table's only story file on main is `apps/storybook/stories/grid-table.stories.tsx`, whose meta is `title: "Composites v1.0/Data Display/Tables/Grid Table"`, `tags: ["deprecated", "autodocs"]`. +- The Documents list is a page in the live app; in the current page tree it is `Labs/Documents/Documents List`. +- The new states to document are "Grouped" and "Grouped Collapsed". + +Decide where the two new stories go. Answer in exactly this form, and nothing else: +EDIT_EXISTING_FILE: +TITLE: +TAGS: +WHY: diff --git a/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/no-deprecated-or-version-tag.md b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/no-deprecated-or-version-tag.md new file mode 100644 index 00000000..5ddd6400 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/no-deprecated-or-version-tag.md @@ -0,0 +1,5 @@ +--- +type: regex +match: not_contains +pattern: 'TAGS:[^\n]*(deprecated|v[12]\.0)' +--- diff --git a/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/not-a-retitle.md b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/not-a-retitle.md new file mode 100644 index 00000000..e73e5dd5 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/not-a-retitle.md @@ -0,0 +1,6 @@ +--- +type: regex +match: contains +flags: i +pattern: 'RECORD_AS_REPLACEMENT:\s*no\b' +--- diff --git a/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/plain-root-same-path.md b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/plain-root-same-path.md new file mode 100644 index 00000000..e3018f40 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/plain-root-same-path.md @@ -0,0 +1,5 @@ +--- +type: regex +match: contains +pattern: 'TITLE:\s*["''`]?Composites/Shared/Artifact List/Documents View\b' +--- diff --git a/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/stable-tag.md b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/stable-tag.md new file mode 100644 index 00000000..073393bb --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/graders/stable-tag.md @@ -0,0 +1,5 @@ +--- +type: regex +match: contains +pattern: 'TAGS:[^\n]*\bstable\b' +--- diff --git a/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/prompt.md b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/prompt.md new file mode 100644 index 00000000..c87aea56 --- /dev/null +++ b/plugins/vibe/evals/pm-storybook-synced-main-moves-own-entries/prompt.md @@ -0,0 +1,28 @@ +--- +runs: 1 +plugins: ["../..", "../../../closedloop-core"] +max_turns: 12 +timeout_seconds: 300 +allowed_tools: [Skill, Read, Grep, Glob] +tags: [pm, storybook] +--- + +This is a rehearsal of one Storybook decision in a vibe session at handoff, +with no real environment. Load the instructions with the Skill tool +(`vibe:handoff`) and decide exactly as those instructions say. 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. + +Session state: +- App session `grouped-list-sections`, seeded data, live ticket ISS-4410, branch `vibe/grouped-list-sections`. +- The session started on a main older than symphony-alpha #8867. On that base the writer added one new story file, `packages/app/documents/components/table/documents-view.stories.tsx`, with `title: "Composites v2.0/Shared/Artifact List/Documents View"` and `tags: ["autodocs", "v2.0"]`. That file is not on main. +- Handoff just synced main, which now includes #8867 (Storybook 3.0.0, the lifecycle scheme): the `v2.0` roots are now the plain roots, the old plain roots are now ` v1.0`, and every story file carries one lifecycle tag. The merge is committed. +- `pnpm --filter storybook validate:catalog` now fails on that one file: its title root `Composites v2.0` is not a root, and its meta has no lifecycle tag. +- The Documents View is a section of shipped list pages in the live app. + +Decide what the writer does to that story file. Answer in exactly this form, and nothing else: +TITLE: +TAGS: +RECORD_AS_REPLACEMENT: +WHY: diff --git a/plugins/vibe/skills/handoff/SKILL.md b/plugins/vibe/skills/handoff/SKILL.md index df3d4be0..00907047 100644 --- a/plugins/vibe/skills/handoff/SKILL.md +++ b/plugins/vibe/skills/handoff/SKILL.md @@ -344,7 +344,9 @@ Every new story follows the repo's `apps/storybook/TAXONOMY.md`, a v1.0 story file is never edited beyond a compile-only repair and any other existing story file keeps its title and tag (`../vibe/references/guardrails.md`, "Storybook taxonomy"); the inventory's -`v1Stories` and `retitledStories` block handoff. Its `compileOnlyV1Repairs` +`v1Stories` and `retitledStories` block handoff, judged by the scheme of the +session's base (`storybookScheme`: from symphony-alpha #8867 on, the +` 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 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 diff --git a/plugins/vibe/skills/handoff/scripts/handoff-inventory-storybook-scheme.test.mjs b/plugins/vibe/skills/handoff/scripts/handoff-inventory-storybook-scheme.test.mjs new file mode 100644 index 00000000..4d53917f --- /dev/null +++ b/plugins/vibe/skills/handoff/scripts/handoff-inventory-storybook-scheme.test.mjs @@ -0,0 +1,261 @@ +import assert from "node:assert/strict"; +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { git, makeCheckout, makeHome, runNode } from "../../vibe/scripts/test-fixtures.mjs"; +import { STORY_GROUP_ORDER_PATH, StorybookScheme, storybookSchemeOf } from "./storybook-taxonomy.mjs"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const INVENTORY = path.join(HERE, "handoff-inventory.mjs"); +const SESSIONS = path.join(HERE, "..", "..", "vibe", "scripts", "vibe-sessions.mjs"); + +// Excerpts of symphony-alpha's apps/storybook/story-group-order.ts as each +// scheme declares itself: before #8867 (ISS-12610) there is no lifecycle and +// the current roots carry `v2.0`; from #8867 on the file exports the lifecycle +// tags and the v1.0 baseline version. +const VERSION_SCHEME_ORDER = [ + `export const storybookPatternsV2Root = "Patterns v2.0";`, + `export const storybookNextVersion = "v2.0";`, + "export function storybookVersionedRoot(level: string, version: string) {", + " return `${level} ${version}`;", + "}", + "", +].join("\n"); +const LIFECYCLE_SCHEME_ORDER = [ + "export const StorybookLifecycle = {", + ` Experimental: "experimental",`, + ` Preview: "preview",`, + ` Stable: "stable",`, + ` Deprecated: "deprecated",`, + "} as const;", + "export type StorybookLifecycle =", + " (typeof StorybookLifecycle)[keyof typeof StorybookLifecycle];", + "", + "export const storybookLifecycleTags: readonly StorybookLifecycle[] =", + " Object.values(StorybookLifecycle);", + "", + `/** The v1.0 baseline: Deprecated, titled under " v1.0". */`, + `export const storybookBaselineVersion = "v1.0";`, + "", + `/** The current release: titled under the plain "" root. */`, + `export const storybookNextVersion = "v2.0";`, + "", +].join("\n"); +const LIFECYCLE_SUMMARY = { + name: StorybookScheme.Lifecycle, + frozenTag: "deprecated", + frozenRoots: " v1.0", + lifecycleTags: ["experimental", "preview", "stable", "deprecated"], +}; + +// A story file whose meta keeps its title and tags apart from its stories, so +// a main import that retitles it and a session that adds a story merge cleanly. +const story = (title, tags, stories = ["Default"]) => + [ + `import type { Meta } from "@storybook/react";`, + "const meta = {", + ` title: "${title}",`, + ` tags: [${tags.map((tag) => `"${tag}"`).join(", ")}],`, + " parameters: { layout: \"padded\" },", + " args: { label: \"Row\" },", + " argTypes: {},", + "} satisfies Meta;", + "export default meta;", + "", + ...stories.map((name) => `export const ${name} = { args: { label: "${name}" } };`), + "", + ].join("\n"); + +function setup(t, slug, files) { + const fixture = makeHome("handoff-storybook-scheme-"); + t.after(fixture.cleanup); + const checkout = path.join(fixture.home, "Documents", "Closedloop.ai - Active Work", "symphony-alpha"); + makeCheckout({ root: fixture.root, home: fixture.home, checkout, files: { "apps/app/page.tsx": "export const page = 1;\n", ...files } }); + const created = runNode( + SESSIONS, + ["new", "--repo", checkout, "--slug", slug, "--summary", slug, "--mode", "seeded", "--operator-id", "user-operator", "--operator-email", "operator@example.com"], + fixture.home + ); + assert.equal(created.status, 0, created.stderr); + return { ...fixture, checkout, worktree: created.json.session.worktree }; +} + +function write(root, file, content) { + const absolute = path.join(root, file); + mkdirSync(path.dirname(absolute), { recursive: true }); + writeFileSync(absolute, content); +} + +function inventory(worktree, home) { + return runNode(INVENTORY, ["--worktree", worktree, "--phase", "handoff"], home); +} + +test("the base's story-group-order.ts decides the scheme, read from its own lifecycle and baseline constants", () => { + assert.deepEqual(storybookSchemeOf(undefined).summary, { name: StorybookScheme.Version }); + assert.deepEqual(storybookSchemeOf(VERSION_SCHEME_ORDER).summary, { name: StorybookScheme.Version }); + const lifecycle = storybookSchemeOf(LIFECYCLE_SCHEME_ORDER); + assert.deepEqual(lifecycle.summary, LIFECYCLE_SUMMARY); + assert.equal(lifecycle.frozenTitle(story("Composites v1.0/Data Display/Tree Row", ["deprecated"])), true); + assert.equal(lifecycle.frozenTitle(story("Composites/Artifacts/Grid Groups", ["stable"])), false); + assert.equal(lifecycle.frozenTitle(story("Composites v2.0/Shared/Artifact List/Documents View", ["v2.0"])), false); + assert.equal(lifecycle.frozenTag(story("Composites/Artifacts/Grid Groups", ["stable"])), false); + assert.equal(lifecycle.frozenTag(story("Composites v1.0/Data Display/Tree Row", ["deprecated", "autodocs"])), true); + assert.equal(lifecycle.frozenTag(story("Composites/Artifacts/Grid Groups", ["!deprecated", "stable"])), false); + const version = storybookSchemeOf(VERSION_SCHEME_ORDER); + assert.equal(version.frozenTitle(story("Composites/Artifacts/Grid Groups", ["stable"])), true); + assert.equal(version.frozenTitle(story("Composites v2.0/Artifacts/Grid Groups", ["v2.0"])), false); + + // The next rename on symphony-alpha's side moves the checks with it. + const renamed = storybookSchemeOf(LIFECYCLE_SCHEME_ORDER.replace(`Deprecated: "deprecated"`, `Deprecated: "retired"`).replace(`= "v1.0"`, `= "v2.0"`)); + assert.deepEqual(renamed.summary, { ...LIFECYCLE_SUMMARY, frozenTag: "retired", frozenRoots: " v2.0", lifecycleTags: ["experimental", "preview", "stable", "retired"] }); + assert.equal(renamed.frozenTitle(story("Composites v2.0/Artifacts/Grid Groups", ["stable"])), true); + assert.equal(renamed.frozenTag(story("Composites/Artifacts/Grid Groups", ["retired"])), true); + + const unreadable = storybookSchemeOf("export const StorybookLifecycle = Object.freeze(LIFECYCLE);\n"); + assert.equal(unreadable.summary.name, StorybookScheme.Unrecognized); + assert.match(unreadable.summary.problem, /update the vibe plugin/); +}); + +test("on a lifecycle base the current roots are not frozen: new entries go there and existing ones take extra stories", (t) => { + // The 1.0.47 failure: every plain-root story file, new or edited, read as v1.0. + const gridGroups = "apps/storybook/stories/composites-v2/v2-grid-groups.stories.tsx"; + const button = "apps/storybook/stories/primitives-v2/button.stories.tsx"; + const { home, worktree } = setup(t, "lifecycle-current-root", { + [STORY_GROUP_ORDER_PATH]: LIFECYCLE_SCHEME_ORDER, + [gridGroups]: story("Composites/Artifacts/Grid Groups", ["stable"]), + [button]: story("Primitives/Actions/Button", ["stable", "autodocs"]), + }); + write(worktree, gridGroups, story("Composites/Artifacts/Grid Groups", ["stable"], ["Default", "Grouped"])); + write(worktree, button, story("Primitives/Actions/Button", ["stable", "autodocs"], ["Default", "Pending"])); + write(worktree, "packages/app/documents/components/table/documents-view.stories.tsx", story("Composites/Shared/Artifact List/Documents View", ["stable", "autodocs"])); + write(worktree, "apps/storybook/stories/composites-v2/v2-issues-list.stories.tsx", story("Composites/Labs/Issues/Issues List/Toolbar", ["preview"])); + write(worktree, "apps/storybook/stories/composites-v2/v2-judges.stories.tsx", story("Composites/Not In The Live App/Judges/Judge Detail/Header", ["experimental"])); + const result = inventory(worktree, home); + assert.equal(result.status, 0, JSON.stringify(result.json)); + assert.deepEqual(result.json.storybookScheme, LIFECYCLE_SUMMARY); + assert.equal(result.json.blocking.storiesInV2Taxonomy, true); + assert.deepEqual(result.json.v1Stories, []); + assert.deepEqual(result.json.compileOnlyV1Repairs, []); + assert.deepEqual(result.json.retitledStories, []); +}); + +test("on a lifecycle base the v1.0 roots and the deprecated tag are frozen, with the compile-only repair kept", (t) => { + const treeRow = "apps/storybook/stories/tree-row.stories.tsx"; + const repaired = "apps/storybook/stories/tree-cell.stories.tsx"; + const moved = "apps/storybook/stories/tree-node.stories.tsx"; + const movedTo = "apps/storybook/stories/tree/tree-node.stories.tsx"; + const history = "apps/storybook/stories/start-here-version-history.stories.tsx"; + const deprecatedRow = (name, stories) => story(`Composites v1.0/Data Display/${name}`, ["deprecated", "autodocs"], stories); + const { home, worktree } = setup(t, "lifecycle-frozen", { + [STORY_GROUP_ORDER_PATH]: LIFECYCLE_SCHEME_ORDER, + [treeRow]: deprecatedRow("Tree Row"), + [repaired]: deprecatedRow("Tree Cell"), + [moved]: deprecatedRow("Tree Node"), + [history]: story("Start Here/Version History", ["stable"]), + }); + write(worktree, treeRow, deprecatedRow("Tree Row", ["Default", "Selected"])); + write(worktree, repaired, deprecatedRow("Tree Cell").replace(`import type { Meta } from "@storybook/react";`, `import type { Meta } from "@storybook/react-vite";`)); + mkdirSync(path.join(worktree, "apps/storybook/stories/tree"), { recursive: true }); + git(worktree, ["mv", moved, movedTo], home); + write(worktree, history, story("Start Here/Version History", ["stable"], ["Default", "Release301"])); + write(worktree, "apps/storybook/stories/composites-v1/tree-row-grouped.stories.tsx", story("Composites v1.0/Data Display/Tree Row Grouped", ["stable"])); + write(worktree, "apps/storybook/stories/composites-v2/tree-row-legacy.stories.tsx", story("Composites/Shared/Navigation/Tree Row Legacy", ["deprecated"])); + write(worktree, "apps/storybook/stories/composites-v2/v2-tree-row-grouped.stories.tsx", story("Composites/Shared/Navigation/Tree Row Grouped", ["stable"], ["Grouped", "GroupedCollapsed"])); + write(worktree, "apps/storybook/stories/composites-v2/v2-tree-row-preview.stories.tsx", `${story("Composites/Shared/Navigation/Tree Row Preview", ["stable"])}export const Design = { tags: ["!stable", "experimental"] };\n`); + + const result = inventory(worktree, home); + assert.equal(result.status, 1); + assert.equal(result.json.storybookScheme.name, StorybookScheme.Lifecycle); + assert.equal(result.json.blocking.storiesInV2Taxonomy, false); + assert.deepEqual(result.json.v1Stories, [ + { path: "apps/storybook/stories/composites-v1/tree-row-grouped.stories.tsx", status: "A", reasons: ["title"] }, + { path: "apps/storybook/stories/composites-v2/tree-row-legacy.stories.tsx", status: "A", reasons: ["tag"] }, + { path: treeRow, status: "M", reasons: ["frozen"] }, + { path: movedTo, status: "A", from: moved, reasons: ["frozen"] }, + ]); + assert.deepEqual(result.json.compileOnlyV1Repairs, [{ path: repaired, status: "M" }]); + assert.deepEqual(result.json.retitledStories, []); + + git(worktree, ["mv", movedTo, moved], home); + git(worktree, ["checkout", "-q", "--", treeRow], home); + git(worktree, ["clean", "-qfd", "apps/storybook/stories/composites-v1"], home); + write(worktree, "apps/storybook/stories/composites-v2/tree-row-legacy.stories.tsx", story("Composites/Shared/Navigation/Tree Row Legacy", ["preview"])); + const clean = inventory(worktree, home); + assert.equal(clean.status, 0, JSON.stringify(clean.json)); + assert.deepEqual(clean.json.v1Stories, []); + assert.deepEqual(clean.json.compileOnlyV1Repairs, [{ path: repaired, status: "M" }]); +}); + +test("a session based before #8867 keeps the version scheme, and after syncing main is not blamed for its renames", (t) => { + const gridGroups = "apps/storybook/stories/composites-v2/v2-grid-groups.stories.tsx"; + const treeRow = "apps/storybook/stories/tree-row.stories.tsx"; + const documentsView = "packages/app/documents/components/table/documents-view.stories.tsx"; + const { home, checkout, worktree } = setup(t, "grouped-list-sections", { + [STORY_GROUP_ORDER_PATH]: VERSION_SCHEME_ORDER, + [gridGroups]: story("Composites v2.0/Artifacts/Grid Groups", ["v2.0"]), + [treeRow]: story("Composites/Data Display/Tree Row", ["v1.0", "autodocs"]), + }); + write(worktree, gridGroups, story("Composites v2.0/Artifacts/Grid Groups", ["v2.0"], ["Default", "Grouped"])); + write(worktree, documentsView, story("Composites v2.0/Shared/Artifact List/Documents View", ["autodocs", "v2.0"])); + git(worktree, ["add", "-A"], home); + git(worktree, ["commit", "-q", "-m", "redeploy 1"], home); + + const before = inventory(worktree, home); + assert.equal(before.status, 0, JSON.stringify(before.json)); + assert.deepEqual(before.json.storybookScheme, { name: StorybookScheme.Version }); + assert.deepEqual(before.json.v1Stories, []); + write(worktree, treeRow, story("Composites/Data Display/Tree Row", ["v1.0", "autodocs"], ["Default", "Selected"])); + const frozenBefore = inventory(worktree, home); + assert.deepEqual(frozenBefore.json.v1Stories, [{ path: treeRow, status: "M", reasons: ["frozen"] }]); + git(worktree, ["checkout", "-q", "--", treeRow], home); + + // Main lands the lifecycle scheme: the v2.0 roots become the plain roots and + // the old plain roots become ` v1.0`, each file tagged by lifecycle. + write(checkout, STORY_GROUP_ORDER_PATH, LIFECYCLE_SCHEME_ORDER); + write(checkout, gridGroups, story("Composites/Artifacts/Grid Groups", ["stable"])); + write(checkout, treeRow, story("Composites v1.0/Data Display/Tree Row", ["deprecated", "autodocs"])); + git(checkout, ["commit", "-q", "-am", "Storybook lifecycle tags; drop v2.0 from root titles"], home); + git(checkout, ["push", "-q", "origin", "main"], home); + git(worktree, ["fetch", "-q", "origin", "main"], home); + git(worktree, ["merge", "-q", "--no-edit", "origin/main"], home); + + const synced = inventory(worktree, home); + assert.equal(synced.json.storybookScheme.name, StorybookScheme.Lifecycle); + assert.equal(synced.json.baseCommit, git(worktree, ["rev-parse", "origin/main"], home)); + assert.deepEqual(synced.json.retitledStories, []); + assert.deepEqual(synced.json.v1Stories, []); + assert.equal(synced.status, 0, JSON.stringify(synced.json)); + assert.match(git(worktree, ["show", `HEAD:${gridGroups}`], home), /title: "Composites\/Artifacts\/Grid Groups",\n {2}tags: \["stable"\],[\s\S]*export const Grouped/); + + // The session's own new entry moves to the current root with a lifecycle + // tag; it is not at the base, so that is not a retitle. + write(worktree, documentsView, story("Composites/Shared/Artifact List/Documents View", ["stable", "autodocs"])); + const placed = inventory(worktree, home); + assert.equal(placed.status, 0, JSON.stringify(placed.json)); + assert.deepEqual(placed.json.retitledStories, []); + assert.deepEqual(placed.json.v1Stories, []); + + write(worktree, treeRow, story("Composites v1.0/Data Display/Tree Row", ["deprecated", "autodocs"], ["Default", "Selected"])); + const frozenAfter = inventory(worktree, home); + assert.equal(frozenAfter.status, 1); + assert.deepEqual(frozenAfter.json.v1Stories, [{ path: treeRow, status: "M", reasons: ["frozen"] }]); +}); + +test("a lifecycle export this script cannot read blocks story changes instead of guessing their rules", (t) => { + const gridGroups = "apps/storybook/stories/composites-v2/v2-grid-groups.stories.tsx"; + const { home, worktree } = setup(t, "lifecycle-unreadable", { + [STORY_GROUP_ORDER_PATH]: "export const StorybookLifecycle = Object.freeze(LIFECYCLE);\n", + [gridGroups]: story("Composites/Artifacts/Grid Groups", ["stable"]), + }); + write(worktree, "apps/app/page.tsx", "export const page = 2;\n"); + const noStories = inventory(worktree, home); + assert.equal(noStories.status, 0, JSON.stringify(noStories.json)); + assert.equal(noStories.json.storybookScheme.name, StorybookScheme.Unrecognized); + assert.equal("storybookSchemeRecognized" in noStories.json.blocking, false); + + write(worktree, gridGroups, story("Composites/Artifacts/Grid Groups", ["stable"], ["Default", "Grouped"])); + const withStories = inventory(worktree, home); + assert.equal(withStories.status, 1); + assert.equal(withStories.json.blocking.storybookSchemeRecognized, false); +}); diff --git a/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs b/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs index efc5ae48..a5caf0b0 100755 --- a/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs +++ b/plugins/vibe/skills/handoff/scripts/handoff-inventory.mjs @@ -30,20 +30,30 @@ // every recorded deletion, file or case, is listed under `retiredTests` with // its status so the reviewer can verify it and the ticket can list it. // -// New story files belong in the v2.0 Storybook taxonomy (symphony-alpha's -// apps/storybook/TAXONOMY.md); one titled under or tagged for a frozen v1.0 -// root, or an existing one newly given either, is listed under `v1Stories` and -// blocks. A story file that was v1.0 at the base is frozen: any edit or move -// of it is listed under `v1Stories` with the reason `frozen` and blocks, so new -// states of its component go in a new v2.0 entry instead. The one exception is -// a compile-only repair (design ruling, 2026-10-15): an in-place edit that +// Story files are judged by the Storybook scheme the session's base uses +// (storybook-taxonomy.mjs), reported as `storybookScheme`. From symphony-alpha +// #8867 on (the lifecycle scheme) the v1.0 baseline is every story file titled +// under a ` v1.0/` root or tagged `deprecated`, and new work goes under +// the plain current roots with one lifecycle tag; on an older base (the +// version scheme) it is the plain roots and the `v1.0` tag, and new work goes +// under ` v2.0/`. A story file newly titled under or tagged for the +// v1.0 baseline is listed under `v1Stories` and blocks. A story file that was +// v1.0 at the base is frozen: any edit or move of it is listed under +// `v1Stories` with the reason `frozen` and blocks, so new states of its +// component go in a new entry under the current roots instead. The one +// exception is a compile-only repair (design ruling, 2026-10-15): an in-place edit that // keeps the meta title, the meta tags and the set of exported story names, and // so every story ID, is listed under `compileOnlyV1Repairs` and does not block; // the ticket asks engineering to confirm it changed no visuals. Any other existing // story file keeps its meta title, because Storybook builds story IDs from it: // a title changed from the base, moves included, is listed under // `retitledStories` and blocks unless the session record lists the file as a -// replacement's entry (`vibe-sessions.mjs story-replacement`). +// replacement's entry (`vibe-sessions.mjs story-replacement`). The base is the +// main the session last synced, so the retitles a main import brought (#8867 +// renamed every root) are in the base and never counted as the session's. A +// base whose story-group-order.ts exports `StorybookLifecycle` in a shape +// storybook-taxonomy.mjs cannot read blocks a session that changed a story +// file (`storybookSchemeRecognized`) instead of guessing its rules. // // A story file whose meta title is under the `Start Here/` root, at the base // or now, is exempt from the freeze, the v1.0 title and tag checks and the @@ -69,6 +79,15 @@ import { isRootTest, isSnapshot, isTestFile, TEST_SPEC_PATTERN } from "../../vib import { readMainSyncProvenance } from "../../vibe/scripts/dist/main-sync.mjs"; import { isShrinkOnlyAllowlistEdit, SHRINK_ONLY_ALLOWLISTS } from "./allowlist-shrink.mjs"; import { isPortableSurfaceAllowlistEdit, PORTABLE_SURFACE_CHECKER } from "./prototype-allowlist.mjs"; +import { + isUnversionedStory, + STORY_GROUP_ORDER_PATH, + StorybookScheme, + storybookSchemeOf, + storyExportNames, + storyMetaTags, + storyMetaTitle, +} from "./storybook-taxonomy.mjs"; const GIT_MAX_BUFFER = 64 * 1024 * 1024; const BRANCH_PREFIX = "vibe/"; @@ -104,22 +123,6 @@ const BACKEND_SEGMENTS = ["/prisma/", "/migrations/"]; const STORY_PATTERN = /\.stories\.tsx?$/; const COMPONENT_PATTERN = /\/components\/.+\.tsx$/; -// v1.0 is the frozen baseline; new work goes under ` v2.0/` -// (apps/storybook/TAXONOMY.md; operator ruling, 2026-10-14). -// A v1.0 story file is never edited except for a compile-only repair -// (design ruling, 2026-10-15). -const V1_STORY_TITLE = /\btitle:\s*["'`](?:Foundations|Primitives|Composites|Surfaces)\//; -const V1_STORY_TAG = /\btags:\s*\[[^\]]*["'`]v1\.0["'`]/; -// The first title segment, as the sidebar root manager.ts leaves unversioned. -const UNVERSIONED_STORY_ROOT = "Start Here"; -// The meta object's title, not a `title:` arg or prop elsewhere in the file. -const STORY_META_START = /\b(?:const\s+meta\b[^=]*=|export\s+default)\s*\{/; -const STORY_META_TITLE = /\btitle:\s*(["'`])((?:(?!\1).)+)\1/; -const STORY_META_TAGS = /\btags:\s*\[([^\]]*)\]/; -const STRING_LITERAL = /(["'`])((?:(?!\1).)*)\1/g; -const NAMED_EXPORT = /\bexport\s+(?:const|let|var|function\*?|class)\s+([\w$]+)/g; -const EXPORT_LIST = /\bexport\s*\{([^}]*)\}/g; -const EXPORT_ALIAS = /\s+as\s+/; const STORY_PATHSPECS = ["*.stories.ts", "*.stories.tsx"]; // Tests are written only when the person explicitly asks for a pull request // (operator ruling, 2026-10-13); earlier phases may only repair existing @@ -183,6 +186,9 @@ const outsideAllowed = changedFiles.filter( const backendFiles = changedFiles.filter((file) => isBackend(file.path)); const componentsWithoutStories = findComponentsWithoutStories(changedFiles); const movedStories = findMovedStories(); +const storybookScheme = storybookSchemeOf(readBaseFile(STORY_GROUP_ORDER_PATH)); +const storybookSchemeUnreadable = storybookScheme.summary.name === StorybookScheme.Unrecognized && + changedFiles.some((file) => STORY_PATTERN.test(file.path)); const { v1Stories, compileOnlyV1Repairs } = findV1Stories(changedFiles); const retitledStories = findRetitledStories(changedFiles); @@ -191,6 +197,7 @@ const blocking = { hasChanges: changedFiles.length > 0, storiesInV2Taxonomy: v1Stories.length === 0, existingStoryTitlesKept: retitledStories.every((story) => story.replacement !== null), + ...(storybookSchemeUnreadable ? { storybookSchemeRecognized: false } : {}), ...(trackedLocalPlans.length ? { localPlansUntracked: false } : {}), ...(prototype ? { prototypePublicationCurrent: publicationProblem === undefined } : {}), ...(mainSync?.pendingMerge ? { mainSyncMergeCommitted: false } : {}), @@ -221,6 +228,7 @@ const result = { backendChanged: backendFiles.length > 0, backendFiles, componentsWithoutStories, + storybookScheme: storybookScheme.summary, v1Stories, compileOnlyV1Repairs, retitledStories, @@ -378,9 +386,9 @@ function isBackend(filePath) { /** * `v1Stories`: story files whose meta title sits in a v1.0 root or whose tags - * include v1.0 where the base had neither, and every modified or moved story - * file that was v1.0 at the base (`frozen`), except under the unversioned - * Start Here root. `compileOnlyV1Repairs`: the in-place edits of a v1.0 file + * mark the v1.0 baseline, by the base's scheme, where the base had neither, + * and every modified or moved story file that was v1.0 at the base + * (`frozen`), except under the unversioned Start Here root. `compileOnlyV1Repairs`: the in-place edits of a v1.0 file * that keep its story IDs and tags. */ function findV1Stories(files) { @@ -401,10 +409,10 @@ function findV1Stories(files) { if (isUnversionedStory(base) || isUnversionedStory(source)) { continue; } - const gained = (pattern) => pattern.test(source) && !(base !== undefined && pattern.test(base)); + const gained = (isV1) => isV1(source) && !(base !== undefined && isV1(base)); const reasons = [ - ...(gained(V1_STORY_TITLE) ? ["title"] : []), - ...(gained(V1_STORY_TAG) ? ["tag"] : []), + ...(gained(storybookScheme.frozenTitle) ? ["title"] : []), + ...(gained(storybookScheme.frozenTag) ? ["tag"] : []), ...(base !== undefined && isV1Story(base) ? ["frozen"] : []), ]; if (reasons.length === 1 && reasons[0] === "frozen" && file.status === "M" && keepsStoryIdentity(base, source)) { @@ -426,12 +434,7 @@ function keepsStoryIdentity(base, source) { } function isV1Story(source) { - return V1_STORY_TITLE.test(source) || V1_STORY_TAG.test(source); -} - -/** A story file whose meta title's root is Start Here, which carries no version. */ -function isUnversionedStory(source) { - return source !== undefined && storyMetaTitle(source)?.split("/")[0].trim() === UNVERSIONED_STORY_ROOT; + return storybookScheme.frozenTitle(source) || storybookScheme.frozenTag(source); } /** @@ -478,30 +481,6 @@ function findMovedStories() { }); } -function storyMetaTitle(source) { - const start = source.search(STORY_META_START); - return STORY_META_TITLE.exec(start === -1 ? source : source.slice(start))?.[2] ?? null; -} - -function storyMetaTags(source) { - const start = source.search(STORY_META_START); - const tags = STORY_META_TAGS.exec(start === -1 ? source : source.slice(start))?.[1]; - return tags === undefined ? null : [...tags.matchAll(STRING_LITERAL)].map((match) => match[2]); -} - -function storyExportNames(source) { - const names = new Set([...source.matchAll(NAMED_EXPORT)].map((match) => match[1])); - for (const [, list] of source.matchAll(EXPORT_LIST)) { - for (const entry of list.split(",")) { - const name = entry.trim().split(EXPORT_ALIAS).at(-1); - if (name && name !== "default") { - names.add(name); - } - } - } - return [...names].sort(); -} - function readWorktreeFile(filePath) { const absolute = path.join(worktree, filePath); return existsSync(absolute) ? readFileSync(absolute, "utf8") : undefined; diff --git a/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs b/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs index dc2d5058..02b9d9f1 100644 --- a/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs +++ b/plugins/vibe/skills/handoff/scripts/handoff-inventory.test.mjs @@ -381,6 +381,9 @@ test("a migration outside the backend packages and a local-fix backend file coun assert.deepEqual(result.json.outsideAllowed, [{ path: "apps/mcp/prisma/migrations/2_x/migration.sql", status: "A" }]); }); +// The story cases below run on a base with no story-group-order.ts, so the +// version scheme (pre-#8867) applies; handoff-inventory-storybook-scheme.test.mjs +// covers the lifecycle scheme and the move between them. test("a v1.0 story file is frozen: an edit or move blocks, and new states go in a new v2.0 entry", (t) => { const v1Meta = (title, tags) => `export default { title: "${title}", tags: [${tags.map((tag) => `"${tag}"`).join(", ")}] };\n`; const subtree = "apps/storybook/stories/composites/sortable-subtree.stories.tsx"; diff --git a/plugins/vibe/skills/handoff/scripts/storybook-taxonomy.mjs b/plugins/vibe/skills/handoff/scripts/storybook-taxonomy.mjs new file mode 100644 index 00000000..5163ecaa --- /dev/null +++ b/plugins/vibe/skills/handoff/scripts/storybook-taxonomy.mjs @@ -0,0 +1,124 @@ +// Storybook taxonomy reading for the handoff inventory: a story file's meta +// title, tags and exported story names, and which story files a base commit +// treats as the frozen v1.0 baseline. +// +// symphony-alpha has used two schemes, and a session is judged by the one its +// base uses, so a branch still on an older main keeps its rules until it +// syncs (design ruling, 2026-10-16): +// - Version scheme, before symphony-alpha #8867 (ISS-12610): the plain level +// roots (`Composites/`) and the `v1.0` tag are the v1.0 baseline; new work +// goes under ` v2.0/` tagged `v2.0`. +// - Lifecycle scheme, from #8867 (Storybook 3.0.0): the ` v1.0/` roots +// and the `deprecated` lifecycle tag are the v1.0 baseline; new work goes +// under the plain roots with one lifecycle tag (`stable`, `preview`, +// `experimental`). +// The base's own apps/storybook/story-group-order.ts says which: it exports +// `StorybookLifecycle` only in the lifecycle scheme, and there the frozen tag +// is that object's `Deprecated` value and every frozen root ends in +// `storybookBaselineVersion`, the same test symphony-alpha's +// validate-story-titles.mjs applies. Reading them from the base keeps the next +// rename there from breaking these checks. The version scheme is closed +// history whose file declares neither, so its two patterns stay written here. + +export const STORY_GROUP_ORDER_PATH = "apps/storybook/story-group-order.ts"; + +export const StorybookScheme = { + Version: "version", + Lifecycle: "lifecycle", + Unrecognized: "unrecognized", +}; + +// The first title segment that carries no version or lifecycle stage, as +// symphony-alpha's .storybook/manager.ts treats it (design ruling, 2026-10-06). +export const UNVERSIONED_STORY_ROOT = "Start Here"; + +const VERSION_SCHEME_FROZEN_TITLE = /\btitle:\s*["'`](?:Foundations|Primitives|Composites|Surfaces)\//; +const VERSION_SCHEME_FROZEN_TAG = /\btags:\s*\[[^\]]*["'`]v1\.0["'`]/; +const LIFECYCLE_EXPORT = /\bexport\s+const\s+StorybookLifecycle\s*=/; +const LIFECYCLE_MEMBERS = /\bexport\s+const\s+StorybookLifecycle\s*=\s*\{([^}]*)\}/; +const LIFECYCLE_MEMBER = /\b(\w+)\s*:\s*(["'`])([^"'`]+)\2/g; +const DEPRECATED_MEMBER = "Deprecated"; +const BASELINE_VERSION = /\bexport\s+const\s+storybookBaselineVersion\s*=\s*(["'`])([^"'`]+)\1/; +const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g; +// The meta object's title, not a `title:` arg or prop elsewhere in the file. +const STORY_META_START = /\b(?:const\s+meta\b[^=]*=|export\s+default)\s*\{/; +const STORY_META_TITLE = /\btitle:\s*(["'`])((?:(?!\1).)+)\1/; +const STORY_META_TAGS = /\btags:\s*\[([^\]]*)\]/; +const STRING_LITERAL = /(["'`])((?:(?!\1).)*)\1/g; +const NAMED_EXPORT = /\bexport\s+(?:const|let|var|function\*?|class)\s+([\w$]+)/g; +const EXPORT_LIST = /\bexport\s*\{([^}]*)\}/g; +const EXPORT_ALIAS = /\s+as\s+/; + +/** + * The scheme a base's story-group-order.ts declares (undefined when the base + * has none: the version scheme). `frozenTitle` and `frozenTag` say whether a + * story file's source places it in the v1.0 baseline by title or by tag; + * `summary` is what the inventory reports. + */ +export function storybookSchemeOf(orderSource) { + if (orderSource === undefined || !LIFECYCLE_EXPORT.test(orderSource)) { + return { + summary: { name: StorybookScheme.Version }, + frozenTitle: (source) => VERSION_SCHEME_FROZEN_TITLE.test(source), + frozenTag: (source) => VERSION_SCHEME_FROZEN_TAG.test(source), + }; + } + const members = [...(LIFECYCLE_MEMBERS.exec(orderSource)?.[1] ?? "").matchAll(LIFECYCLE_MEMBER)]; + const deprecatedTag = members.find((member) => member[1] === DEPRECATED_MEMBER)?.[3]; + const baselineVersion = BASELINE_VERSION.exec(orderSource)?.[2]; + if (deprecatedTag === undefined || baselineVersion === undefined) { + return { + summary: { + name: StorybookScheme.Unrecognized, + problem: `${STORY_GROUP_ORDER_PATH} exports StorybookLifecycle but no readable ${DEPRECATED_MEMBER} tag or storybookBaselineVersion; update the vibe plugin before handing off story changes.`, + }, + frozenTitle: () => false, + frozenTag: () => false, + }; + } + const rootSuffix = ` ${baselineVersion}`; + const tag = new RegExp(String.raw`\btags:\s*\[[^\]]*["'\`]${deprecatedTag.replace(REGEXP_SPECIAL, String.raw`\$&`)}["'\`]`); + return { + summary: { + name: StorybookScheme.Lifecycle, + frozenTag: deprecatedTag, + frozenRoots: `${rootSuffix}`, + lifecycleTags: members.map((member) => member[3]), + }, + frozenTitle: (source) => storyTitleRoot(source)?.endsWith(rootSuffix) === true, + frozenTag: (source) => tag.test(source), + }; +} + +/** A story file whose meta title's root is Start Here, which carries no version. */ +export function isUnversionedStory(source) { + return source !== undefined && storyTitleRoot(source) === UNVERSIONED_STORY_ROOT; +} + +export function storyMetaTitle(source) { + const start = source.search(STORY_META_START); + return STORY_META_TITLE.exec(start === -1 ? source : source.slice(start))?.[2] ?? null; +} + +export function storyMetaTags(source) { + const start = source.search(STORY_META_START); + const tags = STORY_META_TAGS.exec(start === -1 ? source : source.slice(start))?.[1]; + return tags === undefined ? null : [...tags.matchAll(STRING_LITERAL)].map((match) => match[2]); +} + +export function storyExportNames(source) { + const names = new Set([...source.matchAll(NAMED_EXPORT)].map((match) => match[1])); + for (const [, list] of source.matchAll(EXPORT_LIST)) { + for (const entry of list.split(",")) { + const name = entry.trim().split(EXPORT_ALIAS).at(-1); + if (name && name !== "default") { + names.add(name); + } + } + } + return [...names].sort(); +} + +function storyTitleRoot(source) { + return storyMetaTitle(source)?.split("/")[0].trim(); +} diff --git a/plugins/vibe/skills/vibe/references/design-pass.md b/plugins/vibe/skills/vibe/references/design-pass.md index da88f57b..404cfc9b 100644 --- a/plugins/vibe/skills/vibe/references/design-pass.md +++ b/plugins/vibe/skills/vibe/references/design-pass.md @@ -76,8 +76,9 @@ not a design session: no second design, no questions, no approval. 6. Placement: before a story is created, retitled or moved or a shared component is added, the writer returns `NEEDS_STORYBOOK` and `vibe-storybook-decomposer` answers with its Placement block (`SKILL.md`, - section 5). New states of an existing component go in a new v2.0 entry - holding only them when its story file is v1.0, which is never edited + section 5). New states of an existing component go in a new entry in the + current root holding only them when its story file is v1.0 (under a + ` v1.0/` root or tagged `deprecated`), which is never edited beyond a compile-only repair, and otherwise in its existing story file, title and tag unchanged; only a new component or a sibling replacement otherwise gets a new entry (design diff --git a/plugins/vibe/skills/vibe/references/guardrails.md b/plugins/vibe/skills/vibe/references/guardrails.md index e98e7981..197fd380 100644 --- a/plugins/vibe/skills/vibe/references/guardrails.md +++ b/plugins/vibe/skills/vibe/references/guardrails.md @@ -202,8 +202,8 @@ When nothing in the catalog fits: (`apps/storybook/AGENTS.md`, `packages/design-system/AGENTS.md`), using the repo skill `.claude/skills/storybook` (`workflows/author-stories.md`, `workflows/design-controls.md`): - - Stories follow "Storybook taxonomy" below: a v2.0 root and the page - tree, never a v1.0 root. + - Stories follow "Storybook taxonomy" below: a current root, the page + tree and a lifecycle tag, never a v1.0 root. - Stories go where Storybook collects them. For `packages/design-system` that is `apps/storybook/stories/` (a story next to the component is never picked up); `packages/app/*/components/` stories may sit beside the @@ -229,33 +229,52 @@ the session adds, moves, renames or touches; that doc is the authority, so its rules are not copied here (operator ruling, 2026-10-14, after the PRD-720 session titled its new stories under v1.0 roots). The bare minimum: -- New stories go in a v2.0 root, titled under ` v2.0/`, never in the - frozen v1.0 roots (`Foundations/`, `Primitives/`, `Composites/`, - `Surfaces/`). +- New stories go in the current roots, titled under the plain level + (`Foundations/`, `Primitives/`, `Composites/`, `Patterns/`, `Surfaces/`), + never in the frozen v1.0 roots (`Foundations v1.0/`, `Primitives v1.0/`, + `Composites v1.0/`, `Surfaces v1.0/`), whose story files are all tagged + `deprecated`. - New states or variants of an existing component never retitle, re-tag or move its existing story file, since story IDs come from the title. A v1.0 - story file is never 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 v2.0 entry holding only them. Any other existing file takes them as extra - stories. 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). + story file (under a ` v1.0/` root or tagged `deprecated`) is never + 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 + 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). - 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. -- Composites, Patterns and Surfaces v2.0 are placed in the page tree the doc +- Composites, Patterns and Surfaces are placed in the page tree the doc describes. -- Each story file carries the `v2.0` version tag. +- Each story file carries exactly one lifecycle tag in its own meta, per the + doc's "Lifecycle": `stable` for a shipped surface, `preview` for anything + under `Labs`, `experimental` for a design-only preview or anything under + `Not In The Live App`; never `deprecated`, which only the v1.0 roots carry. + 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. - -The handoff inventory blocks a new story file titled under or tagged for a -v1.0 root and any other edit or move of a v1.0 story file (`v1Stories`, -`storiesInV2Taxonomy`), and an existing non-v1.0 one whose title changed (`retitledStories`, `existingStoryTitlesKept`) unless the writer +- 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 ` v2.0/` + tagged `v2.0`. Syncing main moves it to this scheme: its own new entries + then take the plain root and a lifecycle tag (design ruling, 2026-10-16). + +The handoff inventory judges story files by the scheme of the session's base +(`storybookScheme`, read from that base's own Storybook sources). It blocks a +new story file titled under or tagged for a v1.0 root and any other edit or +move of a v1.0 story file (`v1Stories`, `storiesInV2Taxonomy`), and an +existing non-v1.0 one whose title changed (`retitledStories`, +`existingStoryTitlesKept`) unless the writer recorded it as a replacement's entry with `vibe-sessions.mjs story-replacement --worktree --path --replacement `, and -`vibe-guardrails-reviewer` checks the placement. A compile-only repair of a +`vibe-guardrails-reviewer` checks the placement. The base is the main the +session last synced, so the retitles a main import brought are never the +session's own. A compile-only repair of a v1.0 file does not block; it is listed under `compileOnlyV1Repairs` for the Engineering checklist. Where in that tree a story goes, and which component owns it, is `vibe-storybook-decomposer`'s Placement diff --git a/plugins/vibe/tools/python/test_vibe_start_here_unversioned_contract.py b/plugins/vibe/tools/python/test_vibe_start_here_unversioned_contract.py index 0376b6be..718ada20 100644 --- a/plugins/vibe/tools/python/test_vibe_start_here_unversioned_contract.py +++ b/plugins/vibe/tools/python/test_vibe_start_here_unversioned_contract.py @@ -21,6 +21,7 @@ DECOMPOSER = PLUGIN_ROOT / "agents" / "vibe-storybook-decomposer.md" HANDOFF_SKILL = PLUGIN_ROOT / "skills" / "handoff" / "SKILL.md" INVENTORY = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "handoff-inventory.mjs" +TAXONOMY_SCRIPT = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "storybook-taxonomy.mjs" EXEMPTION = ( "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 " @@ -52,13 +53,15 @@ def paragraph(text: str, marker: str) -> str: def test_guardrails_state_the_exemption_beside_the_freeze() -> None: taxonomy = section(GUARDRAILS.read_text(), "## Storybook taxonomy") assert EXEMPTION in taxonomy - assert taxonomy.index("A v1.0 story file is never edited beyond a compile-only repair") < taxonomy.index(EXEMPTION) + assert taxonomy.index("A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`) is never edited") < taxonomy.index( + EXEMPTION + ) def test_the_placement_owner_states_the_exemption_in_its_v1_rule() -> None: order = paragraph(DECOMPOSER.read_text(), "4. Decide in this order") assert EXEMPTION in order - assert order.index("A v1.0 story file is frozen and never edited") < order.index(EXEMPTION) + assert order.index("is frozen and never edited, not even to add a story") < order.index(EXEMPTION) def test_the_handoff_storybook_paragraph_states_the_exemption() -> None: @@ -84,9 +87,12 @@ def test_each_holder_states_the_exemption_once() -> None: def test_the_inventory_exempts_the_start_here_title_root_from_both_checks() -> None: inventory = INVENTORY.read_text() - assert 'const UNVERSIONED_STORY_ROOT = "Start Here";' in inventory - assert "storyMetaTitle(source)?.split(\"/\")[0].trim() === UNVERSIONED_STORY_ROOT" in inventory - assert "start-here-version-history" not in inventory + taxonomy = TAXONOMY_SCRIPT.read_text() + assert 'export const UNVERSIONED_STORY_ROOT = "Start Here";' in taxonomy + assert "storyTitleRoot(source) === UNVERSIONED_STORY_ROOT" in taxonomy + assert 'return storyMetaTitle(source)?.split("/")[0].trim();' in taxonomy + for script in (inventory, taxonomy): + assert "start-here-version-history" not in script for check in ("function findV1Stories(files)", "function findRetitledStories(files)"): start = inventory.index(check) body = inventory[start : inventory.index("\n}\n", start)] diff --git a/plugins/vibe/tools/python/test_vibe_storybook_existing_component_contract.py b/plugins/vibe/tools/python/test_vibe_storybook_existing_component_contract.py index 3354b1f4..0d82eea8 100644 --- a/plugins/vibe/tools/python/test_vibe_storybook_existing_component_contract.py +++ b/plugins/vibe/tools/python/test_vibe_storybook_existing_component_contract.py @@ -4,14 +4,18 @@ (new states or variants, a column action, a footer) comes first: it never retitles, re-tags or moves the existing story file. A v1.0 story file is frozen and never edited beyond a compile-only repair - (test_vibe_v1_compile_only_repair_contract.py), so the new states go in a new v2.0 entry holding - only them, named descriptively per the taxonomy doc without a version - number; any other existing file takes them as extra stories, its title, - tag and story names unchanged. Otherwise only a new component or a sibling - replacement gets a new entry and v2.0 placement. This outranks the - "touched story takes v2.0" reading. + (test_vibe_v1_compile_only_repair_contract.py), so the new states go in a + new entry in the current root holding only them, named descriptively per + the taxonomy doc without a version number; any other existing file takes + them as extra stories, its title, tag and story names unchanged. Otherwise + only a new component or a sibling replacement gets a new entry and a place + in the current roots. This outranks the "touched story moves to the + current roots" reading. Since symphony-alpha #8867 (design ruling, + 2026-10-16) a v1.0 story file is one under a ` v1.0/` root or + tagged `deprecated`. 2. The guardrails and design-pass placement rules carry it as a one-liner, and - no role still sends touched stories to v2.0 or adds stories to a v1.0 file. + no role still sends touched stories to the current roots or adds stories to + a v1.0 file. 3. The guardrails reviewer blocks an edited or moved v1.0 story file, and a retitled, re-tagged or moved non-v1.0 one unless it is a recorded replacement; the handoff inventory finds both mechanically (its node tests @@ -32,11 +36,13 @@ DESIGN_PASS = VIBE / "references" / "design-pass.md" HANDOFF_SKILL = PLUGIN_ROOT / "skills" / "handoff" / "SKILL.md" INVENTORY = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "handoff-inventory.mjs" +TAXONOMY_SCRIPT = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "storybook-taxonomy.mjs" SESSIONS = VIBE / "scripts" / "vibe-sessions.mjs" README = PLUGIN_ROOT / "README.md" RULING = "design ruling, 2026-10-15" CANONICAL = "never retitles, re-tags or moves its existing story file" -FROZEN = "A v1.0 story file is frozen and never edited" +FROZEN = "is frozen and never edited, not even to add a story" +V1_FILE = "A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`)" def flat(text: str) -> str: @@ -62,16 +68,19 @@ def test_existing_components_come_first_in_the_placement_owners_decision_order() "(new states or variants, an extra column action, a new footer)", f"{CANONICAL}.", "Storybook builds story IDs from the title path, so a retitle breaks every link to those stories", - f"{FROZEN}, not even to add a story: the new states go in a new v2.0 entry for that component " + f"{V1_FILE} {FROZEN}: the new states go in a new entry for that component in the current root " "holding only the new states, its name descriptive, permanent, per `apps/storybook/TAXONOMY.md` and " - "without a version number, and the v1.0 stories stay where they are.", + "without a version number, with the lifecycle tag its place in the tree takes, and the v1.0 stories " + "stay where they are.", "Any other existing story file takes the new states as extra stories, its title, tag and existing " "story names unchanged", - "Otherwise only a new component, or a sibling replacement such as a V3, gets a new entry and v2.0 placement", - "This outranks reading a touched story file as taking the v2.0 version.", + "Otherwise only a new component, or a sibling replacement such as a V3, gets a new entry and a place in " + "the current roots", + "This outranks reading a touched story file as moving to the current roots or taking a new lifecycle tag.", ): assert required in order, required assert "even in a v1.0 root" not in order + assert "v2.0" not in order assert order.index(f"First ({RULING})") < order.index("Then (") placement = flat(agent("vibe-storybook-decomposer")) assert "an existing file's title and tag unchanged, a v1.0 file not edited beyond a compile-only repair (step 4)" in placement @@ -89,32 +98,34 @@ def test_guardrails_and_design_pass_carry_the_one_liner() -> None: guardrails = paragraph(GUARDRAILS.read_text(), "- New states or variants of an existing component") for required in ( "never retitle, re-tag or move its existing story file, since story IDs come from the title", - "A v1.0 story file is never 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 v2.0 entry holding only them.", - "Any other existing file takes them as extra stories.", + "A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`) is never 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 as extra stories, its title and tag unchanged.", "Otherwise only a new component or a sibling replacement (a V3) gets a new entry", f"({RULING}; `vibe-storybook-decomposer.md`, \"Precedent search\" step 4)", ): assert required in guardrails, required design = paragraph(DESIGN_PASS.read_text(), "6. Placement:") assert ( - "New states of an existing component go in a new v2.0 entry holding only them when its story file is " - "v1.0, which is never edited beyond a compile-only repair, and otherwise in its existing story file, title " - "and tag unchanged; only a " + "New states of an existing component go in a new entry in the current root holding only them when its " + "story file is v1.0 (under a ` v1.0/` root or tagged `deprecated`), which is never edited beyond a " + "compile-only repair, and otherwise in its existing story file, title and tag unchanged; only a " f"new component or a sibling replacement otherwise gets a new entry ({RULING})." ) in design -def test_no_role_still_sends_touched_stories_to_v2() -> None: +def test_no_role_still_sends_touched_stories_to_the_current_roots() -> None: for path in sorted(PLUGIN_ROOT.rglob("*.md")): text = flat(path.read_text()) for retired in ("new or touched story", "New and touched stories", "every new or touched story"): assert retired not in text, (path.name, retired) writer = flat(agent("vibe-change-worker")) assert ( - "new states of an existing component go in a new v2.0 entry holding only them when its story file is " - "v1.0, which you never edit beyond a compile-only repair, and otherwise in its existing story file, title " - "and tag unchanged" + "new states of an existing component go in a new entry in the current root holding only them when its " + "story file is v1.0 (under a ` v1.0/` root or tagged `deprecated`), which you never edit beyond a " + "compile-only repair, and otherwise in its existing story file, title and tag unchanged" ) in writer handoff = flat(HANDOFF_SKILL.read_text()) assert ( @@ -123,7 +134,7 @@ def test_no_role_still_sends_touched_stories_to_v2() -> None: ) in handoff for path in sorted(PLUGIN_ROOT.rglob("*.md")): text = flat(path.read_text()) - for retired in ("even in a v1.0 root", "an existing v1.0 file keeps"): + for retired in ("even in a v1.0 root", "an existing v1.0 file keeps", "new v2.0 entry", "v2.0 placement"): assert retired not in text, (path.name, retired) @@ -131,8 +142,8 @@ def test_the_reviewer_blocks_an_edited_v1_file_and_a_retitled_one_unless_it_is_a check = paragraph(agent("vibe-guardrails-reviewer"), "10. Storybook taxonomy") for required in ( "or any edit or move of a story file that is v1.0 on main (the inventory's `v1Stories`, reason `frozen`), " - "is blocking; that component's new states belong in a new v2.0 entry holding only them, with no version " - "number in its name", + "is blocking; that component's new states belong in a new entry in the current root holding only them, " + "with no version number in its name", "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", @@ -146,21 +157,22 @@ def test_the_inventory_freezes_v1_files_and_admits_only_recorded_replacements_fo for required in ( '...(base !== undefined && isV1Story(base) ? ["frozen"] : []),', "existingStoryTitlesKept: retitledStories.every((story) => story.replacement !== null),", - "const STORY_META_START = ", '"--diff-filter=R"', "record?.storyReplacements", ): assert required in inventory, required + assert "const STORY_META_START = " in TAXONOMY_SCRIPT.read_text() guardrails = flat(GUARDRAILS.read_text()) assert "and any other edit or move of a v1.0 story file (`v1Stories`, `storiesInV2Taxonomy`)" in guardrails - assert "an existing non-v1.0 one whose title changed (`retitledStories`, `existingStoryTitlesKept`)" in guardrails + assert "existing non-v1.0 one whose title changed (`retitledStories`, `existingStoryTitlesKept`)" in guardrails assert "`vibe-sessions.mjs story-replacement --worktree --path --replacement `" in guardrails sessions = SESSIONS.read_text() assert 'case "story-replacement":' in sessions readme = flat(README.read_text()) for required in ( - "A v1.0 story file is never edited beyond a compile-only repair, so new states of its component go in a " - "new v2.0 entry holding only them.", + "A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`) is never edited beyond a " + "compile-only repair, so new states of its component go in a new entry in the current root holding only " + "them.", "New states of any other existing component go in its existing story file under the same title and tag", "handoff blocks any other edit of a v1.0 story file except a compile-only repair, and a retitle unless it " "is a sibling replacement's entry", diff --git a/plugins/vibe/tools/python/test_vibe_storybook_placement_contract.py b/plugins/vibe/tools/python/test_vibe_storybook_placement_contract.py index 92e2e0ef..4214e43f 100644 --- a/plugins/vibe/tools/python/test_vibe_storybook_placement_contract.py +++ b/plugins/vibe/tools/python/test_vibe_storybook_placement_contract.py @@ -132,7 +132,7 @@ def test_precedent_search_reports_everything_from_the_graph_and_the_built_index( "Leave out the session's own stories.", "Report everything found, not a pick", "how each host consumes it", - "every existing story that exercises it, with its title, story names, version tag and file", + "every existing story that exercises it, with its title, story names, lifecycle tag and file", "Never invent a placement", ): assert required in search, required @@ -161,7 +161,7 @@ def test_stories_document_consumed_components_not_internal_pieces() -> None: "Either way, stories document the components consumers actually render, one story per state, never internal render pieces", "a shared capability's generic mechanics stay in the shared component's existing stories", "the page-level component consumers render gets variants showing only its domain-specific configuration", - "the doc's v2.0 Composites row, \"a section of a page\"", + "the doc's Composites row, \"a section of a page\"", ): assert required in order, required stories = paragraph(text, "2. Stories.") @@ -291,12 +291,12 @@ def test_same_area_means_same_capability_not_same_folder() -> None: MIRROR_STRUCTURE_ONLY = ( - "Mirroring copies structure only, never location or version: the root, version tag and v2.0 " + "Mirroring copies structure only, never location or lifecycle: the root, lifecycle tag and " f"tree position always come from {TAXONOMY_DOC}, and a precedent in a v1.0 root never puts new work in v1.0." ) TIEBREAK = ( "mirror one precedent, chosen in this order: built on the same providing component, then already in the " - "v2.0 tree, then most recently changed (the latest commit on main to its story file)" + "current roots, then most recently changed (the latest commit on main to its story file)" ) CONFLICTS_RULE = '("Conflicts")' @@ -320,7 +320,7 @@ def test_mirroring_copies_structure_never_root_or_version() -> None: def test_the_precedent_tiebreak_order_is_stated_once() -> None: conflict = paragraph(agent("vibe-storybook-decomposer"), "5. A precedent that disagrees") assert TIEBREAK in conflict - assert conflict.index("same providing component") < conflict.index("v2.0 tree") < conflict.index("most recently changed") + assert conflict.index("same providing component") < conflict.index("current roots") < conflict.index("most recently changed") holders = [ path.name for path in sorted(PLUGIN_ROOT.rglob("*.md")) diff --git a/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py b/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py index c072f5b5..c84500cd 100644 --- a/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py +++ b/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py @@ -1,13 +1,18 @@ -"""Contract checks for the 2026-10-14 operator ruling on Storybook taxonomy. +"""Contract checks for the 2026-10-14 operator ruling on Storybook taxonomy, +as translated to symphony-alpha's lifecycle scheme (#8867, ISS-12610; design +ruling, 2026-10-16). 1. The writer follows symphony-alpha's `apps/storybook/TAXONOMY.md` for every - new story: a v2.0 root under ` v2.0/`, the page tree, the - `v2.0` tag and the Storybook release bump, never a frozen v1.0 root. The - plugin points at the doc and restates only that minimum. + new story: the plain current roots, the page tree, one lifecycle tag and + the Storybook release bump, never a frozen ` v1.0/` root or the + `deprecated` tag. A session on a base older than #8867 keeps that base's + ` v2.0/` rule. The plugin points at the doc and restates only that + minimum. 2. The rule lives once, in `guardrails.md`; the writer, the Storybook and primitive advisors, the handoff skill and the guardrails reviewer point at it. 3. The handoff inventory blocks a new story titled under or tagged - for a v1.0 root (its node test proves the check itself). + for a v1.0 root by the scheme its base declares (its node tests prove the + check itself, on both schemes). """ from __future__ import annotations @@ -21,6 +26,7 @@ GUARDRAILS = PLUGIN_ROOT / "skills" / "vibe" / "references" / "guardrails.md" HANDOFF_SKILL = PLUGIN_ROOT / "skills" / "handoff" / "SKILL.md" INVENTORY = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "handoff-inventory.mjs" +TAXONOMY_SCRIPT = PLUGIN_ROOT / "skills" / "handoff" / "scripts" / "storybook-taxonomy.mjs" TAXONOMY_DOC = "`apps/storybook/TAXONOMY.md`" POINTER = '"Storybook taxonomy"' @@ -48,14 +54,26 @@ def test_the_rule_defers_to_the_repo_taxonomy_doc_and_states_only_the_minimum() f"Read the repo's {TAXONOMY_DOC} and follow it", "that doc is the authority, so its rules are not copied here", "operator ruling, 2026-10-14", - "New stories go in a v2.0 root, titled under ` v2.0/`", - "never in the frozen v1.0 roots (`Foundations/`, `Primitives/`, `Composites/`, `Surfaces/`)", + "New stories go in the current roots, titled under the plain level " + "(`Foundations/`, `Primitives/`, `Composites/`, `Patterns/`, `Surfaces/`)", + "never in the frozen v1.0 roots (`Foundations v1.0/`, `Primitives v1.0/`, `Composites v1.0/`, " + "`Surfaces v1.0/`), whose story files are all tagged `deprecated`", "placed in the page tree the doc describes", - "the `v2.0` version tag", + "Each story file carries exactly one lifecycle tag in its own meta", + "`stable` for a shipped surface, `preview` for anything under `Labs`, `experimental` for a design-only " + "preview or anything under `Not In The Live App`; never `deprecated`, which only the v1.0 roots carry", + "(`tags: [\"!stable\", \"experimental\"]`)", "Adding, moving or renaming stories is a Storybook release", + "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 " + "` v2.0/` tagged `v2.0`", + "The handoff inventory judges story files by the scheme of the session's base (`storybookScheme`", "(`v1Stories`, `storiesInV2Taxonomy`)", + "the retitles a main import brought are never the session's own", ): assert required in text, required + for retired in ("New stories go in a v2.0 root", "Each story file carries the `v2.0` version tag"): + assert retired not in text, retired assert "V2_PLACEMENT_BY_ID" not in text assert "story-group-order.ts" not in text @@ -71,16 +89,22 @@ def test_every_story_author_and_advisor_points_at_the_doc_and_the_rule() -> None assert f"Place them per the repo's {TAXONOMY_DOC}" in primitive assert f"(`guardrails.md`, {POINTER})" in primitive primitives = section(GUARDRAILS.read_text(), "## Missing primitives") - assert 'Stories follow "Storybook taxonomy" below: a v2.0 root and the page tree, never a v1.0 root.' in primitives + assert ( + 'Stories follow "Storybook taxonomy" below: a current root, the page tree and a lifecycle tag, never a v1.0 root.' + in primitives + ) def test_the_guardrails_reviewer_blocks_v1_placement() -> None: reviewer = agent("vibe-guardrails-reviewer") for required in ( f"10. Storybook taxonomy (`guardrails.md`, {POINTER})", - "titled under or tagged for a frozen v1.0 root, or any edit or move of a story file that is v1.0 on main " - "(the inventory's `v1Stories`, reason `frozen`), is blocking", - "without the `v2.0` tag", + "the inventory's `storybookScheme` names that base's scheme", + "titled under or tagged for a frozen v1.0 root (a ` v1.0/` root or the `deprecated` tag; on a " + "version-scheme base, a plain root or the `v1.0` tag), or any edit or move of a story file that is v1.0 " + "on main (the inventory's `v1Stories`, reason `frozen`), is blocking", + "a title still under a ` v2.0/` root", + "a story file without exactly one lifecycle tag in its meta or with the wrong one for its place", "without the release bump the doc names", ): assert required in reviewer, required @@ -90,13 +114,38 @@ def test_the_guardrails_reviewer_blocks_v1_placement() -> None: def test_handoff_blocks_on_the_inventory_check() -> None: storybook = section(HANDOFF_SKILL.read_text(), "## 4. Storybook") assert f"(`../vibe/references/guardrails.md`, {POINTER})" in storybook - assert "the inventory's `v1Stories` and `retitledStories` block handoff" in storybook + assert "the inventory's `v1Stories` and `retitledStories` block handoff, judged by the scheme of the session's base" in storybook inventory = INVENTORY.read_text() assert "storiesInV2Taxonomy: v1Stories.length === 0," in inventory - assert "(?:Foundations|Primitives|Composites|Surfaces)\\//" in inventory + assert "storybookSchemeOf(readBaseFile(STORY_GROUP_ORDER_PATH))" in inventory + taxonomy = TAXONOMY_SCRIPT.read_text() + assert "(?:Foundations|Primitives|Composites|Surfaces)\\//" in taxonomy + assert "const\\s+StorybookLifecycle" in taxonomy + assert "const\\s+storybookBaselineVersion" in taxonomy def test_the_rule_is_stated_once() -> None: - canonical = "New stories go in a v2.0 root" + canonical = "New stories go in the current roots, titled under the plain level" holders = [path.name for path in sorted(PLUGIN_ROOT.rglob("*.md")) if canonical in flat(path.read_text())] assert holders == ["guardrails.md"] + + +def test_every_role_judges_a_session_by_the_scheme_its_base_uses() -> None: + decomposer = agent("vibe-storybook-decomposer") + assert ( + "Read that doc as the session's base has it: on a base older than the lifecycle scheme it still places " + "new work under ` v2.0/` with the `v2.0` tag." + ) in decomposer + writer = agent("vibe-change-worker") + assert ( + "after a main sync your own new entries move to the plain root with a lifecycle tag, which is not a retitle" + ) in writer + readme = flat((PLUGIN_ROOT / "README.md").read_text()) + assert ( + "Handoff judges story files by the scheme of the session's base, read from that base's " + "`apps/storybook/story-group-order.ts`, so a session still based before symphony-alpha #8867 keeps the " + "`v1.0`/`v2.0` rules until it syncs main, and a sync is never blamed for the renames it imported." + ) in readme + inventory = INVENTORY.read_text() + assert " storybookScheme: storybookScheme.summary,\n" in inventory + assert "...(storybookSchemeUnreadable ? { storybookSchemeRecognized: false } : {})," in inventory diff --git a/plugins/vibe/tools/python/test_vibe_v1_compile_only_repair_contract.py b/plugins/vibe/tools/python/test_vibe_v1_compile_only_repair_contract.py index 61fa143a..3a39df11 100644 --- a/plugins/vibe/tools/python/test_vibe_v1_compile_only_repair_contract.py +++ b/plugins/vibe/tools/python/test_vibe_v1_compile_only_repair_contract.py @@ -69,7 +69,7 @@ def agent(name: str) -> str: def test_the_placement_owner_states_the_exception_in_full_once() -> None: order = paragraph(agent("vibe-storybook-decomposer"), "4. Decide in this order") assert EXCEPTION in order - assert order.index("A v1.0 story file is frozen and never edited") < order.index(EXCEPTION) + assert order.index("is frozen and never edited, not even to add a story") < order.index(EXCEPTION) assert f"First ({RULING})" in order holders = [path.name for path in sorted(PLUGIN_ROOT.rglob("*.md")) if EXCEPTION in flat(path.read_text())] assert holders == ["vibe-storybook-decomposer.md"] @@ -78,8 +78,8 @@ def test_the_placement_owner_states_the_exception_in_full_once() -> None: def test_guardrails_design_pass_handoff_and_readme_carry_the_one_liner() -> None: guardrails = flat(GUARDRAILS.read_text()) assert ( - "A v1.0 story file is never edited beyond a compile-only repair (prop renames, imports, types; no story, " - "title, tag or visual change)" + "A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`) is never edited beyond a " + "compile-only repair (prop renames, imports, types; no story, title, tag or visual change)" ) in guardrails assert ( "A compile-only repair of a v1.0 file does not block; it is listed under `compileOnlyV1Repairs` for the " @@ -92,7 +92,7 @@ def test_guardrails_design_pass_handoff_and_readme_carry_the_one_liner() -> None "Its `compileOnlyV1Repairs` do not; the ticket worker puts each on the Engineering checklist." ) in storybook readme = flat(README.read_text()) - assert "A v1.0 story file is never edited beyond a compile-only repair" in readme + assert "A v1.0 story file (under a ` v1.0/` root or tagged `deprecated`) is never edited beyond a compile-only repair" in readme assert "handoff blocks any other edit of a v1.0 story file except a compile-only repair" in readme assert "handoff blocks any other edit of a v1.0 file," not in readme From c03ea163120dde396046ce7363ebb7530b08ce3c Mon Sep 17 00:00:00 2001 From: Daniel Ochoa Date: Fri, 16 Oct 2026 11:54:04 -0500 Subject: [PATCH 2/2] chore(vibe): Release 1.0.49 Changelog entry for the Storybook lifecycle scheme fix, and the base-scheme contract test reads agent text through flat() after the shared helpers from #299 started returning raw text. --- CHANGELOG.md | 15 +++++++++++++++ plugins/vibe/.claude-plugin/plugin.json | 2 +- plugins/vibe/.codex-plugin/plugin.json | 2 +- .../test_vibe_storybook_taxonomy_contract.py | 4 ++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 096d3f6b..12c101dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to the claude-plugins project will be documented in this fil The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Entries are listed newest-first; each plugin section is treated as released when merged to `main`. +### vibe v1.0.49 + +#### Fixed +- Handoff no longer reads symphony-alpha's current Storybook roots as frozen v1.0. symphony-alpha #8867 (ISS-12610, Storybook 3.0.0) made the ` v2.0` roots the plain roots, moved the old plain roots to ` v1.0`, and replaced the `v1.0`/`v2.0` tags with one lifecycle tag per story file (`stable`, `preview`, `experimental`, `deprecated`). 1.0.47 still treated every plain-root title as v1.0, so a session that synced main and moved its own entries to `Composites/...` with a `stable` tag was blocked, and an edit of a `Surfaces v1.0/` file went unchecked. +- `handoff-inventory.mjs` now judges story files by the scheme of the session's base, reported as `storybookScheme`. A new `storybook-taxonomy.mjs` reads the base's own `apps/storybook/story-group-order.ts`: when it exports `StorybookLifecycle`, the frozen tag is its `Deprecated` value and every frozen root ends in `storybookBaselineVersion`, the same test symphony-alpha's `validate:catalog` applies, so the next rename there moves these checks with it. A base without it keeps the version scheme (plain roots and the `v1.0` tag frozen). A lifecycle export this script cannot read blocks a session that changed a story file (`storybookSchemeRecognized`) instead of guessing. +- Every earlier ruling carries over on both schemes: v1.0 story files stay frozen apart from the compile-only repair (same meta title, tags and story exports as the base), a retitle is judged against the main the session last synced so a sync is never blamed for #8867's renames, and `Start Here/` stays exempt. The inventory's output keys are unchanged. + +#### Changed +- `guardrails.md` ("Storybook taxonomy"), the Storybook decomposer, the change and primitive workers, the guardrails reviewer (check 10), `design-pass.md`, the handoff skill and the README place new stories in the current roots with one lifecycle tag (`stable` shipped, `preview` under Labs, `experimental` design-only or not in the live app), put new states of a deprecated component in a new entry in the current root with a permanent name and no version number, keep an existing non-deprecated file's title and tag, and tell a session on a pre-#8867 base to follow that base's `v2.0` rules until it syncs. + +#### Tests +- `handoff-inventory-storybook-scheme.test.mjs` (5 cases): scheme detection from the real constant shapes and from a renamed scheme; current-root files new and edited on a lifecycle base (the 1.0.47 failure: both edited `stable` files were listed as `frozen`); the v1.0 roots and `deprecated` tag frozen with the compile-only repair kept; a session based before #8867 that syncs main and is not blamed for its renames; an unreadable lifecycle export. Four of the five fail on 1.0.47. +- The Storybook contract tests follow the new wording, plus one for the base-scheme rule. +- Two eval cases tagged `storybook` (Claude, and Codex through the runner): new states for a deprecated component, and a synced session's own `v2.0` entry. On this branch both pass on both harnesses; on main before this change Codex failed both and Claude failed the first. + ### vibe v1.0.48 #### Added diff --git a/plugins/vibe/.claude-plugin/plugin.json b/plugins/vibe/.claude-plugin/plugin.json index c1660f7d..3ce420ab 100644 --- a/plugins/vibe/.claude-plugin/plugin.json +++ b/plugins/vibe/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "vibe", "description": "Vibe sessions, handoff, seed refresh, and ClosedLoop ticket workflows", - "version": "1.0.48", + "version": "1.0.49", "dependencies": ["closedloop-core"], "author": {"name": "ClosedLoop", "email": "support@closedloop.ai"} } diff --git a/plugins/vibe/.codex-plugin/plugin.json b/plugins/vibe/.codex-plugin/plugin.json index 28a34512..0984efef 100644 --- a/plugins/vibe/.codex-plugin/plugin.json +++ b/plugins/vibe/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vibe", - "version": "1.0.48", + "version": "1.0.49", "description": "Vibe sessions, handoff, seed refresh, and ClosedLoop ticket workflows", "author": {"name": "ClosedLoop", "email": "support@closedloop.ai"}, "homepage": "https://github.com/closedloop-ai/claude-plugins", diff --git a/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py b/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py index 7dac7261..31ec0936 100644 --- a/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py +++ b/plugins/vibe/tools/python/test_vibe_storybook_taxonomy_contract.py @@ -115,12 +115,12 @@ def test_the_rule_is_stated_once() -> None: def test_every_role_judges_a_session_by_the_scheme_its_base_uses() -> None: - decomposer = agent("vibe-storybook-decomposer") + decomposer = flat(agent("vibe-storybook-decomposer")) assert ( "Read that doc as the session's base has it: on a base older than the lifecycle scheme it still places " "new work under ` v2.0/` with the `v2.0` tag." ) in decomposer - writer = agent("vibe-change-worker") + writer = flat(agent("vibe-change-worker")) assert ( "after a main sync your own new entries move to the plain root with a lifecycle tag, which is not a retitle" ) in writer