diff --git a/design-shotgun/SKILL.md b/design-shotgun/SKILL.md index 3386d18fa..fc4043052 100644 --- a/design-shotgun/SKILL.md +++ b/design-shotgun/SKILL.md @@ -1006,8 +1006,8 @@ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null || echo ``` If a local site is running AND the user referenced a URL or said something like "I don't -like how this looks," screenshot the current page and use `$D evolve` instead of -`$D variants` to generate improvement variants from the existing design. +like how this looks," capture the current page and use the screenshot-evolution path in +Step 3c instead of generating unrelated variants from text alone. **AskUserQuestion with pre-filled context:** Pre-fill what you inferred from the codebase, DESIGN.md, and office-hours output. Then ask for what's missing. Frame as ONE question diff --git a/design-shotgun/SKILL.md.tmpl b/design-shotgun/SKILL.md.tmpl index 230dbc292..7db001da2 100644 --- a/design-shotgun/SKILL.md.tmpl +++ b/design-shotgun/SKILL.md.tmpl @@ -127,8 +127,8 @@ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null || echo ``` If a local site is running AND the user referenced a URL or said something like "I don't -like how this looks," screenshot the current page and use `$D evolve` instead of -`$D variants` to generate improvement variants from the existing design. +like how this looks," capture the current page and use the screenshot-evolution path in +Step 3c instead of generating unrelated variants from text alone. **AskUserQuestion with pre-filled context:** Pre-fill what you inferred from the codebase, DESIGN.md, and office-hours output. Then ask for what's missing. Frame as ONE question @@ -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..5edf7f403 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -786,6 +786,52 @@ 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. Invoke the documented +\`$imagegen\` skill for mockup generation and editing. Its default built-in mode uses +Codex's \`image_gen\` tool and does not require \`OPENAI_API_KEY\`. + +Resolve both local helpers. \`$B\` captures an existing page for screenshot evolution. +\`$D\` is allowed only for the local comparison-board commands \`compare\` and \`serve\`: + +\`\`\`bash +_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +D="" +[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" +[ -z "$D" ] && D="${ctx.paths.designDir}/design" +if [ -x "$D" ]; then + echo "DESIGN_BOARD_READY: $D" +else + echo "DESIGN_BOARD_NOT_AVAILABLE" +fi +B="" +[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" ] && B="$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" +[ -z "$B" ] && B="${ctx.paths.browseDir}/browse" +if [ -x "$B" ]; then + echo "BROWSE_READY: $B" +else + echo "BROWSE_NOT_AVAILABLE" +fi +\`\`\` + +If \`BROWSE_NOT_AVAILABLE\` while evolving an existing page, ask the user for a +screenshot or stop that evolution path. New-image exploration can still proceed. + +If \`DESIGN_BOARD_NOT_AVAILABLE\`, show the generated images inline and collect +feedback with AskUserQuestion. Do not create a platform-specific static-board fallback. + +Codex generation restrictions: +- no \`$D generate\`, \`$D variants\`, \`$D evolve\`, \`$D iterate\`, or \`$D check\` +- no \`$imagegen\` CLI fallback +- no OpenAI API key setup or \`OPENAI_API_KEY\` lookup + +Design artifacts still belong in \`~/.gstack/projects/$SLUG/designs/\`. Copy the +paths returned by \`$imagegen\` into the design session directory before building +the comparison board.`; + } + return `## DESIGN SETUP (run this check BEFORE any design mockup command) \`\`\`bash @@ -909,7 +955,236 @@ 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 \`$imagegen\` Generation + +**Codex host rule:** Invoke the documented \`$imagegen\` skill once per confirmed +variant. Keep it in default built-in mode so it uses the host \`image_gen\` tool. +Do not invoke \`image_gen\` as an undocumented command, use \`scripts/image_gen.py\`, +or run any \`$D\` generation command. + +**If evolving from a screenshot** (user said "I don't like THIS"), first confirm +CODEX IMAGE SETUP printed \`BROWSE_READY\`, navigate \`$B\` to the target URL when needed, and +capture ONE current-state screenshot: + +\`\`\`bash +$B screenshot "$_DESIGN_DIR/current.png" +\`\`\` + +Load \`$_DESIGN_DIR/current.png\` with the host \`view_image\` tool so it is visible +in conversation context. Then invoke \`$imagegen\` in edit mode once per variant, +using the screenshot as the edit target. State the invariant that the product, +content, and interaction purpose stay recognizable while the confirmed visual +direction changes. If \`BROWSE_NOT_AVAILABLE\`, do not invent a screenshot from text. + +**For a new design**, invoke \`$imagegen\` in generate mode once per variant. Each +invocation must use the default built-in mode and a complete prompt containing: + +- \`Use case: ui-mockup\` +- the product/screen brief and 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 the independent \`$imagegen\` invocations in parallel when the host supports it. +Otherwise run them sequentially and show each result as it lands. If the built-in +path fails or is unavailable, report the failure and stop; do not switch to the CLI +fallback or ask for \`OPENAI_API_KEY\`. + +**Save returned images into the design session directory.** Copy each path returned +by \`$imagegen\` from the Codex generated-images directory to the canonical design +session path: + +\`\`\`bash +cp "<$imagegen returned path for variant A>" "$_DESIGN_DIR/variant-A.png" +cp "<$imagegen returned path for variant B>" "$_DESIGN_DIR/variant-B.png" +cp "<$imagegen returned path for variant C>" "$_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 \`$imagegen\` did not return usable images. + Do NOT fall back to the gstack design binary on Codex. +5. Proceed to Step 4 (comparison board or inline feedback fallback). + +**Build a portable image list for the comparison board.** Include every successful +variant without relying on shell glob behavior: + +\`\`\`bash +_IMAGES=$(find "$_DESIGN_DIR" -maxdepth 1 -type f -name 'variant-*.png' -print 2>/dev/null | sort | awk 'BEGIN { sep="" } { printf "%s%s", sep, $0; sep="," }') +echo "IMAGES: $_IMAGES" +\`\`\` + +Use \`$_IMAGES\` for 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 + +The gstack design binary remains useful here because \`compare\` and \`serve\` are +local board operations. They do not generate images or require an OpenAI API key. + +**If \`DESIGN_BOARD_READY\`:** Create the board with the portable gstack board path: + +\`\`\`bash +$D compare --images "$_IMAGES" --output "$_DESIGN_DIR/design-board.html" --serve +\`\`\` + +This generates the board HTML, starts the cross-platform HTTP server on a random +port, and opens the user's default browser. Run it in the background while the user +interacts with the board. Parse \`BOARD_URL: http://127.0.0.1:N/boards//\` from +stderr; the URL already includes the per-board path. + +Use AskUserQuestion only as the blocking wait mechanism. Include \`BOARD_URL\` so the +user can reopen the board, and ask them to submit feedback there. Do not use +AskUserQuestion as a second variant chooser while the board is available. + +After the user responds, check the board feedback files: + +\`\`\`bash +if [ -f "$_DESIGN_DIR/feedback.json" ]; then + echo "SUBMIT_RECEIVED" + cat "$_DESIGN_DIR/feedback.json" +elif [ -f "$_DESIGN_DIR/feedback-pending.json" ]; then + echo "REGENERATE_RECEIVED" + cat "$_DESIGN_DIR/feedback-pending.json" + rm "$_DESIGN_DIR/feedback-pending.json" +else + echo "NO_FEEDBACK_FILE" +fi +\`\`\` + +The feedback JSON has this shape: +\`\`\`json +{ + "preferred": "A", + "ratings": { "A": 4, "B": 3, "C": 2 }, + "comments": { "A": "Love the spacing" }, + "overall": "Go with A, bigger CTA", + "regenerated": false +} +\`\`\` + +**If \`feedback-pending.json\` is found:** +1. Read \`regenerateAction\` (\`"different"\`, \`"match"\`, \`"more_like_B"\`, + \`"remix"\`, or custom text) and any \`remixSpec\`. +2. Invoke \`$imagegen\` once per replacement variant in default built-in mode. For + \`more_like\` or \`remix\`, first load the selected variant PNGs with \`view_image\` + and use edit/reference mode. For \`different\`, generate from the updated brief. +3. Copy every returned image into its canonical \`variant-.png\` path and + rebuild \`$_IMAGES\` with the portable loop from Step 3d. +4. Rebuild the board locally: + \`$D compare --images "$_IMAGES" --output "$_DESIGN_DIR/design-board.html"\` +5. Reload the same daemon board: + \`curl -s -X POST "\${BOARD_URL}api/reload" -H 'Content-Type: application/json' -d '{"html":"$_DESIGN_DIR/design-board.html"}'\` +6. AskUserQuestion again with the same board URL and repeat until + \`feedback.json\` appears. + +Never use \`$D iterate\`, \`$D variants\`, or the \`$imagegen\` CLI fallback for +regeneration on Codex. + +**If \`DESIGN_BOARD_NOT_AVAILABLE\` or the board server fails:** Show every variant +inline, then use AskUserQuestion to ask which variant the user prefers and what should +change. This fallback is host-native and does not use \`open\`, \`start\`, or +\`xdg-open\` directly. + +**After receiving feedback (any path):** Output a clear summary confirming what was +understood, then use AskUserQuestion to verify before proceeding. + +**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 +1429,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/codex-e2e.test.ts b/test/codex-e2e.test.ts index 2f2817f90..62626013f 100644 --- a/test/codex-e2e.test.ts +++ b/test/codex-e2e.test.ts @@ -55,6 +55,13 @@ if (!evalsEnabled) { // Codex E2E touchfiles — keyed by test name, same pattern as E2E_TOUCHFILES const CODEX_E2E_TOUCHFILES: Record = { 'codex-discover-skill': ['codex/**', '.agents/skills/**', 'test/helpers/codex-session-runner.ts'], + 'codex-design-shotgun-routing': [ + 'design-shotgun/**', + '.agents/skills/gstack-design-shotgun/**', + 'scripts/resolvers/design.ts', + 'scripts/resolvers/index.ts', + 'test/helpers/codex-session-runner.ts', + ], 'codex-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'codex/**', 'test/helpers/codex-session-runner.ts'], }; @@ -159,6 +166,51 @@ describeCodex('Codex E2E', () => { ).toBe(true); }, 120_000); + testIfSelected('codex-design-shotgun-routing', async () => { + const skillDir = path.join(testWorktree, '.agents', 'skills', 'gstack-design-shotgun'); + + const result = await runCodexSkill({ + skillDir, + prompt: `Use the gstack-design-shotgun skill for this routing smoke test. +Do not generate images, run shell commands, modify files, or ask questions. Read the +Codex-specific workflow and state exactly: +1. which skill handles each generated or edited variant; +2. which command captures an existing page for screenshot evolution; +3. which command creates and serves the comparison board; +4. whether OPENAI_API_KEY is required. +Keep the answer under 150 words.`, + timeoutMs: 120_000, + cwd: testWorktree, + skillName: 'gstack-design-shotgun', + homeParent: path.join(testWorktree, '.codex-e2e-homes'), + model: 'gpt-5.4', + }); + + logCodexCost('codex-design-shotgun-routing', result); + + if (result.exitCode === 124 || result.exitCode === 137) { + console.warn(`codex-design-shotgun-routing: Codex timed out (exit ${result.exitCode}) - skipping assertions`); + recordCodexE2E('codex-design-shotgun-routing', result, true); + return; + } + + const output = result.output; + const passed = result.exitCode === 0 + && /\$imagegen|imagegen/i.test(output) + && /\$B\s+screenshot/i.test(output) + && /\$D\s+compare/i.test(output) + && /OPENAI_API_KEY/i.test(output) + && /not required|does not require|no API key|without.*API key/i.test(output); + recordCodexE2E('codex-design-shotgun-routing', result, passed); + + expect(result.exitCode).toBe(0); + expect(output).toMatch(/\$imagegen|imagegen/i); + expect(output).toMatch(/\$B\s+screenshot/i); + expect(output).toMatch(/\$D\s+compare/i); + expect(output).toMatch(/OPENAI_API_KEY/i); + expect(output).toMatch(/not required|does not require|no API key|without.*API key/i); + }, 180_000); + // Validates that Codex can invoke the gstack-review skill, run a diff-based // code review, and produce structured review output with findings/issues. // Accepts Codex timeout (exit 124/137) as non-failure since that's a CLI perf issue. diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd..dae41bdf3 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1797,6 +1797,68 @@ describe('Codex generation (--host codex)', () => { expect(reviewContent).not.toContain('CODEX_REVIEWS'); }); + test('Codex design-shotgun routes generation through $imagegen and keeps local helpers', () => { + const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-design-shotgun', 'SKILL.md'), 'utf-8'); + expect(content).toContain('gstack-design-shotgun'); + expect(content).toContain('$imagegen'); + expect(content).toContain('image_gen'); + expect(content).toContain('default built-in mode'); + expect(content).toContain('$B screenshot "$_DESIGN_DIR/current.png"'); + expect(content).toContain('Load `$_DESIGN_DIR/current.png` with the host `view_image` tool'); + expect(content).toContain('DESIGN_BOARD_READY: $D'); + expect(content).toContain('BROWSE_READY: $B'); + expect(content).toContain('$D compare --images "$_IMAGES"'); + expect(content).toContain('no OpenAI API key setup or `OPENAI_API_KEY` lookup'); + expect(content).not.toContain('Run: {$D path} generate'); + expect(content).not.toContain('$D evolve --screenshot'); + expect(content).not.toContain('node - "$_DESIGN_DIR"'); + expect(content).not.toContain('open "file://$_DESIGN_DIR/design-board.html"'); + expect(content).not.toContain('$HOME$GSTACK_DESIGN'); + expect(content).not.toContain('$HOME$GSTACK_BROWSE'); + }); + + test('Codex design-shotgun setup resolves browser and board binaries', () => { + const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-design-shotgun', 'SKILL.md'), 'utf-8'); + const sectionStart = content.indexOf('## CODEX IMAGE SETUP'); + const fenceStart = content.indexOf('```bash\n', sectionStart) + '```bash\n'.length; + const fenceEnd = content.indexOf('\n```', fenceStart); + expect(sectionStart).toBeGreaterThanOrEqual(0); + expect(fenceStart).toBeGreaterThan(sectionStart); + expect(fenceEnd).toBeGreaterThan(fenceStart); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-codex-design-setup-')); + try { + const designDir = path.join(tempDir, 'design', 'dist'); + const browseDir = path.join(tempDir, 'browse', 'dist'); + fs.mkdirSync(designDir, { recursive: true }); + fs.mkdirSync(browseDir, { recursive: true }); + fs.writeFileSync(path.join(designDir, 'design'), '#!/bin/sh\nexit 0\n'); + fs.writeFileSync(path.join(browseDir, 'browse'), '#!/bin/sh\nexit 0\n'); + fs.chmodSync(path.join(designDir, 'design'), 0o755); + fs.chmodSync(path.join(browseDir, 'browse'), 0o755); + + const result = Bun.spawnSync(['bash', '-c', content.slice(fenceStart, fenceEnd)], { + cwd: tempDir, + env: { + ...process.env, + HOME: tempDir, + GSTACK_DESIGN: designDir, + GSTACK_BROWSE: browseDir, + }, + stdout: 'pipe', + stderr: 'pipe', + }); + + expect(result.exitCode).toBe(0); + const output = result.stdout.toString(); + expect(output).toContain(`DESIGN_BOARD_READY: ${path.join(designDir, 'design')}`); + expect(output).toContain(`BROWSE_READY: ${path.join(browseDir, 'browse')}`); + expect(result.stderr.toString()).toBe(''); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + test('--host codex --dry-run freshness', () => { const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--dry-run'], { cwd: ROOT, diff --git a/test/helpers/codex-session-runner.ts b/test/helpers/codex-session-runner.ts index 404aa6bb2..1dae29f27 100644 --- a/test/helpers/codex-session-runner.ts +++ b/test/helpers/codex-session-runner.ts @@ -144,6 +144,8 @@ export async function runCodexSkill(opts: { cwd?: string; // Working directory skillName?: string; // Skill name for installation (default: dirname) sandbox?: string; // Sandbox mode (default: 'read-only') + homeParent?: string; // Parent for isolated HOME (default: OS temp dir) + model?: string; // Optional stable model override for evals }): Promise { const { skillDir, @@ -152,6 +154,8 @@ export async function runCodexSkill(opts: { cwd, skillName, sandbox = 'read-only', + homeParent = os.tmpdir(), + model, } = opts; const startTime = Date.now(); @@ -174,7 +178,8 @@ export async function runCodexSkill(opts: { } // Set up temp HOME with skill installed - const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-e2e-')); + fs.mkdirSync(homeParent, { recursive: true }); + const tempHome = fs.mkdtempSync(path.join(homeParent, 'codex-e2e-')); const realHome = os.homedir(); try { @@ -201,6 +206,7 @@ export async function runCodexSkill(opts: { // Build codex exec command const args = ['exec', prompt, '--json', '-s', sandbox]; + if (model) args.push('-m', model); // Spawn codex with temp HOME so it discovers our installed skill. // Hermetic scrub (test/helpers/hermetic-env.ts) with codex's auth surface