src/rubric.js (5140 bytes)
1 // How ideas are judged and which model builds them. Both triage paths (the MCP tool used by 2 // /ideas-go and /ideas-sort, and the headless `ideamine sort`) read this file. 3 4 import path from 'node:path'; 5 import { lane, listIdeas } from './store.js'; 6 import { clip } from './text.js'; 7 8 // Aliases are what Claude Code accepts (`claude --model sonnet`, Agent tool `model: "sonnet"`), 9 // so a recommendation stays valid when a new generation ships under the same alias. 10 // Prices: USD per million input/output tokens. 11 export const MODELS = [ 12 { 13 alias: 'haiku', 14 name: 'Claude Haiku 4.5', 15 price: '$1/$5', 16 use: 'mechanical, fully specified, local work: typos, renames, config tweaks, boilerplate, small scripts, lookups', 17 }, 18 { 19 alias: 'sonnet', 20 name: 'Claude Sonnet 5', 21 price: '$2/$10', 22 use: 'the default: ordinary features, bug fixes with a clear repro, tests, docs, contained refactors', 23 }, 24 { 25 alias: 'opus', 26 name: 'Claude Opus 5', 27 price: '$5/$25', 28 use: 'ambiguous or cross-cutting work: architecture, hard debugging, performance, security, unfamiliar domains', 29 }, 30 { 31 alias: 'fable', 32 name: 'Claude Fable 5.1', 33 price: '$10/$50', 34 use: 'only the hardest long-horizon or research-grade problems, where Opus would likely fail', 35 }, 36 ]; 37 38 export const RUBRIC = `Triage each idea. Be decisive: the user wants a short list worth building, not encouragement. 39 40 For every idea return: 41 - verdict: "do" (clear value, worth building soon) | "maybe" (unclear value, blocked, or needs a decision from the user) | "skip" (low value, already exists, or costs far more than it returns) 42 - impact: 1-5, how much it would matter to the user or project if it shipped 43 - size: xs (<15 min) | s (<1 h) | m (a few hours) | l (1-2 days) | xl (multi-day, should be split) 44 - model: the CHEAPEST model likely to finish it in one pass. A failed attempt on a weaker model plus a retry costs more than the right model once. 45 ${MODELS.map((m) => ` ${m.alias.padEnd(6)} ${m.price.padEnd(7)} ${m.use}`).join('\n')} 46 (prices in USD per million input/output tokens) 47 - title: imperative, at most 60 characters ("Add ...", "Fix ...") 48 - why: one sentence with the deciding reason 49 - brief: 1-4 sentences an agent can act on without this conversation: goal, scope, and "done when". Leave empty for skip. 50 - dup_of: id of an existing idea this one duplicates (use verdict "skip"), if any`; 51 52 // Only when there are projects to choose from. 53 const PAIRING = 54 '- project: the name (the word before the colon) of the project in "Projects on this machine" that the idea is ' + 55 'about. The user saves ideas from any chat, so the folder where an idea was saved often has nothing to do with it. ' + 56 '"" if no project fits, for example an idea for a new product.'; 57 58 /** JSON Schema for a batch of verdicts; used for `claude -p --json-schema` and the MCP tool. */ 59 export const VERDICT_SCHEMA = { 60 type: 'object', 61 properties: { 62 id: { type: 'integer' }, 63 verdict: { type: 'string', enum: ['do', 'maybe', 'skip'] }, 64 impact: { type: 'integer', minimum: 1, maximum: 5 }, 65 size: { type: 'string', enum: ['xs', 's', 'm', 'l', 'xl'] }, 66 model: { type: 'string', enum: MODELS.map((m) => m.alias) }, 67 title: { type: 'string' }, 68 why: { type: 'string' }, 69 brief: { type: 'string' }, 70 dup_of: { type: 'integer' }, 71 project: { type: 'string' }, 72 }, 73 required: ['id', 'verdict', 'impact', 'size', 'model', 'title', 'why', 'brief'], 74 }; 75 76 export const BATCH_SCHEMA = { 77 type: 'object', 78 properties: { verdicts: { type: 'array', items: VERDICT_SCHEMA } }, 79 required: ['verdicts'], 80 }; 81 82 /** Ideas waiting for a verdict: the inbox, or the given ids (re-triage). */ 83 export function pendingIdeas(db, { ids = null, limit = 30 } = {}) { 84 if (Array.isArray(ids) && ids.length) { 85 const wanted = new Set(ids.map(Number)); 86 return db.ideas.filter((i) => wanted.has(i.id)); 87 } 88 return listIdeas(db, { filter: 'inbox' }).reverse().slice(0, limit); // oldest first 89 } 90 91 /** The rubric plus the ideas to judge, as one prompt. `projects` (from knownProjects) turns on pairing. */ 92 export function triagePrompt(db, pending, projects = []) { 93 const pendingIds = new Set(pending.map((i) => i.id)); 94 const existing = db.ideas 95 .filter((i) => !pendingIds.has(i.id) && ['do', 'maybe', 'doing', 'done'].includes(lane(i))) 96 .slice(-60) 97 .map((i) => `#${i.id} ${clip(i.title, 70)} (${lane(i)})`); 98 const lines = [RUBRIC]; 99 if (projects.length) lines.push(PAIRING); 100 lines.push(''); 101 if (existing.length) lines.push('Existing ideas, for duplicate checks:', ...existing, ''); 102 if (projects.length) { 103 lines.push('Projects on this machine:', ...projects.map((p) => `${p.name}: ${p.dir}${p.about ? ` — ${p.about}` : ''}`), ''); 104 } 105 lines.push(`Ideas to triage (${pending.length}):`); 106 for (const i of pending) { 107 const ctx = [i.project && `project: ${path.basename(i.project)}`, i.tags?.length && `tags: ${i.tags.join(', ')}`] 108 .filter(Boolean) 109 .join('; '); 110 lines.push(`#${i.id}${ctx ? ` [${ctx}]` : ''}: ${i.text.replace(/\r?\n/g, ' / ')}`); 111 } 112 return lines.join('\n'); 113 }