Files
gbrain/src/commands/brainstorm.ts
T
39e14cd50e v0.37.1.0 feat: brainstorm + lsd — bisociation idea generator grounded in your own brain (#1214)
* feat: brainstorm + lsd (v0.37 wave, pre-merge snapshot)

Brainstorm + LSD bisociation idea generator. Will rebase + bump after master merge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: CHANGELOG voice tightening — strip meta-content from v0.37.1.0 entry

CLAUDE.md gains an IRON RULE for CHANGELOG entries: the changelog describes
what the user gets, not how the work happened. No mentions of review
processes, plan files, decision tags, migration version drama, or 'what we
caught and fixed before merging.' If a fact only exists because of the
development workflow, it does not belong in release notes.

Rewrite v0.37.1.0 entry to comply: cut the 'what we caught' section
(architectural review drama), the 'plan + reviews' bullet, and the
migration-renumbering aside. Entry shrinks 67 → 56 lines, every sentence
now answers 'what can I do / how do I use it / what should I watch for.'

Regenerated llms-full.txt to absorb the CLAUDE.md voice update.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 22:32:52 -07:00

231 lines
8.1 KiB
TypeScript

/**
* gbrain brainstorm — bisociation-style idea generation grounded in your
* own notes.
*
* v0.37.0 wave (D14 + D6 + D11 + D12). Pulls a small close-set via
* hybridSearch, a far-set via prefix-stratified domain-bank, crosses them
* via gateway.chat, and judges via the shared 5-axis rubric. Output cites
* close + far slugs with a 0-1 distance score (D6 transparency).
*
* For "Lateral Synaptic Drift" — the inverted-judge / stale-bias variant —
* see `src/commands/lsd.ts` which calls the same orchestrator with the
* LSD_PROFILE config object.
*/
import type { BrainEngine } from '../core/engine.ts';
import {
runBrainstorm,
formatBrainstormMarkdown,
buildBrainstormFrontmatter,
BRAINSTORM_PROFILE,
LSD_PROFILE,
type BrainstormProfile,
} from '../core/brainstorm/orchestrator.ts';
import { loadConfig } from '../core/config.ts';
export interface BrainstormCliArgs {
question?: string;
json: boolean;
save?: boolean;
yes: boolean;
limit?: number;
help: boolean;
error?: string;
}
export function parseBrainstormArgs(args: string[]): BrainstormCliArgs {
const out: BrainstormCliArgs = { json: false, yes: false, help: false };
const positional: string[] = [];
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === '--help' || arg === '-h') {
out.help = true;
} else if (arg === '--json') {
out.json = true;
} else if (arg === '--save') {
out.save = true;
} else if (arg === '--no-save') {
out.save = false;
} else if (arg === '--yes' || arg === '-y') {
out.yes = true;
} else if (arg === '--limit') {
const v = args[++i];
const n = v ? parseInt(v, 10) : NaN;
if (!Number.isFinite(n) || n <= 0) {
out.error = `--limit requires a positive integer (got ${v})`;
return out;
}
out.limit = n;
} else if (arg.startsWith('--')) {
out.error = `unknown flag: ${arg}`;
return out;
} else {
positional.push(arg);
}
i++;
}
if (positional.length > 0) {
out.question = positional.join(' ');
}
return out;
}
const BRAINSTORM_HELP = `Usage: gbrain brainstorm <question> [options]
Bisociation idea generator grounded in your own notes. Pulls a close-set
via hybrid search and a far-set via prefix-stratified domain-bank, crosses
them, judges with a 5-axis rubric. Output cites close + far slugs with a
0-1 distance score so you can see how far each collision actually traveled.
Options:
--json Emit BrainstormResult as JSON (for agents)
--save Save to wiki/ideas/<date>-brainstorm-<slug>.md (default ON)
--no-save Don't save; print only
--yes, -y Skip the 10s cost-preview wait (TTY only)
--limit N Override the far-bank size (default 6 brainstorm / 12 LSD)
--help, -h Show this help
Examples:
gbrain brainstorm "why are AI coding tools converging on the same UX?"
gbrain brainstorm "what's the real bottleneck on lab automation" --json
Cost: ~$0.05-0.15 per run. Set GBRAIN_NO_BRAINSTORM_PREVIEW=1 or pass --yes
to skip the TTY grace window in scripted callers.
See also: gbrain lsd — Lateral Synaptic Drift, the inverted-judge variant
that prefers forgotten pages and rejects ideas that are "too obvious."
`;
const LSD_HELP = `Usage: gbrain lsd <question> [options]
LSD = Lateral Synaptic Drift. Same bisociation engine as \`gbrain brainstorm\`
with the distance dial maxed: bigger far-bank (12 pages), smaller close-set
(2 pages), forgotten pages preferred via the stale-bias signal, inverted
judge that REJECTS ideas scoring too high on coherence ("too obvious — you'd
have thought of this without LSD"), every idea must invert at least one
implicit axiom. Output is ephemeral by default — pass --save if an idea lands.
Options:
--json Emit BrainstormResult as JSON
--save Persist to wiki/ideas/<date>-lsd-<slug>.md (default OFF)
--yes, -y Skip the 10s cost-preview wait (TTY only)
--limit N Override the far-bank size (default 12)
--help, -h Show this help
Examples:
gbrain lsd "why are AI coding tools converging on the same UX?"
gbrain lsd "the unspoken assumption in venture pricing" --save
Cost: ~$0.20-0.40 per run.
See also: gbrain brainstorm — the sober, cite-heavy default variant.
`;
/** Shared body: brainstorm.ts → runBrainstormCli(BRAINSTORM_PROFILE); lsd.ts → runBrainstormCli(LSD_PROFILE). */
async function runBrainstormCli(
engine: BrainEngine,
args: string[],
profile: BrainstormProfile,
help: string,
): Promise<void> {
const parsed = parseBrainstormArgs(args);
if (parsed.help) {
console.log(help);
return;
}
if (parsed.error) {
console.error(`gbrain ${profile.label}: ${parsed.error}`);
console.error(help);
process.exit(2);
return;
}
if (!parsed.question || parsed.question.trim().length === 0) {
console.error(`gbrain ${profile.label}: question required`);
console.error(help);
process.exit(2);
return;
}
const config = loadConfig() ?? {};
// Honor env-var skip for scripted environments that can't easily pass --yes.
const skipPreview = parsed.yes || process.env.GBRAIN_NO_BRAINSTORM_PREVIEW === '1';
// --limit override: replace m_far on a shallow copy of the profile.
const effectiveProfile: BrainstormProfile = parsed.limit
? { ...profile, m_far: parsed.limit }
: profile;
const result = await runBrainstorm(engine, config, {
question: parsed.question,
profile: effectiveProfile,
skipCostPreview: skipPreview,
});
if (parsed.json) {
console.log(JSON.stringify(result, null, 2));
return;
}
// Human-readable: render via formatBrainstormMarkdown. `onlyPassed: true`
// by default — passes-only output is the headline; failed ideas live in
// the JSON / saved page for triage.
const md = formatBrainstormMarkdown(result, { onlyPassed: true, includeMeta: true });
console.log(md);
// Save policy: brainstorm defaults to save-on; lsd defaults to save-off.
// CLI --save / --no-save overrides the default.
const shouldSave = parsed.save ?? profile.default_save;
if (shouldSave) {
const slug = buildIdeaSlug(parsed.question, profile.label);
const frontmatter = buildBrainstormFrontmatter(result, { slug });
// Re-render content for save: include filtered ideas too so --retry-judge
// (when implemented) has the full set to re-score.
const body = formatBrainstormMarkdown(result, { onlyPassed: false, includeMeta: true });
const content = frontmatter + body;
try {
await engine.putPage(slug, {
title: `${profile.label === 'lsd' ? 'LSD' : 'Brainstorm'}: ${parsed.question.slice(0, 100)}`,
type: 'note',
compiled_truth: content,
frontmatter: {
mode: profile.frontmatter_mode,
generated_at: new Date().toISOString(),
question: parsed.question,
judge_failed: result.judge_failed,
unscored: result.judge_failed,
close_slugs: result.close_set.map((c) => c.slug),
far_slugs: result.far_set.map((f) => f.slug),
},
timeline: '',
});
console.log(`\n_Saved to \`${slug}\`._`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`gbrain ${profile.label}: save failed: ${msg}`);
}
}
}
/** Slugify the question for the saved page path. Capped + collision-resistant via date prefix. */
function buildIdeaSlug(question: string, label: 'brainstorm' | 'lsd'): string {
const date = new Date().toISOString().slice(0, 10);
const stem = question
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 60)
.replace(/^-+|-+$/g, '');
return `wiki/ideas/${date}-${label}-${stem || 'untitled'}`;
}
/** CLI entry: `gbrain brainstorm`. */
export async function runBrainstormCommand(engine: BrainEngine, args: string[]): Promise<void> {
return runBrainstormCli(engine, args, BRAINSTORM_PROFILE, BRAINSTORM_HELP);
}
/** CLI entry: `gbrain lsd`. */
export async function runLsdCommand(engine: BrainEngine, args: string[]): Promise<void> {
return runBrainstormCli(engine, args, LSD_PROFILE, LSD_HELP);
}