15 KiB
𝕿𝖍𝖊 𝕰𝖓𝖉 — The Fable Has Been Told
The complete build logic of Fablino: Anthropic-ecosystem-only multi-model fusion for fable-agent. Every changed line, every fix, the full multi-agent orchestration that built it, and the verified proof it works. One file. The whole fable.
Built 2026-06-14 on Opus 4.8 (1M) with Claude-only subagents — Sonnet drafted, Opus judged.
I. What Was Built
Fable-agent already existed: a 111-file TypeScript self-improving agent harness with a fusion module (panel of models → judge → synthesis) that didn't actually work and depended on a dead local proxy. This build made fusion real and 100% Anthropic:
- Reroute — every panelist + the judge now shell the local
claudeClaude Code CLI (desktop auth). No API key, no OpenRouter, no127.0.0.1:18901proxy. - New panel
sonnet-opus— Sonnet 4.6 + Opus 4.8 draft independently, Opus 4.8 judges. Now thefusion rundefault. - 4 bugs fixed, including the critical one that made fusion silently never fuse.
- 1 regression test guarding the critical fix (proven red-on-bug).
- DEP0190 warning killed.
II. The Journey (multi-agent build logic)
Each phase was deterministic multi-agent orchestration via the Workflow harness — Claude-only, Sonnet + Opus fused.
| Phase | Agents | What happened |
|---|---|---|
| Recon | 16 (9 Sonnet mappers → build-verify → 5 Opus critics → Opus synth) | Mapped all 111 files, verified the base build, surfaced 48 findings incl. the critical fuse() inversion → wrote BUILD.md. |
| Design | brainstorming | Locked the Anthropic-only reroute + bug-fix scope. User-approved. |
| De-risk | inline | Proved claude -p --model <m> returns PONG for Sonnet + Opus before building on it. |
| Build | 5 (3 Sonnet builders, disjoint files → Opus integration review → Opus build-fix loop) | All builders done; review pass; build green round 1; 50/50 tests. |
| Live-fix | inline | First fusion run failed (Windows .cmd shim) → rewrote helper (spawn+shell+stdin) → real fusion. |
| Guard | inline | Added fuse() regression test; proved RED on the restored bug, GREEN on the fix. |
| Polish | inline | Killed the DEP0190 warning (single-string command + model regex-guard). |
The two orchestration scripts (the literal build logic of phases Recon and Build) are saved at:
…/.claude/projects/…/workflows/scripts/fable-agent-reorchestrate-wf_c6359e7c-97b.js…/.claude/projects/…/workflows/scripts/fablino-anthropic-fusion-build-wf_4c3c77a2-5cb.js
Their structure: phase('Build') → parallel([builder1, builder2, builder3]) (Sonnet, disjoint
file ownership, shared contract) → phase('Review') Opus integration critic → phase('Verify')
while (round < 3) { build-fix agent; if green break }. Agent variety + loop-until-green.
III. THE BUILD LOGIC (exact final source)
1. src/core/claude-cli-caller.ts — the Anthropic-only call primitive (NEW, full)
/**
* Claude CLI Caller — shells out to `claude --print --model <model>` with the prompt on stdin.
*
* Uses Claude Code desktop auth; no API key required. The prompt is piped via stdin
* (not passed as an argument) so multi-line / quoted prompts need no escaping. The
* command is passed as a SINGLE string (not a program + args array) with `shell: true`
* so the shell resolves the `claude` / `claude.cmd` shim AND Node does not emit the
* DEP0190 warning (which only fires for an args array combined with shell:true). The
* only interpolated value is `model`, which is regex-guarded to a safe id below.
*/
import { spawn } from "node:child_process";
export async function callClaudeCli(model: string, prompt: string): Promise<string> {
return new Promise((resolve, reject) => {
// Guard: model ids come from FUSION_PANELS constants, but validate anyway so the
// single-string shell command below is injection-proof regardless of caller.
if (!/^[a-zA-Z0-9._-]+$/.test(model)) {
reject(new Error(`callClaudeCli: unsafe model id "${model}"`));
return;
}
const child = spawn(`claude --print --model ${model}`, {
shell: true,
timeout: 120000,
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => { stdout += d.toString(); });
child.stderr.on("data", (d) => { stderr += d.toString(); });
child.on("error", (err) => {
reject(new Error(`claude CLI spawn failed: ${err.message}`));
});
child.on("close", (code) => {
if (code === 0) {
resolve(stdout.trim());
} else {
reject(new Error(`claude CLI exited with code ${code}${stderr ? ` | stderr: ${stderr.slice(0, 400)}` : ""}`));
}
});
child.stdin.write(prompt);
child.stdin.end();
});
}
2. src/core/fusion-types.ts — the sonnet-opus panel (exact)
// added to the FusionPanelSlug union:
| "sonnet-opus" // Sonnet 4.6 + Opus 4.8 via Claude CLI (no proxy)
// added to FUSION_PANELS:
"sonnet-opus": {
slug: "sonnet-opus",
modelIds: ["claude-sonnet-4-6", "claude-opus-4-8"],
judgeModel: "claude-opus-4-8",
maxTokensPerPanelist: 8192,
panelistTimeoutMs: 120_000,
},
3. src/examples/executors/fusion-executor.ts — the fix + reroute (exact excerpts)
Constructor — judge override now applied:
constructor(config?: Partial<FusionExecutorConfig>) {
this.config = { panel: "opus4.8-4.8", verbose: false, ...config };
this.panel = {
...FUSION_PANELS[this.config.panel],
judgeModel: this.config.judgeModelOverride ?? FUSION_PANELS[this.config.panel].judgeModel,
};
this.safetyBoundary = this.config.safetyBoundary ?? new SafetyBoundary();
}
fuse() — THE CRITICAL FIX (synthesize on ≥2 answers, not on blindSpots):
async fuse(task: string, panelSlug?: FusionPanelSlug): Promise<FusionResult> {
const panel = panelSlug ? FUSION_PANELS[panelSlug] : this.panel;
const startTime = Date.now();
// 1. DRAFT — dispatch panelists in parallel
const panelResults = await this.dispatchPanel(task, panel);
// 2. CRITIQUE — analyze panelist responses
const analysis = this.analyzePanel(panelResults);
// 3. FUSE — synthesize final answer whenever >=2 panelists succeeded
const answered = panelResults.filter(p => p.raw && p.raw.length > 0 && !p.error);
const finalAnswer = answered.length > 1
? await this.synthesizeFinal(task, panelResults, analysis)
: (answered[0]?.raw ?? "No panelists returned results.");
return { task, panelSlug: panel.slug, panelSize: panel.modelIds.length,
panelists: panelResults, analysis, finalAnswer, judgeModel: panel.judgeModel,
totalDurationMs: Date.now() - startTime, error: null };
}
makeModelCall() — defaults to the claude CLI (no proxy in the default path):
private async makeModelCall(modelId: string, prompt: string, maxTokens: number): Promise<string> {
if (this.config.callModel) {
return this.config.callModel(modelId, prompt, maxTokens);
}
this.totalCalls++;
const prefix = `[Fusion call #${this.totalCalls} to ${modelId}]`;
if (this.config.verbose) console.error(`${prefix} sending ${prompt.length} chars…`);
if (modelId === "openrouter-fusion") return `${prefix} OpenRouter Fusion not available via CLI`;
try {
const result = await callClaudeCli(modelId, prompt); // ← Anthropic-only default
return result;
} catch (e) {
const msg = (e as Error).message;
console.error(`${prefix} ERROR: ${msg}`);
return `${prefix} Error: ${msg}`;
}
}
The bug, in one line: the old
fuse()hadanalysis.blindSpots.length > 0 ? synthesize : panelResults[0].raw. BecauseblindSpotsis empty when all panelists succeed, the judge ran only on failure — the happy path silently returned panelist #0's raw draft. The default panel never fused.
4. src/tier2-primitives/loops/state-accumulator.ts — resetSession (exact)
/**
* Reset in-memory state and switch to a new sessionId so subsequent
* record() calls persist to a fresh log file, not the previous run's.
* The old persisted log is untouched (load() can still read it by id).
*/
resetSession(newSessionId: string): void {
this.iterations = [];
this.sessionId = newSessionId;
}
5. src/tier2-primitives/loops/feedback-loop.ts — per-run rotation + single convergence (exact)
// fields added:
private store: StateStore | undefined;
private baseSessionId: string | undefined;
async run(task: string, executor: PhaseExecutor): Promise<LoopResult> {
// Rotate to a fresh per-run session so each run persists to its own log.
const runId = StateStore.uid();
const runSessionId = this.baseSessionId ? `${this.baseSessionId}-${runId}` : runId;
this.accumulator.resetSession(runSessionId);
const startTime = Date.now();
let iterationNumber = 0;
while (iterationNumber < this.maxIterations) {
iterationNumber++;
const iteration = await this.executeIteration(iterationNumber, task, executor);
// Record, THEN compute the authoritative convergence verdict (once).
this.accumulator.record(iteration);
const verdict = this.convergence.evaluate(this.accumulator, this.maxIterations);
// Stamp the phase now that we have the real verdict.
iteration.phase = verdict.converged ? "converged" : "refine";
await this.fireHooks("onIteration", iteration, iterationNumber);
if (verdict.converged) { /* … return converged result … */ }
// …
}
}
6. src/index.ts — the fusion command wiring (exact)
fusion
.command("run <task>")
.description("Run a task through a fusion panel of models")
.option("-p, --panel <slug>", "Panel: sonnet-opus, opus4.8-4.8, …", "sonnet-opus") // ← default
.option("-j, --judge <model>", "Override judge model")
.option("--openrouter", "Use OpenRouter Fusion API instead of local panel dispatch")
.action(async (task, opts) => {
if (opts.openrouter) { /* … unchanged OpenRouterFusionExecutor path … */ return; }
const { FusionExecutor } = await import("./examples/executors/fusion-executor.js");
const { callClaudeCli } = await import("./core/claude-cli-caller.js");
const executor = new FusionExecutor({
panel: (opts.panel as any) ?? "sonnet-opus",
judgeModelOverride: opts.judge,
callModel: (model, prompt) => callClaudeCli(model, prompt), // ← Anthropic-only injection
verbose: true,
});
const result = await executor.fuse(task, opts.panel as any);
// …print result…
});
7. src/examples/executors/fusion-executor.test.ts — the regression guard (NEW, full)
import { describe, it, expect } from "vitest";
import { FusionExecutor } from "./fusion-executor.js";
const JUDGE_SENTINEL = "FUSED_SYNTHESIS_OUTPUT";
function makeMockCallModel(draftFor: (modelId: string) => string) {
const calls: Array<{ modelId: string; isJudge: boolean }> = [];
const callModel = async (modelId: string, prompt: string, _maxTokens: number): Promise<string> => {
const isJudge = prompt.includes("You are the judge");
calls.push({ modelId, isJudge });
return isJudge ? JUDGE_SENTINEL : draftFor(modelId);
};
return { callModel, calls };
}
describe("FusionExecutor.fuse() — judge synthesis", () => {
it("runs the judge and returns its synthesis when >=2 panelists answer", async () => {
const { callModel, calls } = makeMockCallModel((id) => `draft-from-${id}`);
const fusion = new FusionExecutor({ panel: "sonnet-opus", callModel });
const result = await fusion.fuse("why is the sky blue?");
expect(calls.filter((c) => c.isJudge)).toHaveLength(1);
expect(result.finalAnswer).toBe(JUDGE_SENTINEL);
expect(result.finalAnswer).not.toContain("draft-from-");
});
it("does NOT run the judge when fewer than 2 panelists answer", async () => {
const { callModel, calls } = makeMockCallModel((id) =>
id === "claude-sonnet-4-6" ? "" : `draft-from-${id}`);
const fusion = new FusionExecutor({ panel: "sonnet-opus", callModel });
const result = await fusion.fuse("why is the sky blue?");
expect(calls.filter((c) => c.isJudge)).toHaveLength(0);
expect(result.finalAnswer).toBe("draft-from-claude-opus-4-8");
});
});
IV. The Bugs & Fixes
| # | Bug | Severity | Fix |
|---|---|---|---|
| 1 | fuse() gated synthesis on blindSpots → judge never ran on success → returned panelist #0's raw draft |
🔴 critical | Synthesize when answered.length > 1 |
| 2 | StateAccumulator.reset() left store/sessionId → runs bled into one log |
🟠 high | resetSession(newId) + per-run rotation in run() |
| 3 | executeIteration computed phase from stale state + double evaluate() |
🟠 high | Record first, evaluate once, stamp phase from real verdict |
| 4 | -j/--judge flag accepted but never applied |
🟡 minor | Apply judgeModelOverride in constructor |
| 5 | execFile("claude") couldn't launch the Windows .cmd shim → instant fail |
🟠 live | spawn + shell:true + prompt via stdin |
| 6 | DEP0190 warning (args-array + shell) | 🟡 cosmetic | Single-string command + model regex guard |
V. Rebuild & Verify
cd "C:/Users/117tr/OneDrive/Desktop/fable-agent-master/fable-agent"
npm install # first time only
npm run build # tsc → dist/ (expect zero output, zero errors)
node dist/index.js fusion panels # sonnet-opus listed
node dist/index.js fusion run "<your hard question>" # Sonnet+Opus → Opus judge, all via claude CLI
npm test # 8 files / 52 tests
Only prerequisite for the Anthropic-only path: the claude CLI on PATH, signed into Claude Code.
VI. Verified Proof (ground truth, 2026-06-14)
npm run build → tsc clean, ZERO type errors
npm test → Test Files 8 passed (8) | Tests 52 passed (52) | vitest v4.1.8
fusion run → Panelist 1 (claude-sonnet-4-6): ✓ ~14s
Panelist 2 (claude-opus-4-8): ✓ ~15s
Final Answer: <Opus judge's structured synthesis> | no DEP0190 lines
regression → RED (2 fail) when the old blindSpots gating restored; GREEN on the fix
Companion docs (same folder / Desktop): BUILD.md (full architecture + 48-finding audit),
fablino-longest-horizon-ever-build.md (the reroute build log), demo 1 fablino.md (live demo
script).
VII. The Moral
Green tests lied twice. The build compiled and 50/50 tests passed while fusion was doubly broken —
the blindSpots inversion and the Windows shim. Neither is reachable without running the real
shell-out, and no test did. The multi-agent audit caught the logic bug; only a live fusion run
caught the shim. Tests prove units behave. Only running the system proves it works. Now there is
a test for the logic bug too — and it goes red if anyone forgets.
Sonnet drafted. Opus judged. Opus 4.8 (1M) orchestrated. No key left the Anthropic ecosystem.
The end. The fable has been told. 🔒