From c9258c97e788d4e762b2fd7faae870edab1f124c Mon Sep 17 00:00:00 2001 From: Jinzhe Date: Sat, 4 Jul 2026 22:19:58 +0800 Subject: [PATCH] Fix Codex design-shotgun image generation --- design-shotgun/SKILL.md.tmpl | 74 +---------- scripts/resolvers/design.ts | 229 ++++++++++++++++++++++++++++++++++- scripts/resolvers/index.ts | 3 +- test/gen-skill-docs.test.ts | 13 ++ 4 files changed, 243 insertions(+), 76 deletions(-) diff --git a/design-shotgun/SKILL.md.tmpl b/design-shotgun/SKILL.md.tmpl index 230dbc292..4d8f0bb2f 100644 --- a/design-shotgun/SKILL.md.tmpl +++ b/design-shotgun/SKILL.md.tmpl @@ -223,79 +223,7 @@ If B: incorporate feedback, re-present concepts, re-confirm. Max 2 rounds. If C: add concepts, re-present, re-confirm. If D: drop specified concepts, re-present, re-confirm. -### Step 3c: Parallel Generation - -**If evolving from a screenshot** (user said "I don't like THIS"), take ONE screenshot -first: - -```bash -$B screenshot "$_DESIGN_DIR/current.png" -``` - -**Launch N Agent subagents in a single message** (parallel execution). Use the Agent -tool with `subagent_type: "general-purpose"` for each variant. Each agent is independent -and handles its own generation, quality check, verification, and retry. - -**Important: $D path propagation.** The `$D` variable from DESIGN SETUP is a shell -variable that agents do NOT inherit. Substitute the resolved absolute path (from the -`DESIGN_READY: /path/to/design` output in Step 0) into each agent prompt. - -**Agent prompt template** (one per variant, substitute all `{...}` values): - -``` -Generate a design variant and save it. - -Design binary: {absolute path to $D binary} -Brief: {the full variant-specific brief for this direction} -Output: /tmp/variant-{letter}.png -Final location: {_DESIGN_DIR absolute path}/variant-{letter}.png - -Steps: -1. Run: {$D path} generate --brief "{brief}" --output /tmp/variant-{letter}.png -2. If the command fails with a rate limit error (429 or "rate limit"), wait 5 seconds - and retry. Up to 3 retries. -3. If the output file is missing or empty after the command succeeds, retry once. -4. Copy: cp /tmp/variant-{letter}.png {_DESIGN_DIR}/variant-{letter}.png -5. Quality check: {$D path} check --image {_DESIGN_DIR}/variant-{letter}.png --brief "{brief}" - If quality check fails, retry generation once. -6. Verify: ls -lh {_DESIGN_DIR}/variant-{letter}.png -7. Report exactly one of: - VARIANT_{letter}_DONE: {file size} - VARIANT_{letter}_FAILED: {error description} - VARIANT_{letter}_RATE_LIMITED: exhausted retries -``` - -For the evolve path, replace step 1 with: -``` -{$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output /tmp/variant-{letter}.png -``` - -**Why /tmp/ then cp?** In observed sessions, `$D generate --output ~/.gstack/...` -failed with "The operation was aborted" while `--output /tmp/...` succeeded. This is -a sandbox restriction. Always generate to `/tmp/` first, then `cp`. - -### Step 3d: Results - -After all agents complete: - -1. Read each generated PNG inline (Read tool) so the user sees all variants at once. -2. Report status: "All {N} variants generated in ~{actual time}. {successes} succeeded, - {failures} failed." -3. For any failures: report explicitly with the error. Do NOT silently skip. -4. If zero variants succeeded: fall back to sequential generation (one at a time with - `$D generate`, showing each as it lands). Tell the user: "Parallel generation failed - (likely rate limiting). Falling back to sequential..." -5. Proceed to Step 4 (comparison board). - -**Dynamic image list for comparison board:** When proceeding to Step 4, construct the -image list from whatever variant files actually exist, not a hardcoded A/B/C list: - -```bash -setopt +o nomatch 2>/dev/null || true # zsh compat -_IMAGES=$(ls "$_DESIGN_DIR"/variant-*.png 2>/dev/null | tr '\n' ',' | sed 's/,$//') -``` - -Use `$_IMAGES` in the `$D compare --images` command. +{{DESIGN_SHOTGUN_GENERATION}} ## Step 4: Comparison Board + Feedback Loop diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index 9f31b3619..f4cd2208d 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -786,6 +786,24 @@ Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developer } export function generateDesignSetup(ctx: TemplateContext): string { + if (ctx.host === 'codex' && ctx.skillName === 'design-shotgun') { + return `## CODEX IMAGE SETUP (run this check BEFORE generating design variants) + +This skill is running as \`gstack-design-shotgun\` in Codex. Use Codex's native +\`image_gen\` tool for visual mockups. + +Do NOT run the gstack design binary for this skill: +- no \`$D generate\` +- no \`$D variants\` +- no \`$D evolve\` +- no OpenAI API key setup +- no \`OPENAI_API_KEY\` lookup + +Design artifacts still belong in \`~/.gstack/projects/$SLUG/designs/\`. The +\`image_gen\` tool may write images elsewhere first; copy the returned image files +into the design session directory before building the comparison board.`; + } + return `## DESIGN SETUP (run this check BEFORE any design mockup command) \`\`\`bash @@ -909,7 +927,215 @@ echo '{"approved_variant":"","feedback":"","date":"'$(date -u Reference the saved mockup in the design doc or plan.`; } -export function generateDesignShotgunLoop(_ctx: TemplateContext): string { +export function generateDesignShotgunGeneration(ctx: TemplateContext): string { + if (ctx.host === 'codex') { + return `### Step 3c: Codex Native Image Generation + +**Codex host rule:** This skill is running as \`gstack-design-shotgun\`. Use the +host-native \`image_gen\` tool directly. Do NOT run \`$D generate\`, \`$D variants\`, +\`$D evolve\`, or any command that requires \`OPENAI_API_KEY\`. + +**If evolving from a screenshot** (user said "I don't like THIS"), take ONE screenshot +first: + +\`\`\`bash +$B screenshot "$_DESIGN_DIR/current.png" +\`\`\` + +Use that screenshot as visual context in the \`image_gen\` prompt when the tool +supports image editing or image reference input. If the tool only accepts text, +describe the screenshot's visible layout, palette, hierarchy, and the requested +change before generating. + +**Generate one image per variant.** For each confirmed direction, call the +\`image_gen\` tool with a complete prompt that includes: + +- the product/screen brief +- the variant-specific direction +- audience and job-to-be-done +- visible states and edge cases that must appear +- design-system constraints from DESIGN.md or the taste profile +- hard output requirement: a polished UI mockup screenshot, no logos, no tiny + unreadable body text, no decorative filler + +Run these calls in parallel if the host supports parallel tool calls. Otherwise run +them sequentially and show each result as it lands. + +**Save returned images into the design session directory.** The \`image_gen\` tool +returns or writes image files under the Codex generated-images directory. Copy each +returned image path to the canonical design session path: + +\`\`\`bash +cp "" "$_DESIGN_DIR/variant-A.png" +cp "" "$_DESIGN_DIR/variant-B.png" +cp "" "$_DESIGN_DIR/variant-C.png" +ls -lh "$_DESIGN_DIR"/variant-*.png +\`\`\` + +If the user requested a different count, save the matching letters only. If a copy +fails, report that variant as failed and keep the successful variants. + +### Step 3d: Results + +After generation completes: + +1. Show every generated image inline so the user sees all variants at once. +2. Report status: "All {N} variants generated. {successes} succeeded, {failures} failed." +3. For any failures: report explicitly with the error. Do NOT silently skip. +4. If zero variants succeeded: stop and say \`image_gen\` did not return usable images. + Do NOT fall back to \`$D generate\` on Codex. +5. Proceed to Step 4 (comparison board or inline feedback fallback). + +**Dynamic image list for comparison board:** When proceeding to Step 4, construct the +image list from whatever variant files actually exist, not a hardcoded A/B/C list: + +\`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat +_IMAGES=$(ls "$_DESIGN_DIR"/variant-*.png 2>/dev/null | tr '\\n' ',' | sed 's/,$//') +echo "IMAGES: $_IMAGES" +\`\`\` + +Use \`$_IMAGES\` for any comparison-board generation.`; + } + + return `### Step 3c: Parallel Generation + +**If evolving from a screenshot** (user said "I don't like THIS"), take ONE screenshot +first: + +\`\`\`bash +$B screenshot "$_DESIGN_DIR/current.png" +\`\`\` + +**Launch N Agent subagents in a single message** (parallel execution). Use the Agent +tool with \`subagent_type: "general-purpose"\` for each variant. Each agent is independent +and handles its own generation, quality check, verification, and retry. + +**Important: $D path propagation.** The \`$D\` variable from DESIGN SETUP is a shell +variable that agents do NOT inherit. Substitute the resolved absolute path (from the +\`DESIGN_READY: /path/to/design\` output in Step 0) into each agent prompt. + +**Agent prompt template** (one per variant, substitute all \`{...}\` values): + +\`\`\` +Generate a design variant and save it. + +Design binary: {absolute path to $D binary} +Brief: {the full variant-specific brief for this direction} +Output: /tmp/variant-{letter}.png +Final location: {_DESIGN_DIR absolute path}/variant-{letter}.png + +Steps: +1. Run: {$D path} generate --brief "{brief}" --output /tmp/variant-{letter}.png +2. If the command fails with a rate limit error (429 or "rate limit"), wait 5 seconds + and retry. Up to 3 retries. +3. If the output file is missing or empty after the command succeeds, retry once. +4. Copy: cp /tmp/variant-{letter}.png {_DESIGN_DIR}/variant-{letter}.png +5. Quality check: {$D path} check --image {_DESIGN_DIR}/variant-{letter}.png --brief "{brief}" + If quality check fails, retry generation once. +6. Verify: ls -lh {_DESIGN_DIR}/variant-{letter}.png +7. Report exactly one of: + VARIANT_{letter}_DONE: {file size} + VARIANT_{letter}_FAILED: {error description} + VARIANT_{letter}_RATE_LIMITED: exhausted retries +\`\`\` + +For the evolve path, replace step 1 with: +\`\`\` +{$D path} evolve --screenshot {_DESIGN_DIR}/current.png --brief "{brief}" --output /tmp/variant-{letter}.png +\`\`\` + +**Why /tmp/ then cp?** In observed sessions, \`$D generate --output ~/.gstack/...\` +failed with "The operation was aborted" while \`--output /tmp/...\` succeeded. This is +a sandbox restriction. Always generate to \`/tmp/\` first, then \`cp\`. + +### Step 3d: Results + +After all agents complete: + +1. Read each generated PNG inline (Read tool) so the user sees all variants at once. +2. Report status: "All {N} variants generated in ~{actual time}. {successes} succeeded, + {failures} failed." +3. For any failures: report explicitly with the error. Do NOT silently skip. +4. If zero variants succeeded: fall back to sequential generation (one at a time with + \`$D generate\`, showing each as it lands). Tell the user: "Parallel generation failed + (likely rate limiting). Falling back to sequential..." +5. Proceed to Step 4 (comparison board). + +**Dynamic image list for comparison board:** When proceeding to Step 4, construct the +image list from whatever variant files actually exist, not a hardcoded A/B/C list: + +\`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat +_IMAGES=$(ls "$_DESIGN_DIR"/variant-*.png 2>/dev/null | tr '\\n' ',' | sed 's/,$//') +\`\`\` + +Use \`$_IMAGES\` in the \`$D compare --images\` command.`; +} + +export function generateDesignShotgunLoop(ctx: TemplateContext): string { + if (ctx.host === 'codex') { + return `### Comparison Board + Feedback Loop + +Codex path: do not use \`$D compare\` or \`$D serve\`. Build a lightweight local board +from the images generated by \`image_gen\`, then collect feedback directly. + +Create a static comparison board: + +\`\`\`bash +node - "$_DESIGN_DIR" <<'NODE' +const fs = require('fs'); +const path = require('path'); +const dir = process.argv[2]; +const files = fs.readdirSync(dir).filter(f => /^variant-[A-Z]\\.png$/.test(f)).sort(); +const cards = files.map(file => { + const label = file.match(/^variant-([A-Z])\\.png$/)?.[1] || file; + return \`

