/** * Spine factory: turns a declarative `Spine` (a Han skill expressed as data) * into a runnable `Flow`. The shape is the one ~most Han skills share — * * angle₁ ─┐ * angle₂ ─┼─▶ fold (code) ─▶ [synthesizer] ─▶ adversarial gate ─▶ render * angle₃ ─┘ (fan-in) (optional) (validator) * * — so new skills are added as config (flows/*.ts), not new code. Band gating * selects how many angles fan out (small = core only; large = all). Skills * with a genuinely different shape (code-review's per-finding verify pipeline) * get a bespoke Flow instead of a Spine. */ import type { Band, Flow, Spine, Step, StepContext } from './types.js'; import { produceContract, reviewContract, type Contract } from './contracts.js'; import { slugify } from './render.js'; const BAND_ORDER: Record = { small: 0, medium: 1, large: 2 }; export function readBand(input: StepContext['input']): Band { const b = input.band; return typeof b === 'string' && b in BAND_ORDER ? (b as Band) : 'small'; } function bandAtLeast(ctx: StepContext, min: Band = 'small'): boolean { return BAND_ORDER[readBand(ctx.input)] >= BAND_ORDER[min]; } /** Appended to every worker when --fast is set — caps the slow tool loop. */ export function fastNote(ctx: StepContext): string { if (!ctx.input.concise) return ''; return '\n\nFAST MODE — optimise for speed over exhaustiveness: limit external/tool calls to the few that matter, cite only decisive evidence, keep every section short, return quickly.'; } interface ResolvedGate { agent: string; label: string; task: (ctx: StepContext) => string; } /** The adversarial gate, built with the Han review checklists for the spine's contracts. */ function defaultValidator(contracts: Contract[]): ResolvedGate { return { agent: 'adversarial-validator', label: 'Validation (adversarial-validator)', task: (ctx) => [ `Adversarially validate the analysis below, for: "${String(ctx.input.question)}".`, 'Attack the evidence, the framing, the conclusion, and the integrity of how the evidence was gathered.', 'Emit findings as V1, V2, … each with a severity and whether it changes the conclusion.', reviewContract(contracts).trim(), 'End with, in this order: a one-line VERDICT (does the conclusion survive?); a plain-language SUMMARY (2–3 sentences, no jargon or IDs); and a CONFIDENCE rating on its own line — `Confidence: High | Medium | Low`.', '', '----- ANALYSIS TO ATTACK -----', ctx.results.synthesis ?? ctx.results.fold ?? '', ].join('\n'), }; } export function buildSpineFlow(spine: Spine): Flow { const contracts: Contract[] = spine.contracts ?? ['evidence']; const validator: ResolvedGate = spine.validator ?? defaultValidator(contracts); const angleIds = spine.angles.map((a) => a.id); const steps: Step[] = []; for (const angle of spine.angles) { steps.push({ id: angle.id, kind: 'agent', agent: angle.agent, when: (ctx) => bandAtLeast(ctx, angle.minBand) && (angle.when ? angle.when(ctx) : true), run: (ctx) => angle.task(ctx) + produceContract(contracts) + fastNote(ctx), }); } // Code fold: concatenate whatever angles produced (skipped angles absent). steps.push({ id: 'fold', kind: 'code', deps: angleIds, run: (ctx) => foldAngles(spine, ctx), }); if (spine.synthesizer) { steps.push({ id: 'synthesis', kind: 'agent', agent: spine.synthesizer.agent, deps: ['fold'], run: (ctx) => spine.synthesizer!.task(ctx) + produceContract(contracts) + fastNote(ctx), }); } steps.push({ id: 'validation', kind: 'agent', agent: validator.agent, deps: [spine.synthesizer ? 'synthesis' : 'fold'], run: (ctx) => validator.task(ctx) + fastNote(ctx), }); return { name: spine.name, description: spine.description, steps, render: (ctx) => renderSpine(spine, validator, contracts, ctx), output: (ctx) => `conductor-report-${spine.name}-${slugify(String(ctx.input.question))}.md`, }; } function foldAngles(spine: Spine, ctx: StepContext): string { const blocks: string[] = []; for (const angle of spine.angles) { const out = ctx.results[angle.id]; if (out) blocks.push(`### ${angle.label}\n\n${out}`); } return blocks.join('\n\n') || '_(no angle produced output)_'; } function renderSpine(spine: Spine, validator: ResolvedGate, contracts: Contract[], ctx: StepContext): string { const question = String(ctx.input.question ?? ''); // model is injected by the flow-runner from flow_runs.model — no env var fallback const model = ctx.model ?? 'llama-swap/qwen3.6-35b-a3b-mxfp4'; const band = readBand(ctx.input); const chain: string[] = []; const rules = [ contracts.includes('evidence') ? 'evidence-rule (trust classes · single-source web gate · no-evidence labeling)' : '', contracts.includes('yagni') ? 'YAGNI gate' : '', ] .filter(Boolean) .join(' · '); const parts: string[] = [ `# Conductor Report — ${spine.name}: ${question}`, `> BooCode code conductor · band=${band}${ctx.input.concise ? ' · fast' : ''} · workers on \`${model}\`. Sequencing, fan-out, and fold are deterministic code; each agent ran as a bounded single-task worker. Han rules applied: ${rules}. The plain-language summary, the **Confidence** rating, and any \`## Deferred (YAGNI)\` items are in the **Validation** section — read it before trusting the conclusion.`, ]; for (const angle of spine.angles) { if (ctx.results[angle.id]) { parts.push(`## ${angle.label}\n\n${ctx.results[angle.id]}`); chain.push(angle.agent); } } if (spine.synthesizer && ctx.results.synthesis) { parts.push(`## ${spine.synthesizer.label}\n\n${ctx.results.synthesis}`); chain.push(spine.synthesizer.agent); } parts.push(`## ${validator.label}\n\n${ctx.results.validation ?? '_no validation output_'}`); chain.push(validator.agent); parts.push( `---\n\n_Conducted by the code conductor: ${chain.join(' → ')}. Band=${band}. The conductor chose every step and passed full outputs forward; no model decided the sequence._`, ); return parts.join('\n\n') + '\n'; }