Client dispatch: In Codex, read
codex-runtimeanddesign/codex.md, execute that native procedure, and do not continue into the Claude procedure below.
Interaction rule: Every question, confirmation, or approval directed at the user — anywhere in this skill, including error recovery — MUST be asked with the
AskUserQuestiontool. Never ask in plain text. If an instruction says "ask the user" or "confirm", that meansAskUserQuestion.
Pencil API reference: Before any Pencil call in this skill, in either
cli-apporeditormode, read thepencil-apireference skill — it is the single source of truth for the current MCP tool surface, theexecuteidiom catalog, the transport table, and document discipline every Pencil call site below relies on.
Phase 0 — Context Loading
Read project-core and resolve neutral configuration before continuing.
Config check: If neither canonical nor legacy configuration exists, stop immediately and tell the user:
"cenci is not configured for this project. Run /cenci:configure first to set up."
Use the config resolved by project-core.
Pencil gating: Check pencil.enabled in the resolved config. If absent or not true, stop immediately and tell the user:
"Pencil design workflows are not enabled for this project. Run /cenci:configure and enable Pencil when prompted."
Read pencil.designPath from the config to determine where design files belong. If the project is a monorepo with pencil.shared: false, determine the per-project designPath from the affected project's entry in the projects array.
Pencil Communication Mode
Read pencil.mode from the resolved config and store as $PENCIL_MODE. Default: "editor" if absent.
Convention: All Pencil tool calls in this skill follow $PENCIL_MODE, per pencil-api's transport table:
"cli-app"(default for new installs): Execute tool calls via apen interactiveheredoc (targeting the desktop app — see Phase 0.5 below for the exact invocation) using the Bash tool. Multiple independent commands can be batched in a single heredoc.pen interactive -a desktop <<'EOF' execute({ input: '<Pencil-script>' }) another_tool({ key: value }) EOFSplit into separate heredoc invocations at decision boundaries — where you need to read output before choosing the next action.
"editor"(legacy MCP fallback): Call the equivalentmcp__pencil__<tool>MCP tool directly (e.g.,mcp__pencil__execute). One tool call per invocation.
Special cases in CLI mode:
| Operation | CLI mode | Editor (MCP) mode |
|---|---|---|
| Screenshots | Use export_nodes({ nodeIds: [...], outputDir: "<path>", format: "png" }) — writes to disk. Then Read the exported PNG with the Read tool. |
Use get_screenshot(nodeId) — returns image inline. |
| Batch reads | Combine multiple execute script reads (see pencil-api's idiom catalog) in one heredoc |
One MCP call per tool |
| Batch writes | Combine multiple execute script writes in one heredoc (when independent) |
One MCP call per tool |
When this skill says "Call <tool_name>(...)" or "run <Pencil-script>", execute it according to $PENCIL_MODE and the execute idiom catalog in pencil-api. Explicit CLI/MCP examples are only given where the modes diverge.
Document-derived values (node IDs, component names — read back out of the .pen document in Step 3B/3C, Step 4A, and Phase 5 Step A) must be validated per pencil-api's idiom catalog before being substituted into any execute script below, in either mode — reject and abort the call site rather than interpolating an unvalidated value. This also covers node IDs interpolated into a filesystem path, such as Step 4A's export_nodes/outputDir and Read targets — see pencil-api's narrower node-ID pattern for those sites.
Phase 0.5 — Pencil Availability Check
Sandbox guard (host-only): /cenci:design is host-only — the Pencil desktop app it drives is never reachable inside the cenci sandbox. In-sandbox sessions only get design access through headless reads via /cenci:implement and verify-ui, never through this skill.
Before doing anything else in this phase — before any Pencil probe, any background auto-launch of Pencil, and any retry, in both cli-app and editor mode below — detect an in-container session with the same two-step check as configure/scripts/detect-project.sh, each as its own Bash call:
test "${CENCI_SANDBOX:-}" = "1"— exit 0 → in container.- If step 1 exited non-zero,
test -f /.dockerenv— exit 0 → in container.
If either check matches, stop immediately and tell the user:
"/cenci:design must run from a host session: the Pencil desktop app is not reachable from inside the cenci sandbox. Exit the container and re-run /cenci:design <args> on the host. Sandboxed sessions get design access through headless reads only (/cenci:implement, verify-ui)."
Stop.
If neither check matches, proceed with the normal availability check below.
Before parsing arguments, verify that Pencil is reachable. Both modes below use the cheap execute({ input: 'Print(1)' }) connectivity probe from pencil-api's idiom catalog — get_app_state is reserved for session orientation (Phase 2 onward), not spent here.
CLI-app mode ($PENCIL_MODE is "cli-app"):
- Probe via Bash:
pen interactive -a desktop <<'EOF' execute({ input: 'Print(1)' }) EOF - If the call succeeds → Pencil is available. Proceed to argument parsing.
- If the call fails → attempt auto-launch:
a. Launch Pencil in the background: run the bare command
penwith the Bash tool's background-execution mode (the equivalent ofpen &). The command string must be exactlypen— never append&to it, or the invocation stops matching the exact-matchBash(pen)grant and prompts. Then retry the probe up to 3 times with 3-second pauses between attempts.- If a retry succeeds → proceed to argument parsing.
- If all 3 retries fail → tell the user:
"Could not reach Pencil. Open the Pencil desktop app manually and ensure it's accepting CLI connections, then re-run
/cenci:design." Stop.
Editor mode ($PENCIL_MODE is "editor"):
- Call
mcp__pencil__executewith{ input: 'Print(1)' }as an MCP connectivity probe. - If the call succeeds → Pencil MCP is available. Proceed to argument parsing.
- If the call fails → attempt auto-launch:
a. Run
which pen 2>/dev/nullto check if thepencommand is available. b. If found: Launch Pencil in the background: run the bare commandpenwith the Bash tool's background-execution mode (the equivalent ofpen &). The command string must be exactlypen— never append&. Then retry theexecute({ input: 'Print(1)' })probe up to 3 times with 3-second pauses between attempts.- If a retry succeeds → proceed to argument parsing.
- If all 3 retries fail → tell the user:
"Could not reach Pencil. Open the Pencil desktop app manually, then re-run
/cenci:design. Check MCP server status in Pencil (View → MCP Server Status) and ensure the Pencil MCP server is listed in your Claude Code MCP configuration." Stop. c. If not found: Tell the user: "The Pencil editor is not running and thepencommand is not in PATH. Either:
- Open Pencil manually and ensure its MCP server is connected, or
- Install the
pencommand (npm install -g @pen.dev/cli, or from within the Pencil app: File → Installpencommand into PATH) for auto-launch support." Stop.
Parse $ARGUMENTS — Mode Detection:
Extract the first whitespace-delimited token from $ARGUMENTS and determine the mode:
If the first token matches
^\d+$or^#\d+$→ ticket mode- Strip any
#prefix to get the numeric ticket ID. - Everything after the first token is optional user context (additional instructions or focus areas).
- Examples:
#1 focus on layout→ ID1, contextfocus on layout;7→ ID7, no context.
- Strip any
Otherwise → ticketless mode
- The entire
$ARGUMENTSstring is the design description. - There is no ticket ID — the design description is the primary input.
- The entire
If ticket mode: Fetch the ticket:
Shell rules: Read the shell-rules skill before running any gh commands (covers heredoc temp-file pattern).
Extract owner/repo from git remote get-url origin (e.g. git@github.com:owner/repo.git → owner/repo), then run:
gh issue view <number> --repo <owner>/<repo> --json number,title,body,labels,state,assignees,milestone,comments
Dedicated-ticket check: if the fetched ticket does not carry the Design label, note that the workflow expects design work on a dedicated design ticket (created by /cenci:refine as a companion or design-first split child). Ask via AskUserQuestion:
"This ticket isn't labeled
Design— the workflow expects design on a dedicated design ticket so the implementation ticket stays separate. Design directly on this ticket anyway?"
- "Stop — create a design ticket first (Recommended)" — stop. Tell the user to re-run
/cenci:refine <ticket-id>(which creates the companion design ticket) or create one manually with theDesignlabel. - "Proceed on this ticket" — continue; the ticket will be labeled
Designedbut not closed (legacy mixed flow).
Read the ticket-ownership reference skill and follow it using the assignees from
the ticket fetch above. Complete its claim-and-verify contract before attachments,
design reasoning, or adding Working. Never replace an existing assignee.
Read the ticket body and look for a Design Direction section (produced by /cenci:refine for frontend tickets). Store it for use in Phase 2.
If ticketless mode: Skip ticket fetching. The design description from $ARGUMENTS is the primary input.
Read any relevant docs/<topic>.md files for entries related to design or this feature area. If a legacy .claude/rules/lessons-learned.md exists in the project, read it as fallback.
Phase 1 — Attachments
If ticketless mode: Skip this section entirely and proceed to Phase 2.
If ticket mode: Read the attachments reference skill and follow its 4-step procedure to discover, present, download, and load ticket attachments. If no attachments are found or the user selects none, proceed to Phase 2.
Phase 2 — Design Understanding
This is the forced reasoning phase. Do not create or modify any .pen files yet.
Label "Working" (at start)
If ticketless mode: Skip this.
If ticket mode: Before starting design work (at the beginning of Phase 2), add the "Working" label. gh issue edit --add-label fails when the label does not exist in the repository, so ensure it exists first — each as its own Bash call (|| true swallows only the "already exists" error):
gh label create "Working" --repo <owner>/<repo> --color "FBCA04" --description "Actively being refined, designed, or implemented" 2>/dev/null || true
gh issue edit <number> --repo <owner>/<repo> --add-label "Working"
Step 2A: Classify Design Type
Based on the ticket description (or design description in ticketless mode), classify what needs designing:
| Type | Examples |
|---|---|
| screen/page | Settings page, profile page, checkout flow |
| component | Date picker, card, notification banner |
| dashboard | Analytics dashboard, admin panel |
| landing-page | Marketing page, product page, hero section |
| form/wizard | Multi-step form, signup wizard, onboarding |
| slides/presentation | Pitch deck, project update, onboarding slides |
Step 2B: Retrieve Pencil Guidelines
Call get_guidelines with the topic most relevant to the classification:
| Design Type | Guideline Topic |
|---|---|
| landing-page | landing-page |
| dashboard, screen/page, form/wizard | design-system |
| component | design-system |
| slides/presentation | slides |
Step 2C: Get Style Inspiration
- Call
get_guidelines({ category: "style" })to list available styles - Select the style that best matches the design task based on classification and context
- Call
get_guidelines({ category: "style", name: "<selected-style>" })to load the full style definition (pass any requiredparamsif the style requests them)
Step 2D: Iterative Propose-First Questioning
Ask questions one at a time using AskUserQuestion. Propose specific answers rather than asking open-ended questions. Limit to 3–5 questions total. Skip any question already answered by the ticket's Design Direction section.
Question 1 — Scope validation:
"Based on [the ticket / your description], I'll design [specific thing] containing [proposed elements]. Does this match your expectations?"
Options: "Yes, that's right", "Adjust scope" (+ description field)
Question 2 — Design system discovery:
- If the user specified a
.penfile path in$ARGUMENTS, skip scanning and use that file directly. - Otherwise, first check the configured
designPathfor existing.penfiles using Glob (<designPath>/*.pen). - If no
.penfiles found indesignPath, fall back to a repo-wide scan: Glob (**/*.pen).
Then:
- If no
.penfiles found → designing from scratch. Mention this to the user. - If exactly one
.penfile found → propose using it: "Found existing design file<path>. Should I use its components as the design system?" - If multiple
.penfiles found → present viaAskUserQuestion:"Found N design files. Which should I use as the design system (or start fresh)?" Options: one per
.penfile path, plus "Start fresh (no design system)"
If using an existing .pen file, verify document identity before reading it — no tool creates or opens documents; opening or switching the active document in Pencil is always a human action (see pencil-api's document discipline section). Call get_app_state and compare its active document path against the target .pen file:
- If it already matches → proceed directly to reading its reusable components with
Get(n=>n.reusable&&Print(n.id,n.name))(perpencil-api's idiom catalog). - If it does not match (including no document open) → ask the user via
AskUserQuestion:"Please open
<target .pen file>in Pencil (File → Open), then confirm here." Options: "Done, file is open", "Cancel"- If "Done" → call
get_app_stateagain to confirm the correct file is now the active document. If it still doesn't match, ask again once more with the same options; if it still mismatches after that second attempt, treat it as "Cancel". Once confirmed, read its reusable components withGet(n=>n.reusable&&Print(n.id,n.name)). - If "Cancel" → skip design system loading and proceed as if designing from scratch.
- If "Done" → call
Question 3 — Visual direction:
If the ticket has a Design Direction section from /cenci:refine, propose using it:
"The ticket specifies this design direction: [summary]. I'll follow this. Any adjustments?"
If no Design Direction exists, propose a direction from the style guide:
"Based on the style guide, I'd suggest [specific aesthetic tone, e.g., 'editorial with high-contrast typography and generous whitespace']. Does this work, or do you have a different direction?"
Options: "Use this direction", "Different direction" (+ description field)
Question 4 — Screen states (conditional — only for screens/pages/forms):
"Which states should I design? I'd suggest [empty, populated, error] at minimum."
Options (multiSelect=true): "Empty state", "Populated / default", "Error state", "Loading state"
Question 5 — Responsive (conditional — only for screens/pages/landing pages):
"Desktop only, or should I also design for mobile/tablet?"
Options: "Desktop only", "Desktop + Mobile", "Desktop + Tablet + Mobile"
Phase 2.5 — Prepare Design Directory
After all design questions are answered, ensure the design directory exists. Design work runs directly on the current branch (expected: main) — no feature branch is created. Pencil keeps the .pen file open across invocations, and branch switching forces the user to re-open it manually in Pencil.
mkdir -p <designPath>
Phase 3 — Design Creation
Now create the design using Pencil tools. All file paths in this phase must be absolute paths within the repository root.
Step 3A: Verify Document Identity
No tool in the current Pencil API creates or opens documents — opening a design-system file or creating a new one is always a human action in the Pencil app (see pencil-api's document discipline section). This step verifies the active document matches what Phase 2/2.5 selected before any read or write in this phase begins.
Determine the target file:
- If a design system
.penfile was selected in Phase 2 → the target is that file's absolute path. - If designing from scratch → the target is a new
.penfile the human will create directly inside<designPath>(see the from-scratch branch below).
If a design system file was selected:
- Call
get_app_stateand compare its active document path against the target. - Match → proceed to Step 3B.
- Mismatch (including no document open) → ask the user via
AskUserQuestion:"Pencil's active document doesn't match
<target file>. Please open it in Pencil (File → Open), then confirm here." Options: "Done, ready to proceed", "Cancel design"- If "Done" → call
get_app_stateagain. If it now matches, proceed to Step 3B. If it still mismatches, ask once more with the same options; if it is still mismatched after that second attempt, Stop and tell the user design cannot continue without the correct document open. Never proceed to Step 3B on a mismatch. - If "Cancel design" → Stop.
- If "Done" → call
If designing from scratch:
- Before prompting, establish the baseline
.penfile set for<designPath>: reuse theGlob(<designPath>/*.pen) result already captured during Phase 2's Question 2 discovery scan — nothing in Phase 2.5 (mkdir -p <designPath>) adds.penfiles, so that result is still an accurate baseline. This baseline matters even when it's non-empty: the from-scratch branch is also reached after the user cancels design-system loading in Phase 2/Step 3A, at which point<designPath>can already contain other, pre-existing.penfiles. If no such Glob result exists yet (the user-specified-path route in Phase 2's Question 2 skipped its discovery scan entirely), runGlob(<designPath>/*.pen) now, before prompting the human to create the new document, so a baseline exists to diff against. - Ask the user via
AskUserQuestion:"Please create and save a new
.penfile inside<designPath>in Pencil, then confirm here." Options: "Done, file created and saved", "Cancel design"- If "Cancel design" → Stop.
- If "Done, file created and saved" → re-run
Glob(<designPath>/*.pen) and diff it against the baseline set captured above to find files present now that were absent from the baseline:- Exactly one new file → this is the target. Call
get_app_stateand confirm the new file is the active document — on mismatch, apply the same re-ask-once-then-stop handling as the design-system-file branch above. Never proceed to Step 3B on a mismatch. - No new file, or more than one new file (ambiguous) → ask the user via
AskUserQuestionto clarify which file is the new document (or confirm it was actually saved, if none appeared). Never guess by picking the first or most-recently-modified match. Once the user identifies a specific file, callget_app_stateand confirm that file is the active document before proceeding to Step 3B — on mismatch, apply the same re-ask-once-then-stop handling as the design-system-file branch above.
- Exactly one new file → this is the target. Call
Important (both modes): no tool in the current Pencil API surface (pencil-api's MCP surface table) accepts a caller-supplied filePath parameter to target a specific document — every call always operates on whatever document is currently open in the app. There is no per-call way to redirect a wayward call to the correct document; identity is established purely by what's open when the call runs, which is why it must be verified with get_app_state before every read/write batch rather than assumed from the check above (see pencil-api's Document Discipline section).
Step 3B: Inventory the Document
Call get_app_state({ include_schema: true }) (per pencil-api's MCP surface) to load the full .pen schema and execute function docs needed for component discovery and later building, alongside session orientation. This call runs unconditionally here, in both the design-system and from-scratch branches — Step 3D's build step needs the schema and execute docs either way, not just when a design system file was selected.
Then run the top-level inventory idiom from pencil-api's catalog to understand the document's existing structure before building:
Get((n,c)=>{c.skipChildren();Print(n.id,n.name)})
Step 3C: Load Design System Components
If a design system file was selected:
- Run the reusable discovery idiom,
Get(n=>n.reusable&&Print(n.id,n.name)), to discover all reusable components. For any component whose full subtree is needed, follow up with the subtree-read idiom,Print(Get("<id>",{depth:3})). - Catalog available components (buttons, inputs, cards, navigation, etc.) for use in the design
Step 3D: Build the Design
Use execute calls to create the design. Follow these rules:
- Split larger designs into multiple
executecalls by logical section (e.g., header first, then content area, then footer) - Use reusable components from the design system where available — insert with the ref-insert idiom,
Insert("<parent>",{type:"ref",ref:"<Comp>",width:"fill_container"}) - For new elements not in the design system, create frames and text nodes directly
- Apply styling from the style guide and Design Direction
- Run
FindEmptySpacewhen positioning new screens to avoid overlapping existing content - Generate images with
Generate(nodeId,"ai"|"stock",…)where needed (hero images, avatars, illustrations) - Set theme variables via
SetVariables({...})if creating a new design system or extending an existing one — always a merge, never passreplace: true, which would destroy the existing theming instead of extending it - Use absolute positioning within flex layouts for floating elements (FABs, modals, overlays, tooltips)
Build order:
- Create the screen/page frame with overall layout
- Add structural sections (header, sidebar, content area, footer)
- Populate each section with components and content
- Apply typography, colors, spacing, and other styling
- Add images and decorative elements
- Create additional screen states if requested (empty, error, loading)
Step 3E: Responsive Variants
If the user requested responsive designs:
- Find empty space on the canvas to the right of the desktop design
- Create mobile (375px wide) and/or tablet (768px wide) variants
- Adapt the layout for each breakpoint (stack columns, resize elements, hide secondary content)
Phase 4 — Visual Validation Loop
Step 4A: Screenshot and Inspect
Re-verify document identity before this batch: multiple AskUserQuestion round-trips happen between Step 3A and here, during which the human could switch Pencil's active document. Call get_app_state and compare its active document path against the target file established in Step 3A. Apply the same re-ask-once-then-Stop handling as Step 3A — never proceed to a screenshot or read/write below on a still-mismatched document.
For each screen/component created:
- Capture a visual snapshot:
CLI mode: Call
export_nodesto save screenshots to a scratch directory outside the repo, then Read the exported PNG. These are local validation artifacts only — never committed.Create the screenshot directory once, on first entry into this step, as its own standalone Bash call — never a shell-variable assignment that captures the command's own output, and never any other form of command substitution:
shell-rulesforbids command substitution where the agent can run the command and read the result, and an assignment wrapper would not match the grantedBash(mktemp -d:*)prefix.mktemp -d "${TMPDIR:-/tmp}/cenci-design-XXXXXX"Verify the printed path with its own standalone Bash call before using it:
test -d "<printed-path>"If this verification fails, stop the step immediately using the same error-recovery convention as Phase 0/0.5 — do not proceed to
export_nodeswith an unverified directory.<node-id>below is a document-derived value — validate it againstpencil-api's narrower node-ID allowlist pattern before interpolating it, since it lands in a filesystem path (outputDir, theReadtarget) as well as theexecutescript; reject and abort rather than interpolating an unvalidated ID.Substitute the printed literal path (never an unsubstituted shell-variable reference — a quoted
<<'EOF'heredoc never expands shell variables, so leaving one in place would reachexport_nodesas literal, wrong text instead of the resolved path) into the heredoc'soutputDir, the subsequentRead(...)call, and Step 4B's re-screenshot loop — reuse the same printed path for every screenshot taken during this design session, in both this step and Step 4B:pen interactive -a desktop <<'EOF' export_nodes({ nodeIds: ["<node-id>"], outputDir: "<screenshot-dir>", format: "png" }) execute({ input: 'Get("<node-id>",(n,c)=>c.problems&&Print(n.name,c.problems))' }) EOFThen:
Read("<screenshot-dir>/<node-id>.png")to view and analyze.Editor mode: Call
get_screenshot(nodeId)to receive the image inline.
- Analyze the screenshot for:
- Alignment issues (elements not lined up properly)
- Readability problems (text too small, low contrast)
- Visual hierarchy (clear headings, proper spacing, content grouping)
- Completeness (all specified elements present)
- Clipping (content cut off or overflowing)
- Review the layout-check
Get(...,(n,c)=>c.problems&&...)output (captured alongside the screenshot) for programmatic layout problems - Fix any issues found via additional
executecalls - Re-screenshot after fixes to confirm they resolved the problems
Step 4B: Present to User
After validation passes, present the design to the user via AskUserQuestion:
"Here's the design for [description]. I've verified alignment, readability, and completeness. What do you think?"
Options:
- "Approve" — proceed to Phase 5
- "Request Changes" — describe what to change
- "Start Over" — redesign from scratch
If "Request Changes":
- Ask what needs changing (via
AskUserQuestionif the user didn't specify inline) - Re-verify document identity before this batch: the Approve/Request-Changes prompt is another
AskUserQuestionround-trip during which the human could switch Pencil's active document. Callget_app_stateand compare its active document path against the target file established in Step 3A. Apply the same re-ask-once-then-Stop handling as Step 3A — never proceed to theexecutecalls below on a still-mismatched document. - Apply the requested changes via additional
executecalls - Re-screenshot and re-validate (loop back to Step 4A)
- Re-present the updated design (back to Step 4B)
If "Start Over":
- Re-verify document identity before this batch: the Approve/Request-Changes prompt is another
AskUserQuestionround-trip during which the human could switch Pencil's active document. Callget_app_stateand compare its active document path against the target file established in Step 3A. Apply the same re-ask-once-then-Stop handling as Step 3A — never delete anything from the canvas on a still-mismatched document. - Delete the current design from the canvas
- Loop back to Phase 2, Question 3 (visual direction) to take a new direction
- Rebuild from Phase 3
Only proceed to Phase 5 after the user selects "Approve".
Phase 5 — Generate DESIGN.md
After the user approves the design in Phase 4, generate a DESIGN.md spec that documents the design for implementation.
Step A: Extract data from .pen file
Re-verify document identity before this batch: the Phase 4 approval round-trip happens between Step 3A/4A and here, during which the human could switch Pencil's active document. Call get_app_state and compare its active document path against the target file established in Step 3A. Apply the same re-ask-once-then-Stop handling as Step 3A — never proceed to the reads below on a still-mismatched document.
In CLI mode, batch all reads into a single invocation, using the execute idioms from pencil-api's catalog:
pen interactive -a desktop <<'EOF'
execute({ input: 'Get((n,c)=>{c.skipChildren();/^Screen\\//.test(n.name)&&Print(n.id,n.name)})' })
execute({ input: 'Get(n=>n.reusable&&Print(n.id,n.name))' })
execute({ input: 'Get((n,c)=>{c.skipChildren();/^Note:/.test(n.name)&&Print(n.id,n.name)})' })
execute({ input: 'Print(GetVariables())' })
EOF
In editor mode, call mcp__pencil__execute once per script above via MCP — but not with the same literal string. The \\/ in /^Screen\\// above is escaped for the CLI heredoc form's own string-literal-parsing layer inside pen interactive; passed through MCP there is no such extra layer, so the \\ would survive as a literal backslash and break the regex. In editor mode, use the un-escaped form instead: mcp__pencil__execute with { input: 'Get((n,c)=>{c.skipChildren();/^Screen\//.test(n.name)&&Print(n.id,n.name)})' }.
Parse the output into:
- Screens: From the
Screen/.*results — extract name, node ID. Derive route from the screen name (e.g.,Screen/training-plan→/training-plan). Add a brief description based on the screen content. - Components: From the reusable-node results — extract name, node ID. Derive the framework component name from the Pencil component name (e.g.,
Component/ExerciseCard→ExerciseCardComponentfor Angular,ExerciseCardfor React). Determine UI library usage (e.g., PrimeNG, Material UI, custom) from component structure. Note which screens use each component. - Annotations: From the
Note:.*results — extract name, node ID, and topic from the note content. - Tokens: From
GetVariables()— categorize variables into Colors, Typography, Radii, and Spacing. Map each to a CSS custom property name (e.g.,$bg-card→--bg-card).
Step B: Detect framework from config
Read stack.frontend (or per-project stack) from the resolved config to determine:
- Column headers for the Components table (Angular, React, Vue, or Generic)
- Component naming conventions (e.g.,
<Name>Componentfor Angular,<Name>for React)
Step C: Write DESIGN.md
Use the template at ${CLAUDE_PLUGIN_ROOT}/templates/design-spec.md as the base. Fill all parameterized sections with extracted data:
- Replace
<design-path>with the configureddesignPath - Replace
<pen-file-name>with the actual.penfile name - Replace
<framework>and select the matching Components table variant - Populate Screens, Components, Annotations, and Design Tokens tables with extracted data
- Write the completed file to
<designPath>/DESIGN.md
If DESIGN.md already exists at that path, ask the user via AskUserQuestion:
"A DESIGN.md already exists at
<designPath>/DESIGN.md. What should I do?"
Options: "Overwrite with new spec", "Merge (add new entries, keep existing)"
- Overwrite: Replace the file entirely.
- Merge: Read the existing file, add new screens/components/tokens that don't already exist, preserve existing entries.
Phase 6 — Report Summary
Report
Summarize what was created:
.penfile path(s) created or modified- Screens/components designed (list each with a brief description)
- Key design decisions (aesthetic tone, color palette, typography, layout approach)
- Design system components used (if any)
DESIGN.mdpath, screen count, component count, token count
Include this note at the end of the report:
"Note: Pencil does not auto-save. Save the
.penfile manually in Pencil (Cmd/Ctrl+S) before closing — unsavedexecutechanges will be lost. The design file remains open for your review."
Phase 7 — Commit Design
After Phase 6 reporting is complete, commit the design artifacts on the current branch. No branch switch, no push, no PR.
Step 7.0: Manual Save Reminder (REQUIRED)
Pencil does NOT auto-save .pen files. Changes made via execute exist only in the open editor until the user manually saves. git add reads from disk, so committing without saving captures a stale .pen file.
Before proceeding, prompt the user via AskUserQuestion:
"Pencil does not auto-save. Please save the
.penfile in Pencil now (File → Save or Cmd/Ctrl+S), then confirm."
Options: "Saved, proceed", "Cancel commit"
- "Saved, proceed" → re-verify document identity before continuing: call
get_app_stateand compare its active document path against the target file established in Step 3A. Apply the same re-ask-once-then-Stop handling as Step 3A — never proceed to Step 7A'sgit addon a still-mismatched document. Once confirmed, continue to Step 7A. - "Cancel commit" → Stop. Do not commit.
Step 7A: Commit
Stage and commit the design artifacts on the current branch. Do not stage screenshots — the .pen file is the source of truth and any screenshots/ directory inside <designPath> is local-only scratch.
Stage and commit as two standalone Bash calls — never a && compound (per shell-rules, Claude Code evaluates every segment of a compound, so a compound can prompt even when both halves are granted).
git add <designPath>/*.pen <designPath>/DESIGN.md
Verify both files were actually staged before committing, using only the client's file tools (e.g. Glob or Read — no additional Bash grant needed): confirm the .pen file exists at <designPath> so the git add glob above is known to have matched it, rather than silently resolving to nothing. If it's missing, stop and apply the same recovery as Step 7D's "Commit fails" case rather than committing an incomplete change.
git commit -m "feat(design): <description>" -- <designPath>/*.pen <designPath>/DESIGN.md
- If ticket mode: Include ticket ref in the commit body:
#<ticket-id> - If ticketless mode: Use the design description slug in the commit message
Step 7B: Post Design Comment
If ticketless mode: Skip this step.
If ticket mode: Post the design reference and key decisions as a ticket comment — this keeps the ticket body owned by its author while still surfacing the design context for humans and for the context-gatherer (which bundles ticket comments during planning).
- Get the commit SHA:
git rev-parse HEAD - Write the comment body with the client's file tool (per
shell-rules— do not use a shell heredoc) to a temp file, e.g.${TMPDIR:-/tmp}/cenci/design-comment-<number>.md. Every posted design comment opens with the cenci attribution banner (blockquoted) and carries the<!-- cenci-design-summary -->marker on its own non-blockquoted line (#951 — seedocs/comment-attribution.md). Include a### Screen nodessubsection listing each screen extracted in Phase 5 Step A by name and node ID; omit the subsection entirely when there are noScreen/*nodes (e.g. a component-only or from-scratch design with nothing to list). The same document-derived-value validation rule frompencil-api'sexecuteIdiom Catalog section also applies here — validate each screen name and node ID against the allowlist (the name pattern for screen names, the narrower node-ID pattern for node IDs) before including it in this posted comment; reject rather than post an unvalidated value:> 🤖 **cenci** — design summary posted by `/cenci:design` (design handoff). ### Design Reference - Design file: `<designPath>/<pen-file-name>` - Design spec: `<designPath>/DESIGN.md` - Commit: `<commit-sha>` ### Design Decisions - Aesthetic tone: <from Phase 6 report> - Color palette: <from Phase 6 report> - Typography: <from Phase 6 report> - Layout approach: <from Phase 6 report> ### Screen nodes - `<screen-name>` — `<node-id>` - `<screen-name>` — `<node-id>` <!-- cenci-design-summary --> - Post it:
gh issue comment <number> --repo <owner>/<repo> --body-file "${TMPDIR:-/tmp}/cenci/design-comment-<number>.md"
Step 7C: Label Ticket
If ticketless mode: Skip labeling.
If ticket mode: Replace "Working" with "Designed". Ensure the label exists first (same self-heal pattern as Working — gh issue edit --add-label fails on a missing label):
gh label create "Designed" --repo <owner>/<repo> --color "5319E7" --description "Design spec approved" 2>/dev/null || true
gh issue edit <number> --repo <owner>/<repo> --add-label "Designed" --remove-label "Working"
If the ticket carries the "Design" label (a design-only ticket from /cenci:refine — the design spec is the deliverable, no implementation follows), additionally:
Find the implementation tickets this design blocks. Union of:
- The design ticket's own native Blocking set — the authoritative source, the exact inverse of the
--add-blocked-bylink/cenci:refineapplies (requiresgh>= 2.94.0):gh issue view <number> --repo <owner>/<repo> --json blocking --jq '.blocking.nodes[].number' - Any
Blocks #<n>lines in the design ticket's body - Open issues whose body carries the supplementary prose dependency line (
Depends on #<number>— a permanent, human-visible line/cenci:refinewrites alongside the native link on every refined ticket that actually has a dependency, i.e. a blocking sibling or a companion design ticket; never a transitional form):gh issue list --repo <owner>/<repo> --state open --search "\"Depends on #<number>\" in:body" --json number,title
Deduplicate the three lists.
There is no
blocked-by:search qualifier, so the native lookup is a direct read of this ticket's ownblockingfield rather than a repo-wide search — which is both cheaper and exact, since it cannot miss a dependent whose body never mentioned the design ticket.- The design ticket's own native Blocking set — the authoritative source, the exact inverse of the
Propagate
Designedto each dependent. This is what satisfies implement's Design gate — the gate checks the label on the ticket being implemented, not the ticket body:gh issue edit <dependent> --repo <owner>/<repo> --add-label "Designed"Also post the same comment from Step 7B on each depe
…(truncated)