Files
paperclip/.agents/skills/directions-lab/references/questions-and-defaults.md
T
TonioandClaude Opus 5.5 dfedd73f56 feat(skills): add the directions-lab skill and its lab generator (#17437)
## 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]>
2026-10-26 12:26:44 -07:00

16 KiB
Raw Blame History

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

  1. Extract from the brief
  2. Ground the subject in the code
  3. The one question call
  4. When no question can be asked
  5. What is never asked
  6. Flag derivation table
  7. The three eval briefs as flag lines
  8. 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/components or only under ui/src/pages; --group follows from that (§6).
  • Flag hook. Every experimental flag is read through a use<Feature>Enabled() hook in ui/src/hooks/ that queries queryKeys.instance.experimentalSettings (ui/src/lib/queryKeys.ts:614) through instanceSettingsApi.getExperimental(), for example ui/src/hooks/useStreamlinedUiEnabled.ts:27-32. The generated api-routes.ts answers that one route from registry.today.flags, so the hook name and the flag key are all you need to write down.
  • API routes. Follow each useQuery/queryFn into ui/src/api/<module>.ts. Every path string there is appended to BASE = "/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-10 takes the area from the file stem of each maps/feature-map/<area>.md; lines 24-31 read every row of the form - `key`: ... under ## Sub-features and build the id <area>/<key>; line 34 enforces /^[a-z0-9-]+\/[a-z0-9-]+$/. --capabilities is validated against that same featureIndex(root), so an id you type must exist as a row, not merely read well. Open the area file's ## Sub-features block 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: Directions
  • question: "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: 2 with description "Today + 1.Refine + 2.Rethink"; 3 with description "Today + 1.Refine + 2.Rework + 3.Rethink".

Q2 (skipped when the brief names the places)

  • header: Surfaces
  • question: "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.