F3: load the taste profile, and report a failed slug lookup

The taste-profile probe double-quoted ~/.claude/.../gstack-slug, so the tilde
never expanded and /design-consultation and /design-shotgun always printed
NO_TASTE_PROFILE. Use quoteSafePath(); stop hiding gstack-slug's stderr and
print TASTE_PROFILE_UNAVAILABLE with the fix when the slug cannot be resolved.
This commit is contained in:
garrytan committed 2026-10-03 21:18:31 +00:00
1 parent 25638ad445
commit 358d91ea87
2 files changed
+73 -4

No files matched your search

+6 -4
View File
@@ -1,5 +1,5 @@
import { outsideVoiceFailurePolicy, outsideVoiceFor, outsideVoiceInvocation, outsideVoicePreflight, outsideVoiceProvenance } from './outside-voice';
import { type TemplateContext, toShellPath } from './types';
import { type TemplateContext, quoteSafePath, toShellPath } from './types';
import { binaryAssignment } from './runtime-root';
import { AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS, CC_BACKGROUND_DEFAULT_SINCE } from './constants';
import { OVERUSED_FONTS_DISPLAY, BANNED_FONTS, FONTS_BODY_UI_OK, FONTS_MONO_OK, FONTS_VERIFIED_FREE, HANDOFF_COMMANDS, selectCatalog, catalogEntries, renderCatalog, detectorSlopEntries, judgmentTellEntries } from '../../lib/design-catalog';
@@ -1382,9 +1382,9 @@ export function generateTasteProfile(ctx: TemplateContext): string {
return `Read this project's taste profile:
\`\`\`bash
SLUG=$("${ctx.paths.binDir}/gstack-slug" --get SLUG 2>/dev/null)
[ -n "\${SLUG:-}" ] || { echo "NO_TASTE_PROFILE"; exit 0; }
GSTACK_STATE_ROOT=$("${ctx.paths.binDir}/gstack-paths" --get GSTACK_STATE_ROOT); : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
SLUG=$("${quoteSafePath(ctx.paths.binDir)}/gstack-slug" --get SLUG) || SLUG=""
[ -n "\${SLUG:-}" ] || { echo "TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup."; exit 0; }
GSTACK_STATE_ROOT=$("${quoteSafePath(ctx.paths.binDir)}/gstack-paths" --get GSTACK_STATE_ROOT); : "\${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
_TASTE_PROFILE="$GSTACK_STATE_ROOT/projects/$SLUG/taste-profile.json"
if [ -f "$_TASTE_PROFILE" ]; then
# Schema v1: { dimensions: { fonts, colors, layouts, aesthetics }, sessions: [] }
@@ -1398,6 +1398,8 @@ else
fi
\`\`\`
**If TASTE_PROFILE_UNAVAILABLE:** say that line once, then continue without a taste profile (do not treat it as "no history").
**If TASTE_PROFILE_FOUND:** Parse the full JSON; malformed/unreadable uses the legacy fallback. After decay, rank each dimension by confidence * approved_count (or rejected_count); take three per kind. Count retained sessions (at most 50, not lifetime). Include in ${ctx.skillName === 'design-consultation' ? 'the Phase 1 product brief (later shared unchanged with both independent voices)' : 'the brief'}:
"Based on [number of retained sessions] recorded sessions, this user's taste leans toward:
+67
View File
@@ -0,0 +1,67 @@
/**
* F3: the rendered taste-profile fence, executed with stub binaries.
* 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 * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runGeneration } from '../scripts/gen-skill-docs';
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-design-probe-'));
const out = path.join(tmp, 'render');
const HOSTS = [{ name: 'claude', dir: (s: string) => path.join(out, s), root: '.claude/skills/gstack' },
{ name: 'codex', dir: (s: string) => path.join(out, '.agents', 'skills', `gstack-${s}`), root: '.codex/skills/gstack' }];
function fenceAfter(file: string, heading: string): string {
const text = fs.readFileSync(file, 'utf8');
return text.slice(text.indexOf(heading)).match(/```bash\n([\s\S]*?)\n```/)![1];
}
/** A HOME with a gstack install whose design binary runs `body`. */
function world(root: string, body: string | null) {
const home = fs.mkdtempSync(path.join(tmp, 'home-'));
const install = path.join(home, root);
const state = path.join(home, 'state');
for (const dir of ['bin', 'lib', 'design/dist']) fs.mkdirSync(path.join(install, dir), { recursive: true });
fs.mkdirSync(state);
fs.writeFileSync(path.join(install, 'bin', 'gstack-paths'), `#!/bin/sh\necho "${state}"\n`, { mode: 0o755 });
if (body !== null) fs.writeFileSync(path.join(install, 'design/dist/design'), `#!/bin/sh\necho launched >> "${home}/launches"\n${body}\n`, { mode: 0o755 });
const cwd = fs.mkdtempSync(path.join(tmp, 'cwd-'));
return { home, install, state, cwd, launches: () => (fs.existsSync(path.join(home, 'launches')) ? fs.readFileSync(path.join(home, 'launches'), 'utf8').split('\n').filter(Boolean).length : 0) };
}
function run(script: string, w: { home: string; cwd: string }, PATH = '/usr/bin:/bin') {
return spawnSync('env', ['-i', `HOME=${w.home}`, `PATH=${PATH}`, 'bash', '-c', script], { cwd: w.cwd, encoding: 'utf8', timeout: 30_000 });
}
beforeAll(async () => {
const r = await runGeneration({ host: 'all', outputRoot: out });
if (r.exitCode !== 0) throw new Error(r.diagnostics.filter(d => d.kind === 'error').map(d => d.message).join('\n'));
});
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
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) {
test(`${host.name} ${skill}`, () => {
const w = world(host.root, 'exit 0');
const fence = fenceAfter(path.join(host.dir(skill), 'SKILL.md'), "Read this project's taste profile:");
fs.mkdirSync(path.join(w.state, 'projects', 'acme'), { recursive: true });
fs.writeFileSync(path.join(w.state, 'projects', 'acme', 'taste-profile.json'), '{"dimensions":{}}');
fs.writeFileSync(path.join(w.install, 'bin', 'gstack-slug'), '#!/bin/sh\necho acme\n', { mode: 0o755 });
const found = run(fence, w);
expect(found.stdout, found.stderr).toContain('TASTE_PROFILE_FOUND');
expect(found.stdout).toContain('{"dimensions":{}}');
fs.writeFileSync(path.join(w.install, 'bin', 'gstack-slug'), '#!/bin/sh\necho "gstack-slug: cannot read state" >&2\nexit 1\n');
const failed = run(fence, w);
expect(failed.stdout.trim()).toBe('TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup.');
expect(failed.stderr).toContain('gstack-slug: cannot read state');
});
}
}
});