This commit is contained in:
Jinzhe 2026-07-16 10:08:16 +00:00 committed by GitHub
commit c58792ea0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 404 additions and 81 deletions

View File

@ -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

View File

@ -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

View File

@ -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":"<VARIANT>","feedback":"<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/<id>/\` 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-<letter>.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":"<V>","feedback":"<FB>","date":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","screen":"<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.`;
}

View File

@ -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<string, ResolverValue> = {
DESIGN_SKETCH: generateDesignSketch,
DESIGN_SETUP: generateDesignSetup,
DESIGN_MOCKUP: generateDesignMockup,
DESIGN_SHOTGUN_GENERATION: generateDesignShotgunGeneration,
DESIGN_SHOTGUN_LOOP: generateDesignShotgunLoop,
BENEFITS_FROM: generateBenefitsFrom,
CODEX_SECOND_OPINION: generateCodexSecondOpinion,

View File

@ -55,6 +55,13 @@ if (!evalsEnabled) {
// Codex E2E touchfiles — keyed by test name, same pattern as E2E_TOUCHFILES
const CODEX_E2E_TOUCHFILES: Record<string, string[]> = {
'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.

View File

@ -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,

View File

@ -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<CodexResult> {
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