/** * Prompt composer. The base is the OD-adapted "expert designer" system * prompt (see ./official-system.ts) — a full identity, workflow, and * content-philosophy charter. Stacked on top: * * 1. The discovery + planning + huashu-philosophy layer (./discovery.ts) * — interactive question-form syntax, direction-picker fork, * brand-spec extraction, TodoWrite reinforcement, 5-dim critique, * and the embedded `directions.ts` library. * 2. The active design system's DESIGN.md (if any) — palette, typography, * spacing rules treated as authoritative tokens. * 3. The active skill's SKILL.md (if any) — workflow specific to the * kind of artifact being built. When the skill ships a seed * (`assets/template.html`) and references (`references/layouts.md`, * `references/checklist.md`), we inject a hard pre-flight rule above * the skill body so the agent reads them BEFORE writing any code. * 4. For decks (skillMode === 'deck' OR metadata.kind === 'deck'), the * deck framework directive (./deck-framework.ts) is pinned LAST so it * overrides any softer slide-handling wording earlier in the stack — * this is the load-bearing nav / counter / scroll JS / print * stylesheet contract that PDF stitching depends on. We also fire on * the metadata path so deck-kind projects without a bound skill * (skill_id null) still get a framework, instead of having the agent * re-author scaling / nav / print logic from scratch each turn. When * the active skill ships its own seed (skill body references * `assets/template.html`), we defer to that seed and skip the generic * skeleton — the skill's framework wins to avoid double-injection. * * The composed string is what the daemon sees as `systemPrompt` and what * the Anthropic path sends as `system`. */ import type { ProjectMetadata, ProjectTemplate } from '../types'; import { OFFICIAL_DESIGNER_PROMPT } from './official-system'; import { DISCOVERY_AND_PHILOSOPHY } from './discovery'; import { DECK_FRAMEWORK_DIRECTIVE } from './deck-framework'; export const BASE_SYSTEM_PROMPT = OFFICIAL_DESIGNER_PROMPT; export interface ComposeInput { skillBody?: string | undefined; skillName?: string | undefined; skillMode?: 'prototype' | 'deck' | 'template' | 'design-system' | undefined; designSystemBody?: string | undefined; designSystemTitle?: string | undefined; // Project-level metadata captured by the new-project panel. Drives the // agent's understanding of artifact kind, fidelity, speaker-notes intent // and animation intent. Missing fields here are exactly what the // discovery form should re-ask the user about on turn 1. metadata?: ProjectMetadata | undefined; // The template the user picked in the From-template tab, when present. // Snapshot of HTML files that the agent should treat as a starting // reference rather than a fixed deliverable. template?: ProjectTemplate | undefined; } export function composeSystemPrompt({ skillBody, skillName, skillMode, designSystemBody, designSystemTitle, metadata, template, }: ComposeInput): string { // Discovery + philosophy goes FIRST so its hard rules ("emit a form on // turn 1", "branch on brand on turn 2", "TodoWrite on turn 3", run // checklist + critique before ) win precedence over softer // wording later in the official base prompt. const parts: string[] = [ DISCOVERY_AND_PHILOSOPHY, '\n\n---\n\n# Identity and workflow charter (background)\n\n', BASE_SYSTEM_PROMPT, ]; if (designSystemBody && designSystemBody.trim().length > 0) { parts.push( `\n\n## Active design system${designSystemTitle ? ` — ${designSystemTitle}` : ''}\n\nTreat the following DESIGN.md as authoritative for color, typography, spacing, and component rules. Do not invent tokens outside this palette. When you copy the active skill's seed template, bind these tokens into its \`:root\` block before generating any layout.\n\n${designSystemBody.trim()}`, ); } if (skillBody && skillBody.trim().length > 0) { const preflight = derivePreflight(skillBody); parts.push( `\n\n## Active skill${skillName ? ` — ${skillName}` : ''}\n\nFollow this skill's workflow exactly.${preflight}\n\n${skillBody.trim()}`, ); } const metaBlock = renderMetadataBlock(metadata, template); if (metaBlock) parts.push(metaBlock); // Decks have a load-bearing framework (nav, counter, scroll JS, print // stylesheet for PDF stitching). Pin it last so it overrides any softer // wording earlier in the stack ("write a script that handles arrows…"). // // We fire on either (a) the active skill is a deck skill OR (b) the // project metadata declares kind=deck. Case (b) catches projects created // without a skill (skill_id null) — without this, a deck-kind project // with no bound skill gets neither a skill seed nor the framework // skeleton, and the agent writes scaling / nav / print logic from scratch // with the same buggy `place-items: center` + transform pattern we keep // having to fix at runtime. Skill seeds (when present) win — they // already define their own opinionated framework (simple-deck's // scroll-snap, guizang-ppt's magazine layout) and re-pinning the generic // skeleton would conflict. The skill-seed path takes over via // `derivePreflight` above, so we only fire the generic skeleton when no // skill seed is on offer. const isDeckProject = skillMode === 'deck' || metadata?.kind === 'deck'; const hasSkillSeed = !!skillBody && /assets\/template\.html/.test(skillBody); if (isDeckProject && !hasSkillSeed) { parts.push(`\n\n---\n\n${DECK_FRAMEWORK_DIRECTIVE}`); } return parts.join(''); } function renderMetadataBlock( metadata: ProjectMetadata | undefined, template: ProjectTemplate | undefined, ): string { if (!metadata) return ''; const lines: string[] = []; lines.push('\n\n## Project metadata'); lines.push( 'These are the structured choices the user made (or skipped) when creating this project. Treat known fields as authoritative; for any field marked "(unknown — ask)" you MUST include a matching question in your turn-1 discovery form.', ); lines.push(''); lines.push(`- **kind**: ${metadata.kind}`); if (metadata.kind === 'prototype') { lines.push( `- **fidelity**: ${metadata.fidelity ?? '(unknown — ask: wireframe vs high-fidelity)'}`, ); } if (metadata.kind === 'deck') { lines.push( `- **speakerNotes**: ${typeof metadata.speakerNotes === 'boolean' ? metadata.speakerNotes : '(unknown — ask: include speaker notes?)'}`, ); } if (metadata.kind === 'template') { lines.push( `- **animations**: ${typeof metadata.animations === 'boolean' ? metadata.animations : '(unknown — ask: include motion/animations?)'}`, ); if (metadata.templateLabel) { lines.push(`- **template**: ${metadata.templateLabel}`); } } if (metadata.inspirationDesignSystemIds && metadata.inspirationDesignSystemIds.length > 0) { lines.push( `- **inspirationDesignSystemIds**: ${metadata.inspirationDesignSystemIds.join(', ')} — the user picked these systems as *additional* inspiration alongside the primary one. Borrow palette accents, typographic personality, or component patterns from them; don't replace the primary system's tokens.`, ); } if (metadata.kind === 'template' && template && template.files.length > 0) { lines.push(''); lines.push( `### Template reference — "${template.name}"${template.description ? ` (${template.description})` : ''}`, ); lines.push( 'These HTML snapshots are what the user wants to start FROM. Read them as a stylistic + structural reference. You may copy structure, palette, typography, and component patterns; you may adapt them to the new brief; do NOT ship them verbatim. The agent should still produce its own artifact, just one that visibly inherits this template\'s design language.', ); for (const f of template.files) { // Cap each file at ~12k chars so a giant template doesn't blow out // the system prompt budget. The agent gets enough to read structure. const truncated = f.content.length > 12000 ? `${f.content.slice(0, 12000)}\n` : f.content; lines.push(''); lines.push(`#### \`${f.name}\``); lines.push('```html'); lines.push(truncated); lines.push('```'); } } return lines.join('\n'); } /** * Detect the seed/references pattern shipped by the upgraded * web-prototype / mobile-app / simple-deck / guizang-ppt skills, and * inject a hard pre-flight rule that lists which side files to Read * before doing anything else. The skill body's own workflow already says * this — but skills get truncated under context pressure and the agent * sometimes skips Step 0. A short up-front directive helps. * * Returns an empty string when the skill ships no side files (legacy * SKILL.md-only skills) so we don't add noise. */ function derivePreflight(skillBody: string): string { const refs: string[] = []; if (/assets\/template\.html/.test(skillBody)) refs.push('`assets/template.html`'); if (/references\/layouts\.md/.test(skillBody)) refs.push('`references/layouts.md`'); if (/references\/themes\.md/.test(skillBody)) refs.push('`references/themes.md`'); if (/references\/components\.md/.test(skillBody)) refs.push('`references/components.md`'); if (/references\/checklist\.md/.test(skillBody)) refs.push('`references/checklist.md`'); if (refs.length === 0) return ''; return ` **Pre-flight (do this before any other tool):** Read ${refs.join(', ')} via the path written in the skill-root preamble. The seed template defines the class system you'll paste into; the layouts file is the only acceptable source of section/screen/slide skeletons; the checklist is your P0/P1/P2 gate before emitting \`\`. Skipping this step is the #1 reason output regresses to generic AI-slop.`; }