diff --git a/.cursor/pstack-models.md b/.cursor/pstack-models.md new file mode 100644 index 0000000..e626535 --- /dev/null +++ b/.cursor/pstack-models.md @@ -0,0 +1,24 @@ +# pstack model configuration + +Per-role model overrides for pstack skills. Each pstack SKILL.md names its defaults in a Models section; the values here override those defaults. Delete a line to fall back to the skill default. A value of `inherit-parent` or `auto` runs that role on the parent session's model (the `Agent` call omits `model`); an alias entry in a panel list still counts toward that panel's fan-out. + +These values are Cursor Task slugs confirmed in this workspace. Claude Code defaults (`claude-opus-4-8`, `claude-opus-5`, `claude-fable-5`, `claude-sonnet-5`, `claude-haiku-4-5`) do not resolve on Cursor Task. Opus 4.8 maps to `claude-opus-5-thinking-high`. Haiku maps to `composer-2.5-fast`. Strongest judgment uses Fable 5 xhigh. + +feature, refactoring: claude-opus-5-thinking-high +bug-fix: claude-opus-5-thinking-high +perf-issue: claude-opus-5-thinking-high +hillclimb: claude-opus-5-thinking-high +judgment and prose: claude-opus-5-thinking-high +strongest judgment: claude-fable-5-thinking-xhigh +how explorer: claude-opus-5-thinking-high +how explainer: claude-opus-5-thinking-high +how critics: claude-opus-5-thinking-high, claude-fable-5-thinking-high, claude-sonnet-5-thinking-high, composer-2.5-fast +why investigators: claude-opus-5-thinking-high +why synthesizer: claude-opus-5-thinking-high +reflect tooling: claude-opus-5-thinking-high +reflect judgment, divergent, synthesizer: claude-opus-5-thinking-high +arena runners: claude-opus-5-thinking-high, claude-fable-5-thinking-high, claude-sonnet-5-thinking-high, composer-2.5-fast +arena cross-judge pool: claude-opus-5-thinking-high, claude-fable-5-thinking-high, claude-sonnet-5-thinking-high +swarm workers: claude-opus-5-thinking-high +architect runners: claude-opus-5-thinking-high, claude-fable-5-thinking-high, claude-sonnet-5-thinking-high, composer-2.5-fast +interrogate reviewers: claude-opus-5-thinking-high, claude-fable-5-thinking-high, claude-sonnet-5-thinking-high, composer-2.5-fast diff --git a/AGENTS.md b/AGENTS.md index d02638a..3eaa1aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,3 +27,8 @@ Agent) and are discovered automatically by compatible agents. When a task matches a skill's trigger description, read that skill's `SKILL.md` and follow it. + +When spawning a pstack subagent, use the role-to-model lines in +`.cursor/pstack-models.md`. `scripts/link-agent-skills.sh` copies that sheet to +`~/.claude/pstack-models.md` on Cursor Cloud boot, which is the path pstack +skills read. Re-run `/setup-pstack` to change a role. diff --git a/scripts/link-agent-skills.sh b/scripts/link-agent-skills.sh index 293d4c5..8000429 100755 --- a/scripts/link-agent-skills.sh +++ b/scripts/link-agent-skills.sh @@ -34,7 +34,37 @@ link_packs() { done } +install_pstack_models_on_cursor_cloud() { + local src="$REPO_ROOT/.cursor/pstack-models.md" + local dest="$HOME_DIR/.claude/pstack-models.md" + local claude_md="$HOME_DIR/.claude/CLAUDE.md" + local include_line="@~/.claude/pstack-models.md" + + [ -f "$src" ] || { echo "link-agent-skills: $src not present, skipping pstack models"; return 0; } + + if [ "${CURSOR_AGENT:-}" != "1" ]; then + echo "link-agent-skills: CURSOR_AGENT unset, leaving home pstack-models unchanged" + return 0 + fi + + mkdir -p "$HOME_DIR/.claude" + cp "$src" "$dest" + echo "link-agent-skills: installed $dest" + + if [ -f "$claude_md" ] && grep -Fqx "$include_line" "$claude_md"; then + echo "link-agent-skills: $claude_md already includes $include_line" + return 0 + fi + if [ -f "$claude_md" ] && [ -s "$claude_md" ]; then + printf '\n%s\n' "$include_line" >> "$claude_md" + else + printf '%s\n' "$include_line" > "$claude_md" + fi + echo "link-agent-skills: wired $include_line into $claude_md" +} + link_packs "$REPO_ROOT/.claude/skills" "$HOME_DIR/.claude/skills" link_packs "$REPO_ROOT/.agents/skills" "$HOME_DIR/.agents/skills" +install_pstack_models_on_cursor_cloud echo "link-agent-skills: done" diff --git a/test/link-agent-skills.test.js b/test/link-agent-skills.test.js new file mode 100644 index 0000000..a0dc43a --- /dev/null +++ b/test/link-agent-skills.test.js @@ -0,0 +1,137 @@ +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const SCRIPT = path.join(REPO_ROOT, 'scripts', 'link-agent-skills.sh'); +const SHEET = path.join(REPO_ROOT, '.cursor', 'pstack-models.md'); +const INCLUDE_LINE = '@~/.claude/pstack-models.md'; + +const REQUIRED_ROLES = [ + 'feature, refactoring', + 'bug-fix', + 'perf-issue', + 'hillclimb', + 'judgment and prose', + 'strongest judgment', + 'how explorer', + 'how explainer', + 'how critics', + 'why investigators', + 'why synthesizer', + 'reflect tooling', + 'reflect judgment, divergent, synthesizer', + 'arena runners', + 'arena cross-judge pool', + 'swarm workers', + 'architect runners', + 'interrogate reviewers' +]; + +const CURSOR_TASK_SLUGS = new Set([ + 'inherit-parent', + 'auto', + 'claude-fable-5-thinking-high', + 'claude-fable-5-thinking-xhigh', + 'claude-opus-5-thinking-high', + 'claude-opus-5-thinking-high-fast', + 'claude-sonnet-5-thinking-high', + 'claude-sonnet-5-thinking-xhigh', + 'composer-2.5', + 'composer-2.5-fast', + 'cursor-grok-4.5-high', + 'cursor-grok-4.5-high-fast', + 'cursor-grok-4.6-high-fast', + 'gemini-3.7-flash-high', + 'gpt-5.6-luna-high', + 'gpt-5.6-sol-high', + 'gpt-5.6-sol-high-fast', + 'gpt-5.6-sol-xhigh', + 'gpt-5.6-sol-xhigh-fast' +]); + +const SLUG = /^[a-z0-9][a-z0-9.-]*$/; + +function parseRoleLines(markdown) { + const roles = new Map(); + for (const line of markdown.split('\n')) { + if (!line || line.startsWith('#') || !line.includes(':')) { + continue; + } + const idx = line.indexOf(':'); + const role = line.slice(0, idx).trim(); + const models = line.slice(idx + 1).split(',').map((s) => s.trim()).filter(Boolean); + if (!role || !models.length || !models.every((slug) => SLUG.test(slug))) { + continue; + } + roles.set(role, models); + } + return roles; +} + +function runLink(homeDir, envExtra = {}) { + return spawnSync('bash', [SCRIPT], { + encoding: 'utf8', + env: { ...process.env, HOME: homeDir, ...envExtra } + }); +} + +describe('pstack model sheet', () => { + test('every role uses a confirmed Cursor Task slug', () => { + const markdown = fs.readFileSync(SHEET, 'utf8'); + const roles = parseRoleLines(markdown); + + for (const role of REQUIRED_ROLES) { + expect(roles.has(role)).toBe(true); + } + + for (const models of roles.values()) { + for (const slug of models) { + expect(CURSOR_TASK_SLUGS.has(slug)).toBe(true); + } + } + }); +}); + +describe('link-agent-skills.sh pstack models', () => { + let tmpHome; + + beforeEach(() => { + tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'pstack-models-')); + }); + + afterEach(() => { + fs.rmSync(tmpHome, { recursive: true, force: true }); + }); + + test('copies the sheet and wires CLAUDE.md when CURSOR_AGENT=1', () => { + const result = runLink(tmpHome, { CURSOR_AGENT: '1' }); + expect(result.status).toBe(0); + + const dest = path.join(tmpHome, '.claude', 'pstack-models.md'); + const claudeMd = path.join(tmpHome, '.claude', 'CLAUDE.md'); + expect(fs.readFileSync(dest, 'utf8')).toBe(fs.readFileSync(SHEET, 'utf8')); + expect(fs.readFileSync(claudeMd, 'utf8').trim()).toBe(INCLUDE_LINE); + }); + + test('appends the include once when CLAUDE.md already has other text', () => { + const claudeDir = path.join(tmpHome, '.claude'); + fs.mkdirSync(claudeDir, { recursive: true }); + const claudeMd = path.join(claudeDir, 'CLAUDE.md'); + fs.writeFileSync(claudeMd, '# existing rules\n'); + + expect(runLink(tmpHome, { CURSOR_AGENT: '1' }).status).toBe(0); + expect(runLink(tmpHome, { CURSOR_AGENT: '1' }).status).toBe(0); + + const body = fs.readFileSync(claudeMd, 'utf8'); + expect(body.startsWith('# existing rules')).toBe(true); + expect(body.split(INCLUDE_LINE).length - 1).toBe(1); + }); + + test('leaves home pstack-models untouched when CURSOR_AGENT is unset', () => { + const result = runLink(tmpHome, { CURSOR_AGENT: '' }); + expect(result.status).toBe(0); + expect(fs.existsSync(path.join(tmpHome, '.claude', 'pstack-models.md'))).toBe(false); + }); +});