Recently Written · git

ideamine

An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.

git clone https://github.com/equwal/ideamine

Log | Files | Refs


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 }