## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Design explorations for the board UI are drawn as coded "directions labs": one page that shows what ships today next to numbered design directions, on every surface a feature appears. The artifact directions lab (#17213) is the first one, and it fed the Artifacts V1 work (#17228). > - That lab was built by hand. Each new lab would repeat the same plumbing: the URL state, the toolbar, the theme and animation toggles, the fetch stub, the frames, the capture script and the Storybook registration. > - Labs should also look and move the same way, so a shared link reads the same to everyone, and the design-proposals Storybook should be the place where every exploration is registered. > - This pull request adds the `directions-lab` agent skill. It asks how many directions and which surfaces, then a generator scaffolds a complete, verified lab in one command and registers it in the design-proposals Storybook. > - The benefit is that a designer gets a working lab with production next to the directions in minutes, and the agent spends its time on the drawing, not on the plumbing. ## Linked Issues or Issue Description Refs #17213 (the artifact directions lab, the precedent the templates were derived from) and #17228 (Artifacts V1, which shipped a direction from that lab). **Subsystem affected** Agent skills (`.agents/skills/`) for UI design work, the `ui/` Vite build, and the design-proposals section of the Storybook (`ui/storybook/stories/design-proposals/`). **Problem or motivation** A directions lab is the repo's way to compare design directions with production, but there is no repeatable way to make one. The artifact lab took many sessions of hand work, and a second lab would copy that work file by file and drift from the first. The animation details, the toggles and the Storybook metadata are easy to get wrong by hand. **Proposed solution** Add a skill that does the intake (two questions: how many directions, which surfaces) and a generator, `scripts/scaffold.mjs`, that writes a self-contained lab from templates derived from the artifact lab. The generator verifies the lab with the catalog check, the lab's tests and a Vite build, and removes everything again when a check fails. Every direction carries the V1 MVP hover motion verbatim, and a test in the skill fails when the precedent's motion block changes. **Alternatives considered** A shared runtime under `ui/src/directions-lab/` that every lab imports. It removes the per-lab copies, but it is product code under typecheck and a much larger change; it stays a follow-up. Instructions only, with no generator: the agent would rebuild the plumbing every time and the labs would drift. **Roadmap alignment** This fits the skills layer in `ROADMAP.md` (reusable procedures for agents). It does not change the Skills Manager, Skill Studio or the Skills Store, and it adds no product feature. ## What Changed - Add `.agents/skills/directions-lab/` with `SKILL.md`, `agents/openai.yaml` (Codex metadata) and the `.claude/skills/directions-lab` symlink, the same layout as the other repo skills. - Add `scripts/scaffold.mjs` with the subcommands `new`, `check`, `add-direction` and `list`, plus `scripts/lib/*`. - `new` renders in memory and refuses comment lines, files of 600 lines or more, and existing paths. - It then writes the lab, two `.claude/launch.json` entries and one `.gitignore` line. - It verifies the result, and rolls everything back on failure. - Add 34 templates under `assets/templates/` and the motion block `assets/motion/v1-mvp-motion.css`, derived from `ui/src/artifact-directions` at the commit recorded in `assets/PRECEDENT`. - A generated lab has: - an html entry and a Vite config; - a registry that the toolbar, keyboard digits, Board and capture script read at runtime; - a toolbar whose surfaces row is written only for a multi-surface lab; - a Today kit stub, one kit per direction with a motion specimen, five Vitest files and `shot.cjs`; - a design-proposals story with meta id `design-<slug>-directions` and a literal `parameters.paperclip` block (`status: "proposed"`, `home: "design-proposals"`). - Add ten references: - lab anatomy; - Today, fixtures, build and deploy; - the motion spec; - questions and defaults; - drawing phases; - design-proposals Storybook registration; - lifecycle; - repo gates; - the generator; - `canonical-directory.md`, the paste-ready governance diffs that would make the design-proposals section the source of truth. They are for a separate pull request and are not applied here. - Add three eval cases under `evals/`: a two-surface lab with three directions, a one-surface lab with two directions, and a terse brief that must make the skill ask how many directions. ## Verification - `node --test .agents/skills/directions-lab/scripts/scaffold.test.mjs`: 29 tests pass in under a second. The tests cover flag parsing, context derivation and refused inputs, ports, the write guards and the shared-file edits. They also render the three eval shapes into a temporary root, check the surfaces-row rule and the mode-B story status rule, and run the motion drift test against `ui/src/artifact-directions/directions/d5-v1-mvp/tokens.css`. - A real scaffold into this checkout at the branch head: ```sh node .agents/skills/directions-lab/scripts/scaffold.mjs new --slug prsmoke --title "PR smoke" --surfaces thread:Task-thread:960@tasks,panel:Right-panel:434@tasks --directions 3 ``` The built-in checks passed: `node scripts/storybook-catalog.mjs --check`, `npx vitest run src/prsmoke-directions` and the lab's Vite build. The lab was then removed, and `git status` shows only the skill. - Browser check on a scaffolded lab (composer, two surfaces, three directions), done earlier in the same session: - The toolbar shows both rows with Today last as `0.Today`, and the theme and animation toggles. - A hovered motion specimen computes `translateY(-6px)` on the front, and the backings turn ±5° from their resting angles with a 300 ms transition. - After pressing `a`, nothing moves. - `add-direction` and `check --json` were proven on a second scaffolded lab: the toolbar legend reads `0–3` from the registry without a hand edit. - The root Vitest run is unaffected: `vitest.config.ts` lists explicit projects, and `.agents` is not one of them. The skill tests with `node --test` like the sibling skills. - Not run: `pnpm -r typecheck`, `pnpm build` and `pnpm build-storybook`. This change adds only files under the git-ignored `.agents/` tree plus one symlink, and touches no product code. ## Risks Low risk. The pull request adds a skill and a symlink and changes no product code, no dependencies and no lockfile. A lab that someone later scaffolds lands under `ui/src/`, where `tsc -b` and `pnpm test` see it. The generator verifies each lab and rolls back on failure, so a half-written lab is not left behind. `/.agents/` is git-ignored, so later edits to the skill must be force-added (`git add -f`), as for the other repo skills. The templates are copies of the precedent. When the artifact lab changes, the skill's drift test fails until the templates are updated and `assets/PRECEDENT` is bumped. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Claude Fable 5.1 (Anthropic), model ID `claude-fable-5-1`, with extended thinking, for most of the work: mapping the precedent, the references, the generator design and implementation, and the proofs. Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, for the final cleanup and this pull request. Both ran in Claude Code with tool use (file edits, shell, the in-app browser) and multi-agent workflows for parallel mapping, judging and verification. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
21 KiB
Repo gates a lab must pass
Nothing in CI knows about labs: a lab PR merges through the same gates as any ui/ change. Each gate
below names the rule it checks and the local command that proves it. Every rule is GENERIC unless marked
ARTIFACT-SPECIFIC; artifact-specific values come from the worked example (the artifact directions lab,
PR #17213) and must be redrawn per feature. Line numbers were confirmed on claude/directions-lab-skill
at 5e26b733b.
Contents
- Storybook catalog rules for design-proposal stories
- Validate the catalog; generated outputs are git-ignored
- Token gates and the lab's
tokens.css - PR rules from CONTRIBUTING and the template
- commitperclip gates (blocking)
- Greptile and the
rules/*.mdcfiles - Vitest conventions and the lab-only test command
- Hand-off checklist
1. Storybook catalog rules for design-proposal stories
scripts/storybook-catalog.mjs parses every story file without executing it (loadCsf, line 65) and
validates the result (lines 83-116). Line numbers in 1.1-1.5 refer to that script unless a path is given.
1.1 Location, title, meta id
- File under
ui/storybook/stories/design-proposals/(storyRootline 12; folder rule line 107). Storybook's glob../stories/**/*.stories.@(ts|tsx|mdx)(ui/storybook/.storybook/main.ts:23) finds it. - Title starts with
Design proposals/(specialHomesline 17; rule line 108). Convention:Design proposals/<Lab display name>/<Suite>. - Explicit meta
id(line 109). Convention:design-<lab>-<suite>. Taken ids:grep -n "^ id:" ui/storybook/stories/design-proposals/*.stories.tsx(20 files, 18 prefixeddesign-). - ARTIFACT-SPECIFIC:
design-artifact-directions-compare,design-artifact-directions-hybrid, groupArtifact directions(ui/storybook/stories/design-proposals/artifact-directions-compare.stories.tsx:39-40,artifact-directions-hybrid.stories.tsx:28-29). - The sub-order under Design proposals is hardcoded in
scripts/storybook-feature-index.mjs:11(Foundations, Components, Layouts, Pages, AI Generated Test Pages, Patterns, Guidelines); a lab group outside the list sorts after them; the catalog does not enforce group names. Never edit the generatedpreview.tsx(maps/storybook-map/README.md:90). ui/storybook/.storybook/preview-base.tsx:853wraps a story whoseparameters.paperclip.homeis"design-proposals"in<AvatarInitialProvider><div className="design-proposal-theme">unlessparameters.designProposalTheme === false;html:has(.design-proposal-theme)switches--font-monoto Paper Mono (ui/src/index.css:3916-3918). Theme toolbar defaults to dark (line 861); a11y istest: "error"(line 876).
1.2 The parameters.paperclip block: fields and allowed values
Allowed keys (line 16): capabilities, status, implementation, boundary, notes, home,
exception, replacement; any other key throws unknown paperclip field (line 57).
| Field | Allowed values | Lab value |
|---|---|---|
home |
foundations, design-proposals (line 95) |
"design-proposals" |
status |
current, proposed, historical, mixed, unreviewed (line 13) |
"proposed"; forced by the home (line 99) |
exception |
nonempty string (line 96) | required by the home (line 100) and by empty capabilities (line 103) |
capabilities |
array of strings (line 94); each an existing <area>/<key> (line 102); no duplicates (line 104) |
see 1.4, or [] with an exception |
implementation |
production, prototype, mixed, guide, unreviewed (line 14) |
"prototype"; "production" when the story mounts shipped components |
boundary |
fixtures, fixture-server, static, unreviewed (line 15) |
"fixtures" |
notes |
string (line 93) | what is simulated; say nothing proves production behaviour |
replacement |
repo-relative path of an existing story file (line 110) | omit |
Defaults when omitted: status, implementation, boundary are "unreviewed"; notes is "" (line 77).
1.3 Placement and the literal-only rule
parametersis an object literal with exactly one non-computedpaperclipproperty (lines 47-51).- Every value inside the block is a string, boolean, number, array or object literal;
asandsatisfiesare unwrapped (lines 26-42). An identifier, call, spread or template string inside the block throwsmetadata must be literal; expressions are not executed(line 42). Expressions inargTypes,argsandcomponentare fine. - No
...spreador computed property may come afterpaperclip(lines 52-54). Literal properties may follow it; placing it last is convention, not rule. annotation()throws at scan time (one error per run);validateCatalogerrors are joined (lines 189-190).
1.4 Capabilities
Ids come from maps/feature-map/<area>.md: the file stem is the area, each row - \key`: ...under## Sub-featuresis a key, and the id is/matching/^[a-z0-9-]+/[a-z0-9-]+$/ (scripts/feature-map.mjs:24-37`).
ls maps/feature-map/lists the areas; read the area's## Sub-features.- No fitting area:
capabilities: []plus anexception(ui/storybook/stories/design-proposals/colors.stories.tsx:13-21). - Do not add a feature-map area without its agent-access companion:
checkMapsvalidates both (scripts/check-maps.mjs:31-34,maps/README.md:10-11) and runs before the catalog (line 184). - Proposals never count as coverage:
renderVerificationskipsproposedandhistoricalentries (line 165) and the index files them under "Proposed design system" (line 133). - ARTIFACT-SPECIFIC:
documents-artifacts/library,documents-artifacts/work-products,documents-artifacts/attachments(maps/feature-map/documents-artifacts.md:17-21).
1.5 Uniqueness
Meta ids are unique among meta ids (lines 112, 114). Story ids (per export) are unique and must not
collide with the generated feature-map-<featureId>--overview ids (line 113): never a meta id
feature-map-<area> with an export named Overview.
1.6 Worked example
ui/storybook/stories/design-proposals/artifact-directions-compare.stories.tsx:38-59. Reproduce the
shape; redraw every string.
const meta = {
id: "design-artifact-directions-compare",
title: "Design proposals/Artifact directions/Compare",
component: DirectionView,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Pick a direction and a surface. Today is the shipped components fed with the lab's fixtures; 1–5 are the explored directions.",
},
},
paperclip: {
capabilities: ["documents-artifacts/library", "documents-artifacts/work-products", "documents-artifacts/attachments"],
status: "proposed",
home: "design-proposals",
implementation: "prototype",
boundary: "fixtures",
exception: "Design exploration staged in the artifact directions lab: the shipped baseline and five proposed directions compared on static mocks of the task thread, the side panel and the Artifacts page.",
notes: "Controls pick a direction kit and a surface. Today mounts the shipped artifact components behind the lab's fetch stub with the acmehats fixture world; the other kits are lab components. Frames are static mocks of the product chrome; nothing here establishes production behaviour or access checks.",
},
},
Lines 60-74 add argTypes built from runtime constants (DIRECTION_IDS, KIT_META, SURFACE_IDS),
default args, and } satisfies Meta<typeof DirectionView>;. GENERIC: layout: "fullscreen",
docs.description.component, the six paperclip fields, runtime constants in argTypes.
ARTIFACT-SPECIFIC: every string, the constants, the default args. The per-direction story
(artifact-directions-hybrid.stories.tsx:27-48) has the same block with no component and one render
export per surface and per slot (lines 56-155).
2. Validate the catalog; generated outputs are git-ignored
node scripts/storybook-catalog.mjs --check
--checkvalidates without writing; no flag or--writealso writes the generated files (lines 202-206); any other argument throws the usage error. Success printsStorybook map: N files, M stories. Structure only; no runtime proof.(316 files, 2973 stories here).cd ui && pnpm typecheckruns the catalog in write mode first:"typecheck": "node ../scripts/storybook-catalog.mjs && tsc -b"(ui/package.json:23).ui/tsconfig.json:19includessrc,storybook,vitest.setup.ts, so lab code, tests and stories are typechecked.- CI runs it on every PR: the Typecheck lane runs
pnpm run typecheck:build-gaps(.github/workflows/pr-trusted.yml:333;package.json:25), andscripts/run-typecheck-build-gaps.mjs:143-150,178-183runspnpm --filter <name> typecheckfor each workspace whosebuildscript has notsc, which includesui("build": "vite build",ui/package.json:18). pnpm storybookandpnpm build-storybookcallgenerateStorybook()at config load (ui/storybook/.storybook/main.ts:10), so a bad annotation also stops Storybook.pnpm build-storybookis not on the default PR path, butDESIGN.md:814lists it as done-when: run it or report it as not run.- The visual suite runs only with the
storybook-visuallabel (.github/workflows/storybook-visual.yml:42) and screenshots every story in both themes withmaxDiffPixels: 0(tests/storybook-visual/playwright.config.ts:42,storybook-visual.spec.ts:108-118); new stories have no baseline until a maintainer runspnpm test:storybook-visual:update. ui/storybook-static/,ui/storybook/generated/,ui/storybook/.storybook/preview.tsxandmaps/generated/are ignored (.gitignore:11-14). Never commit or hand-edit them (maps/storybook-map/README.md:86-90).
3. Token gates and the lab's tokens.css
scripts/check-token-gates.mjs scans only ui/src/components/** and ui/src/pages/** (SCAN_DIRS,
line 80), only .ts/.tsx/.js/.jsx files (line 111); Gate 4 reads ui/src/index.css alone (lines 81,
296-303). No other CSS file is read.
- GENERIC: a lab under
ui/src/<lab>/is outside gates 1-3. Its per-directiontokens.cssmay hold raw hex, px, ms andcubic-beziervalues and its components may use Tailwind bracket values;DESIGN.md:30permits this for demo/UX-lab surfaces (doc/design/DECISION-SHEET.md:20, ruling B1). Keep every raw value in atokens.cssscoped to[data-direction="<id>"], light values in the base block and dark overrides under.dark [data-direction="<id>"]. - Anything that graduates to product moves under
components/orpages/and must become tokens inui/src/index.css(DESIGN.md:17,30; atokens.cssis allowed there only whenindex.cssimports it). The gates would fail the lab code as it stands. - A lab page that must live under
ui/src/pages/needs an* allow ui/src/pages/<File>.tsx — <reason>line inindex.css(check-token-gates.mjs:89-104; precedentui/src/index.css:3195-3198). - ARTIFACT-SPECIFIC evidence:
ui/src/artifact-directions/directions/d5-v1-mvp/PanelRow.tsx:30,32,65(gap-[3px],h-[2px],rounded-[4px]),d5-v1-mvp/tokens.css:9-12,24-25,56-65(hex, rgba, px),d4-hybrid/tokens.css:97(--hy-stagger: 28ms).
pnpm run check:token-gates
package.json:47 chains check-token-gates.mjs, sync-agent-palette-tokens.mjs --check and
sync-cliplab-character.mjs --check. Use pnpm run; the bare shorthand has failed with
Command not found under pnpm 9.15.4 in a worktree. Expect four gates CLEAN.
4. PR rules from CONTRIBUTING and the template
- Fill every section of
.github/PULL_REQUEST_TEMPLATE.md:## Thinking Path(line 3),## Linked Issues or Issue Description(19),## What Changed(52),## Verification(58),## Risks(67),## Model Used(78),## Checklist(93-108). Delete the template's HTML comments. CONTRIBUTING.md:49: no code file moves from under 1,000 lines to over.CONTRIBUTING.md:50: "Do not submit a PR that includes code comments." Neither is checked by commitperclip; Greptile enforces the 1,000-line rule throughrules/pull-request-review.mdc:22-25. PR #17213 merged with doc comments, which are still in the lab's stories,tokens.cssfiles and Vite config; the Artifacts V1 feature (PR #17228) needed a follow-up commit (e1433f508) to strip its own: scaffold comment-free.- Public references only:
#NNN,Fixes #NNN,github.com/paperclipai/paperclipURLs; noPAP-123ids, instance or localhost links (CONTRIBUTING.md:68-80). - Descriptive kebab-case branch name before pushing (
CONTRIBUTING.md:82-98). No screenshots in the repo; put them in the PR (CONTRIBUTING.md:100-106). Model Used: provider, exact model id, context window, reasoning mode, tools (CONTRIBUTING.md:108-110). Simplified Technical English (CONTRIBUTING.md:173). Verification names what was not run and why (AGENTS.mdsection 7).
5. commitperclip gates (blocking)
.github/workflows/commitperclip-review.yml runs on pull_request_target (lines 4-5) from master
(lines 20-23) and fails when a blocking gate fails (lines 57-63). Blocking (.github/scripts/run-quality-gates.mjs:139-145;
the check scripts below sit in the same directory):
template, linked issue, dedup search, test coverage, lockfile. Dependencies, release bootstrap and
co-authors are informational.
Template (check-pr-template.mjs)
Lines 9-15: ## Thinking Path needs 3 or more items; ## What Changed, ## Verification, ## Risks,
## Model Used need 1. Content runs to the next \n## (lines 23-29); items split on terminal
punctuation, bullet or quote line starts, or blank lines, keeping items over 5 characters (line 35).
Empty content, _No response_ or content starting with <!-- fails (line 56). Model Used fails on
provider, model id, your model, <model> (lines 17-21).
Linked issue (check-pr-linked-issue.mjs)
Skipped for docs, chore, build, ci, style, test, revert (line 21). A feat: PR passes by
path A or B:
- A:
(fixes|closes|resolves|refs) #N, agithub.com/paperclipai/paperclip/issues/NURL, or a bare#N(lines 14-18); HTML comments are stripped first (145-147, 178). - B: at least 3 filled fields from ONE template's field set (line 25; loop 153-157); labels from different
templates do not add up. A label stands alone on its line as
## Label,**Label**orLabel:; the content under it must be real: a lone-or[...]does not count (109-118, 122-132), and a label followed directly by another label is empty. - Feature-request labels the gate counts (lines 39-44):
Problem or motivation(Problem,Motivation),Proposed solution(Solution),Alternatives considered(Alternatives),Roadmap alignment(Roadmap)..github/ISSUE_TEMPLATE/feature_request.ymlalso hasSubsystem affected(line 12) andAdditional context(line 52); they do not count toward the 3. - PR #17213 used path B with all six as
**Label**lines. ARTIFACT-SPECIFIC: its roadmap line ("Artifacts & Work Products").
Dedup search (check-pr-dedup-search.mjs)
A checked checkbox line matching
/^\s*[-*]\s*\[\s*([ xX])\s*\][^\n]*search(?:ed)?[^\n]*(?:similar|duplicate|prior)[^\n]*\bprs?\b/im
(lines 20-21). Check template line 98:
- [x] I have searched GitHub for duplicate or related PRs and linked them above. The other 13 checklist
items are not machine-checked.
Test coverage (check-pr-test-coverage.mjs)
Skipped for docs, chore, build, ci, style, refactor, revert (line 21). Otherwise at least
one non-removed file matches /\.test\.(ts|js|tsx|jsx|mjs|cjs)$/, /\.spec\....$/, /(?:^|\/)tests?\//
or /\/__tests__\// (lines 11-16, 64-66). .stories.tsx files and fixture factories like
test-artifact.ts do not count. A docs:/chore:/build:/ci: title with source changes fails
(lines 25, 43-57). Title a lab PR feat(ui): ... and ship at least one .test.ts(x).
Lockfile (check-pr-lockfile.mjs)
A diff touching pnpm-lock.yaml fails unless the author is github-actions[bot] on
chore/refresh-lockfile (lines 9-14). A lab adds no dependencies and commits no lockfile change.
6. Greptile and the rules/*.mdc files
- Greptile must score 5/5 with no open P2+, recommendations or follow-ups (
CONTRIBUTING.md:126-134); it reads rules through.greptile/files.jsonand.greptile/rules.md(rules/README.md:15-19). - No rule's
globscoversui/orui/storybook/(scoped rules coverserver/,packages/db, adapters, adapter-utils, plugins, runner). Only the threealwaysApply: truerules apply to a lab PR:rules/pull-request-review.mdc(no screenshot requests, lines 17-21; 1,000-line crossing, lines 22-25),rules/module-boundaries.mdc,rules/product-harness-responsibility.mdc. .greptile/rules.mdlast line: "Do not add package.json scripts for new utilities, checks, benchmarks, or specialized tests." Invoke the lab's Vite config directly and record the command in.claude/launch.json(ARTIFACT-SPECIFIC:.claude/launch.json:27-44, ports 5180 and 5190;ui/vite.artifact-directions.config.mjs:10-26).
7. Vitest conventions and the lab-only test command
The ui project (vitest.config.ts:28) uses environment: "node", processes no CSS except
motion-tokens.css, and loads ./vitest.setup.ts (ui/vitest.config.ts:11-14), which mocks
localStorage and stubs Element.prototype.scrollIntoView (ui/vitest.setup.ts:3-40). Kit
import "./tokens.css" lines are inert; assert on class names, attributes and inline styles, not
computed styles.
- Pure modules: no pragma,
import { describe, expect, it } from "vitest", relative import of the module (ui/src/artifact-directions/lab-state.test.ts:1-7,directions/d5-v1-mvp/when.test.ts:1-4,kinds.test.ts,MediaPrint.test.ts). - DOM tests (
ui/src/artifact-directions/directions/d5-v1-mvp/Thread.test.tsx:1-27; the hooks sit insidedescribeafterlet container: HTMLDivElement; let root: Root;):
// @vitest-environment jsdom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
Render with act(() => root.render(<X />)). A test that stubs window.fetch restores it in afterEach
(ui/src/artifact-directions/api-stub.test.ts:7-14). A fixture factory test-<entity>.ts takes
Partial<T> & Pick<T, "kind"> (d5-v1-mvp/test-artifact.ts) and is not a test file for the coverage gate.
When a tested component renders the app's Link, mock CompanyContext and wrap in MemoryRouter:
ui/src/lib/router.tsx:67 exports a Link that reads useCompany() (lines 5, 31). Pattern
(ui/src/plugins/outlet-errors.test.tsx:4,13,35):
import { MemoryRouter } from "react-router-dom";
vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompany: null }) }));
The artifact lab's own tests render no Link and do not use this mock.
Run only the lab's tests (ui/package.json has no test script):
cd ui && npx vitest run src/<lab>
ARTIFACT-SPECIFIC: cd ui && npx vitest run src/artifact-directions runs 6 files, 31 tests. Run one
full ui vitest at a time; concurrent full runs produce spurious failures.
8. Hand-off checklist
Run each from the repo root, in this order; paste results under ## Verification with a Not run: line
for anything skipped and why. The first five are what
node .agents/skills/directions-lab/scripts/scaffold.mjs check --slug <slug> --run runs, stopping at the
first failure (scripts/lib/check.mjs:148-154); pnpm build-storybook stays a manual step that --run
reports as Not run:.
cd ui && npx vitest run src/<slug>-directions
node scripts/storybook-catalog.mjs --check
pnpm run check:token-gates
cd ui && <SLUG>_DIRECTIONS_OUT_DIR=node_modules/.<slug>-directions-dist npx vite build --config vite.<slug>-directions.config.mjs
cd ui && pnpm typecheck
pnpm build-storybook
- Fresh worktree:
pnpm install --frozen-lockfileand build@paperclipai/plugin-sdkfirst, ortscreportsCannot find module '@paperclipai/plugin-sdk/ui'inPluginOrganizationSwitcher(.test).tsx. Repo-widepnpm -r typecheckneeds the Rust toolchain forpaperclip-runner; report it as not run when absent. - PR title
feat(ui): ...; one or more.test.ts(x)files in the diff; nopnpm-lock.yamlchange; no newpackage.jsonscript; no file crossing 1,000 lines; no code comments; no committed screenshots or generated files. - Body: five
##sections with content, path A link or path B with 3 filled feature-request labels, checked dedup checkbox, concreteModel Used. Then Greptile 5/5 and maintainer approval.