B4: DESIGN_READY only when the design binary actually starts

DESIGN SETUP (and office-hours' visual exploration) printed DESIGN_READY for
any executable binary, so one SIGKILLed at launch by an invalid macOS code
signature still passed. Launch it under the portable 10-second deadline
(gtimeout, timeout, or perl alarm); otherwise print DESIGN_NOT_AVAILABLE with
the exit code, 'timed out after 10s', or on 137 the code-signature hint and
'Fix: cd <checkout> && ./setup'. A success is cached by inode and mtime under
the state root so Gatekeeper's first-launch scan is paid once.

The probe launches the binary with no arguments (usage, exit 0): the design
CLI has no --version command today.
This commit is contained in:
garrytan committed 2026-10-03 21:18:31 +00:00
1 parent 358d91ea87
commit 79bfc02ac2
2 files changed
+98 -9

No files matched your search

+28 -6
View File
@@ -1115,16 +1115,38 @@ ${slopSection}
Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4) (Mar 2026) + gstack design methodology.`;
}
/**
* B4 (#1076, #1254): DESIGN_READY only when the binary actually starts. A
* binary that is merely executable can still be SIGKILLed at launch (an
* invalid macOS code signature), so launch it (no arguments prints usage and
* exits 0) under the portable deadline aside.ts uses. A success is cached by
* inode + mtime so Gatekeeper's first-launch scan is paid once.
*/
function designReadyProbe(ctx: TemplateContext): string {
return `_DS=$("${toShellPath(ctx.paths.skillRoot)}/bin/gstack-paths" --get GSTACK_STATE_ROOT 2>/dev/null); _DC=\${_DS:+$_DS/design-ready}
_DK="$(ls -diL "$D" 2>/dev/null | awk '{print $(1)}') $( (stat -L -c %Y "$D" || stat -L -f %m "$D") 2>/dev/null) $D"
_dt() { if command -v gtimeout >/dev/null; then gtimeout 10 "$@"; elif command -v timeout >/dev/null; then timeout 10 "$@"
elif command -v perl >/dev/null; then perl -e 'alarm(shift);exec(@ARGV)' 10 "$@"; else return 125; fi; }
_RC=0
if [ ! -x "$D" ]; then _RC=missing
elif [ -z "$_DC" ] || [ "$(cat "$_DC" 2>/dev/null)" != "$_DK" ]; then _dt "$D" >/dev/null 2>&1 </dev/null || _RC=$?
fi
case "$_RC" in
0) echo "DESIGN_READY: $D"; [ -n "$_DC" ] && printf '%s' "$_DK" > "$_DC" 2>/dev/null ;;
missing) echo "DESIGN_NOT_AVAILABLE: $D is not installed. Fix: cd \${D%/design/dist/design} && ./setup" ;;
124|142) echo "DESIGN_NOT_AVAILABLE: $D timed out after 10s at launch" ;;
125) echo "DESIGN_NOT_AVAILABLE: no timeout, gtimeout or perl to bound the $D launch check" ;;
137) echo "DESIGN_NOT_AVAILABLE: $D exited 137 (killed at launch; on macOS usually an invalid code signature). Fix: cd \${D%/design/dist/design} && ./setup" ;;
*) echo "DESIGN_NOT_AVAILABLE: $D exited $_RC at launch" ;;
esac`;
}
export function generateDesignSetup(ctx: TemplateContext): string {
return `## DESIGN SETUP (run this check BEFORE any design mockup command)
\`\`\`bash
${binaryAssignment(ctx, 'design')}
if [ -x "$D" ]; then
echo "DESIGN_READY: $D"
else
echo "DESIGN_NOT_AVAILABLE"
fi
${designReadyProbe(ctx)}
\`\`\`
${ctx.skillName === 'design-consultation' ? `If \`DESIGN_NOT_AVAILABLE\`: use Phase 5 Path B (HTML preview). Mockups are optional.
@@ -1159,7 +1181,7 @@ export function generateDesignMockup(ctx: TemplateContext): string {
\`\`\`bash
${binaryAssignment(ctx, 'design')}
[ -x "$D" ] && echo "DESIGN_READY" || echo "DESIGN_NOT_AVAILABLE"
${designReadyProbe(ctx)}
\`\`\`
**If \`DESIGN_NOT_AVAILABLE\`:** Fall back to the HTML wireframe approach below
+70 -3
View File
@@ -1,11 +1,16 @@
/**
* F3: the rendered taste-profile fence, executed with stub binaries.
* The taste-profile probe double-quoted `~/.claude/...`, so it never found
* B4 (#1076, #1254) and F3: the rendered DESIGN SETUP and taste-profile fences,
* executed with stub binaries.
*
* B4: an executable design binary that dies at launch (SIGKILL from an invalid
* macOS code signature) used to print DESIGN_READY. Now the binary must start
* within 10 seconds; a success is cached by inode + mtime.
* F3: the taste-profile probe double-quoted `~/.claude/...`, so it never found
* gstack-slug and always printed NO_TASTE_PROFILE; a failed slug lookup is now
* reported as TASTE_PROFILE_UNAVAILABLE instead of "no profile".
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import { spawn, spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
@@ -44,6 +49,68 @@ beforeAll(async () => {
});
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
describe('B4: DESIGN_READY requires a binary that starts', () => {
for (const host of HOSTS) {
const setup = () => fenceAfter(path.join(host.dir('design-review'), 'SKILL.md'), '## DESIGN SETUP');
test(`${host.name}: a binary that launches is ready, and the success is cached by inode and mtime`, () => {
const w = world(host.root, 'exit 0');
const first = run(setup(), w);
expect(first.stdout.trim(), first.stderr).toBe(`DESIGN_READY: ${w.install}/design/dist/design`);
expect(run(setup(), w).stdout.trim()).toStartWith('DESIGN_READY: ');
expect(w.launches()).toBe(1);
fs.writeFileSync(path.join(w.install, 'design/dist/design'), `#!/bin/sh\necho launched >> "${w.home}/launches"\nexit 3\n`);
fs.utimesSync(path.join(w.install, 'design/dist/design'), new Date(), new Date(Date.now() + 5_000));
expect(run(setup(), w).stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design exited 3 at launch`);
});
test(`${host.name}: a binary killed at launch is not ready and names the fix`, () => {
const w = world(host.root, 'kill -9 $$');
const r = run(setup(), w);
expect(r.stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design exited 137 (killed at launch; on macOS usually an invalid code signature). Fix: cd ${w.install} && ./setup`);
expect(fs.existsSync(path.join(w.state, 'design-ready'))).toBe(false);
});
test(`${host.name}: a missing binary says how to install it`, () => {
const w = world(host.root, null);
expect(run(setup(), w).stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design is not installed. Fix: cd ${w.install} && ./setup`);
});
}
test('claude: without timeout, gtimeout or perl the launch is not attempted unbounded', () => {
const w = world(HOSTS[0].root, 'exit 0');
const bin = path.join(w.home, 'minbin');
fs.mkdirSync(bin);
for (const tool of ['ls', 'awk', 'stat', 'cat', 'git', 'bash', 'sh', 'env']) {
const found = spawnSync('bash', ['-c', `command -v ${tool}`], { encoding: 'utf8', timeout: 5_000 }).stdout.trim();
if (found) fs.symlinkSync(found, path.join(bin, tool));
}
const r = run(fenceAfter(path.join(HOSTS[0].dir('design-review'), 'SKILL.md'), '## DESIGN SETUP'), w, bin);
expect(r.stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: no timeout, gtimeout or perl to bound the ${w.install}/design/dist/design launch check`);
expect(w.launches()).toBe(0);
});
test('claude: a binary that hangs times out after 10 seconds', async () => {
const w = world(HOSTS[0].root, 'sleep 60');
const script = fenceAfter(path.join(HOSTS[0].dir('design-review'), 'SKILL.md'), '## DESIGN SETUP');
const stdout = await new Promise<string>((resolve, reject) => {
const child = spawn('env', ['-i', `HOME=${w.home}`, 'PATH=/usr/bin:/bin', 'bash', '-c', script], { cwd: w.cwd, timeout: 30_000 });
let text = '';
child.stdout.on('data', d => { text += d; });
child.on('error', reject);
child.on('close', () => resolve(text));
});
expect(stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design timed out after 10s at launch`);
}, 30_000);
test('office-hours visual exploration uses the same probe', () => {
const w = world(HOSTS[0].root, 'kill -9 $$');
const r = run(fenceAfter(path.join(HOSTS[0].dir('office-hours'), 'SKILL.md'), '## Visual Design Exploration'), w);
expect(r.stdout.trim()).toStartWith('DESIGN_NOT_AVAILABLE: ');
expect(r.stdout).toContain('exited 137 (killed at launch');
});
});
describe('F3: the taste profile loads, and a failed slug lookup is not "no profile"', () => {
for (const skill of ['design-shotgun', 'design-consultation']) {
for (const host of HOSTS) {