## 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]>
16 KiB
Questions and defaults - the intake before scaffold.mjs new
Read this after SKILL.md and before touching the generator. It covers only the intake: what to pull out
of the brief, how to ground it in the code, the one AskUserQuestion call, and how every flag of
node .agents/skills/directions-lab/scripts/scaffold.mjs new is derived. What the generated files look
like is in generator.md §5, their precedent in lab-anatomy.md and today-fixtures-and-build.md; the
Storybook entry and its ids are in design-proposals-storybook.md and repo-gates.md §1; later stage
moves are hand edits per lifecycle.md.
Contents
- Extract from the brief
- Ground the subject in the code
- The one question call
- When no question can be asked
- What is never asked
- Flag derivation table
- The three eval briefs as flag lines
- State every derived value before
new
1. Extract from the brief
Read the brief once and fill these four slots. Each slot either comes from the brief or from a step below; nothing else is invented.
| Slot | Look for | Becomes |
|---|---|---|
| Subject | the feature noun ("the task composer", "the inbox row", "the agent profile card") | --slug (kebab-case of the noun, no "directions", no "lab") and --title (sentence case) |
| Places | "shows up in", "appears on", "in the right panel", "on the inbox page", a list of locations | --surfaces, one entry per place; one place means no surfaces row |
| Count or names | "3 new directions", "2 directions", or named directions ("Solid, Tint") | --directions and, only when named, --names |
| Flags and components | enable..., use...Enabled, a component or file name |
the Today kit's flags, and a head start for the grep in §2 |
A brief that states a count skips Q1; a brief that names the places skips Q2 (§3). Everything the brief leaves open is either asked (count, places) or derived (everything else, §6). Keep the brief's own words for the surface labels when it gives them ("Task thread", "Right panel"); the generator only needs them to be short.
2. Ground the subject in the code
Do this before asking anything. Why: Q2's options must be real usage sites, and Phase 2 of the drawing
(drawing-phases.md) needs the flag hook and the API routes that the Today kit must answer. Run from the
repo root; drop test files so the hits are product code.
rg -l -i "<subject word>" ui/src/components ui/src/pages --glob '!*.test.*'
rg -ln "<ComponentName>\b" ui/src/components ui/src/pages --glob '!*.test.*'
rg -n "use[A-Za-z]*Enabled\(" <component files>
rg -n "useQuery\(|queryFn|queryKeys\." <component files>
rg -n -i "<subject word>" maps/feature-map/*.md
- Component files. The first grep finds the files that define and render the subject. The second,
run on each defining component, lists the places it is used; those usage sites are the Q2 options.
Record whether the hits are under
ui/src/componentsor only underui/src/pages;--groupfollows from that (§6). - Flag hook. Every experimental flag is read through a
use<Feature>Enabled()hook inui/src/hooks/that queriesqueryKeys.instance.experimentalSettings(ui/src/lib/queryKeys.ts:614) throughinstanceSettingsApi.getExperimental(), for exampleui/src/hooks/useStreamlinedUiEnabled.ts:27-32. The generatedapi-routes.tsanswers that one route fromregistry.today.flags, so the hook name and the flag key are all you need to write down. - API routes. Follow each
useQuery/queryFnintoui/src/api/<module>.ts. Every path string there is appended toBASE = "/api"(ui/src/api/client.ts:5, used at:54), which is why the lab's fetch stub can answer them by pathname (today-fixtures-and-build.md§3). List the paths; Phase 2 registers exactly those. - Capability ids.
scripts/feature-map.mjs:6-10takes the area from the file stem of eachmaps/feature-map/<area>.md; lines24-31read every row of the form- `key`: ...under## Sub-featuresand build the id<area>/<key>; line34enforces/^[a-z0-9-]+\/[a-z0-9-]+$/.--capabilitiesis validated against that samefeatureIndex(root), so an id you type must exist as a row, not merely read well. Open the area file's## Sub-featuresblock and pick the row whose description names what the subject does; when no row does, the answer is[](§6).
3. The one question call
One AskUserQuestion call, at most two questions, placed after §2 and before any file is written. Each
question is dropped when §1 already answered it; when both are answered, no call is made. Headers stay
within 12 characters and each question carries 2-4 options, which is the tool's limit.
Q1 (skipped when the brief states a count or names the directions)
header:Directionsquestion: "How many directions should the lab explore next to Today? Each direction is a level of exploration. Unnamed directions scaffold as 1.Refine (tokens and spacing, same structure), 2.Rework (new components, same layout), 3.Rethink (new layout) and are renamed later by hand (lab-anatomy.md§5 lists the touch points)."- options:
2with description "Today + 1.Refine + 2.Rethink";3with description "Today + 1.Refine + 2.Rework + 3.Rethink".
Q2 (skipped when the brief names the places)
header:Surfacesquestion: "Which product surfaces does appear on? One tab per place."multiSelect: true- options: up to three usage sites from §2, each labelled by the product place and described by the file
that renders it there (for example "Task list row" /
ui/src/components/IssuesList.tsx), plus a last option "Only one place" described as " on its primary surface only; no surfaces row". With more than three sites, choose the three most distinct product places and name the rest in the closest option's description; a surface left out is added by hand later (lab-anatomy.md§8).
Two or more selections produce the surfaces row; one selection or "Only one place" produces a
single-surface lab with the legend joined into row 1 (lab-anatomy.md §8, last paragraph).
4. When no question can be asked
AskUserQuestion is not in every harness. When the call is unavailable or the run is non-interactive,
write this sentence in the reply before running anything, then proceed with the §6 defaults:
Defaulting to three directions and one surface () because I cannot ask; change later with
scaffold.mjs add-direction.
<surface> is the place the brief names, else the subject's primary surface from §2: the first usage site
under ui/src/pages in grep order, else the first under ui/src/components. Say which one and why in the
same reply. add-direction is the only lifecycle subcommand this version has; removing a direction or
adding a surface is a hand edit (lab-anatomy.md §5 and §8), which is why the sentence names only the
move the CLI can make.
5. What is never asked
Theme (both palettes ship behind the t toggle), animation (always on behind a), Today (always present,
always the shipped components on fixtures), ports, slug, landing, title, group, company and capabilities.
Why: each is fixed by the lab contract or derivable from the brief and the grep, and the question call is
the one chance to ask; spending it on a derivable value costs an answer that only Tonio can give.
6. Flag derivation table
Run-mode flags (--dry-run, --json, --skip-verify, --no-build, --keep-on-failure, --root) are not
derived; always run --dry-run first.
| Flag | Derivation | Default |
|---|---|---|
--slug |
kebab-case of the subject noun; the generator adds -directions to the directory, html, config, story and launch entries, refuses a slug that already ends in -directions, and refuses when any path it would write exists, so run scaffold.mjs list first |
required |
--title |
sentence case of the subject ("Inbox row"); the page title becomes <Title> directions |
the slug in sentence case (Agent profile card) |
--surfaces |
id:Label:width@nav,..., one per place, 1-6 entries. id is a short lowercase word (never board), Label the brief's own name for the place, rendered as typed (the precedent hyphenates, Task-thread; quote the flag for spaces), width from the product constant (table below), nav one of tasks, artifacts, other (below) |
required |
--directions |
the stated count or the Q1 answer; 2 or 3; 4-9 only with --force-count, which WARNs |
required |
--names |
only when the brief names the directions, comma-separated, in order | Refine,Rework,Rethink; Refine,Rethink for 2; required with --force-count |
--group |
Components when any §2 hit is under ui/src/components, Pages when every hit is under ui/src/pages; Layouts and Patterns only when the brief says so. The vocabulary is the existing Storybook section order at scripts/storybook-feature-index.mjs:11 |
Components |
--capabilities |
the validated <area>/<key> ids from §2 whose description names what the subject does; when none fits, [], and the story carries the exception sentence repo-gates.md §1.4 describes |
[] |
--company |
name:PREFIX, fixed so the fixtures and the stub's /companies fallback match the precedent (today-fixtures-and-build.md §3) |
acmehats:ACM |
--landing |
auto picks d=all&s=board when cells = (directions + 1) × surfaces ≤ 8, else d=all&s=<first surface>; all, today or a direction id override it |
auto |
--ports |
chosen by the generator: the first dev,dev+10 pair from 5200/5210 upward in steps of 20 that avoids every port in .claude/launch.json (today 3121, 6016, 4173, 5180, 5190), RESERVED_PORTS in scripts/lib/ports.mjs:5 and any ui/node_modules/.vite-*-<port> cache directory; pass dev,preview only to override, and a taken pair is refused |
generator |
Widths are product constants, never measured from a screenshot. Re-check the line numbers when you cite
them; three frame comments in the precedent had drifted (lab-anatomy.md §8).
| Place | Width | Source |
|---|---|---|
| Task-chat column (thread, composer dock) | 960 |
ui/src/index.css:575 --tc-shell-max-w: 60rem, used by ui/src/components/task-chat/TaskChatComposerDock.tsx:19 and ui/src/components/TaskChatThread.tsx:3072 |
| Right pane (task detail properties panel) | 434 |
ui/src/components/PropertiesPanel.tsx:88 TASK_DETAIL_DEFAULT_PANE_WIDTH = 434 |
| Company page body | 1152 |
max-w-6xl (72rem) at ui/src/pages/Artifacts.tsx:248; the same class caps ui/src/pages/PluginManager.tsx:192 and ui/src/pages/Secrets.tsx:1808 |
A place with no max-w-* on its wrapper (the inbox list fills the main column; ui/src/pages/Inbox.tsx sets none) |
960 |
the default; say in the reply that the width is the default, not a product constant |
nav takes tasks, artifacts or other (scripts/lib/context.mjs:5); the generated AppShell highlights only
Tasks (ui/src/components/Sidebar.production.tsx:182, route /issues) or Artifacts (:196). A place under
Inbox (:156) or Agents (ui/src/components/Sidebar.tsx:273) takes other and gets its highlight by hand in
Phase 1 (drawing-phases.md §1).
7. The three eval briefs as flag lines
Each case under .agents/skills/directions-lab/evals/<name>/prompt.md exercises one intake path. Read the
prompt, then compare with the line below.
multi-surface-three-directions - the brief states 3 and names two places, so no question is asked.
Subject "task composer" → composer. The grep finds ui/src/components/task-chat/TaskChatComposer.tsx,
rendered at the bottom of the thread by ui/src/components/TaskChatThread.tsx:3305 inside the dock
(TaskChatComposerDock.tsx:19, width 960); the thread is IssueChatThread or TaskChatThread, chosen at
ui/src/pages/IssueDetail.tsx:1473-1474 and mounted at :2519. The product has no chat tab in the right
panel: the 434px pane (PropertiesPanel.tsx:88) carries Properties | Plan | Artifacts (:72), so the
panel surface comes from the brief alone and its frame follows the precedent's PanelFrame.
The flag hook is useStreamlinedUiEnabled (TaskChatThread.tsx:495). maps/feature-map/tasks.md:21
tasks/discussion ("persist comments, references, and files on the task") is the row that names what a
composer does. Group Components. Cells 4 × 2 = 8, so the landing is the board.
node .agents/skills/directions-lab/scripts/scaffold.mjs new --slug composer --title Composer \
--surfaces thread:Task-thread:960@tasks,panel:Right-panel:434@tasks --directions 3 \
--capabilities tasks/discussion --dry-run
single-surface-two-directions - the brief states 2 and one place ("on the inbox page"), so no question
is asked. Subject "inbox row" → inbox-row, title Inbox row. The row is IssueRow from
ui/src/components/IssueRow.tsx, imported at ui/src/pages/Inbox.tsx:87 and rendered at :2851; the
inbox list has no width cap, so 960 is the stated default; the shell highlights neither Tasks nor Artifacts
there, so nav is other. Group Components (the row lives under ui/src/components).
maps/feature-map/inbox-search.md:17 inbox-search/views names the views the row lists; add
inbox-search/triage (:18) only when the brief includes the archive affordance. Cells 3 × 1 = 3.
node .agents/skills/directions-lab/scripts/scaffold.mjs new --slug inbox-row --title "Inbox row" \
--surfaces inbox:Inbox:960@other --directions 2 --capabilities inbox-search/views --dry-run
asks-how-many-directions - "make me a directions lab for the agent profile card" states neither a count
nor a place, so Q1 and Q2 are asked in one call. No file is named *ProfileCard*; the grep lands on
ui/src/components/AgentIdentity.tsx:3, AgentCapsule.tsx:88 and AgentAvatar.tsx, used from
ui/src/components/IssuesList.tsx, KanbanBoard.tsx, task-chat/TaskChatBubble.tsx,
issue-properties/IssueProperties.tsx, ActiveAgentsPanel.tsx and more, so Q2 offers three of those
places plus "Only one place" and names the rest in the descriptions. No row under
maps/feature-map/agents.md:15-27 describes an identity card, so capabilities are [] with the exception
sentence. Without the question tool, the §4 sentence is written and the lab is 3 directions × 1 surface:
node .agents/skills/directions-lab/scripts/scaffold.mjs new --slug agent-profile-card \
--title "Agent profile card" --surfaces list:Task-list:960@tasks --directions 3 --dry-run
With answers, --directions takes the Q1 value and --surfaces gains one entry per selected place.
8. State every derived value before new
Before the real new, the reply lists, in this order: slug and title; each surface as id:Label:width@nav
with the width's source line; the direction count and names; the group; the capability ids with the row
each came from, or [] and the exception; the landing and cell count; the flag hook and API paths the
Today kit will need; the --dry-run output. Then run new and quote its --json output.
Why this is not optional: this version of the generator has new, check, add-direction and list
only. There is no rename, no add-surface and no stage, so a wrong slug, width, surface list or capability
after new means removing the lab directory, its html, its vite config, the two launch entries, the
.gitignore line and the story by hand before trying again. The story meta id design-<slug>-directions
is permanent once catalogued (design-proposals-storybook.md §4). An unknown capability is refused before
any file is written (scripts/lib/capabilities.mjs); the catalog check, the lab's Vitest files and the Vite
build run after the write and roll everything back on failure. A derived value stated in prose costs Tonio
one line to correct; the same value baked into files costs a rollback.