Variant \${label}

Variant \${label}
\`; +}).join('\\n'); +const html = \` + +Design Shotgun Variants + +

Design Shotgun Variants

+
\${cards}
\`; +fs.writeFileSync(path.join(dir, 'design-board.html'), html); +console.log(path.join(dir, 'design-board.html')); +NODE +open "file://$_DESIGN_DIR/design-board.html" 2>/dev/null || true +\`\`\` + +Show each variant inline, then ask for feedback: + +"I've opened a comparison board with the generated variants. Which variant do you +prefer, and what should change?" + +Use AskUserQuestion when available. If AskUserQuestion is unavailable, ask in prose +and wait for the user's typed answer. + +**After receiving feedback:** Output a clear summary confirming what was understood: + +"Here's what I understood from your feedback: +PREFERRED: Variant [X] +YOUR NOTES: [comments] +DIRECTION: [overall] + +Is this right?" + +Use AskUserQuestion or prose confirmation before saving. + +**Save the approved choice:** +\`\`\`bash +echo '{"approved_variant":"","feedback":"","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"","branch":"'$(git branch --show-current 2>/dev/null)'"}' > "$_DESIGN_DIR/approved.json" +\`\`\``; + } + return `### Comparison Board + Feedback Loop Create the comparison board and serve it over HTTP: @@ -1154,4 +1380,3 @@ Flat design can strip away useful visual information that signals interactivity. Prioritize ruthlessly: things needed in a hurry go close at hand, everything else a few taps away with an obvious path to get there.`; } - diff --git a/scripts/resolvers/index.ts b/scripts/resolvers/index.ts index aa598b867..e1e6052a9 100644 --- a/scripts/resolvers/index.ts +++ b/scripts/resolvers/index.ts @@ -20,7 +20,7 @@ import type { TemplateContext, ResolverFn, ResolverValue } from './types'; import { generatePreamble } from './preamble'; import { generateTestFailureTriage } from './preamble'; import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup } from './browse'; -import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch, generateDesignSetup, generateDesignMockup, generateDesignShotgunLoop, generateTasteProfile, generateUXPrinciples } from './design'; +import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch, generateDesignSetup, generateDesignMockup, generateDesignShotgunGeneration, generateDesignShotgunLoop, generateTasteProfile, generateUXPrinciples } from './design'; import { generateTestBootstrap, generateTestCoverageAuditPlan, generateTestCoverageAuditShip, generateTestCoverageAuditReview } from './testing'; import { generateReviewDashboard, generatePlanFileReviewReport, generateExitPlanModeGate, generateAntiShortcutClause, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generateCodexDocReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec, generateScopeDrift, generateCrossReviewDedup } from './review'; import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow } from './utility'; @@ -66,6 +66,7 @@ export const RESOLVERS: Record = { DESIGN_SKETCH: generateDesignSketch, DESIGN_SETUP: generateDesignSetup, DESIGN_MOCKUP: generateDesignMockup, + DESIGN_SHOTGUN_GENERATION: generateDesignShotgunGeneration, DESIGN_SHOTGUN_LOOP: generateDesignShotgunLoop, BENEFITS_FROM: generateBenefitsFrom, CODEX_SECOND_OPINION: generateCodexSecondOpinion, diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd..8e50e7eeb 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1797,6 +1797,19 @@ describe('Codex generation (--host codex)', () => { expect(reviewContent).not.toContain('CODEX_REVIEWS'); }); + test('Codex design-shotgun uses image_gen instead of gstack design binary', () => { + const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-design-shotgun', 'SKILL.md'), 'utf-8'); + expect(content).toContain('gstack-design-shotgun'); + expect(content).toContain('image_gen'); + expect(content).toContain('host-native'); + expect(content).toContain('no OpenAI API key setup'); + expect(content).toContain('no `OPENAI_API_KEY` lookup'); + expect(content).not.toContain('Run: {$D path} generate'); + expect(content).not.toContain('$D compare --images'); + expect(content).not.toContain('DESIGN_READY: $D'); + expect(content).not.toContain('DESIGN_NOT_AVAILABLE'); + }); + test('--host codex --dry-run freshness', () => { const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--dry-run'], { cwd: ROOT,