From 9c1400d73878b9091108af8058cdb9f6b52a35c5 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 20:27:12 -0500 Subject: [PATCH] =?UTF-8?q?Phase=2022=20=E2=80=94=20Internal=20Knowledge?= =?UTF-8?q?=20Library=20(KB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Meta-layer over Phase 19 playbook_memory. Phase 19 answers "which WORKERS worked for this event"; KB answers "which CONFIG worked for this playbook signature" — model choice, budget hints, pathway notes, error corrections. tests/multi-agent/kb.ts: - computeSignature(): stable sha256 hash of the (kind, role, count, city, state) tuple sequence. Same scenario shape → same sig. - indexRun(): extracts sig, embeds spec digest via sidecar, appends outcome record, upserts signature to data/_kb/signatures.jsonl. - findNeighbors(): cosine-ranks the k most-similar signatures from prior runs for a target spec. - detectErrorCorrections(): scans outcomes for same-sig fail→succeed pairs, diffs the model set, logs to error_corrections.jsonl. - recommendFor(): feeds target digest + k-NN neighbors + recent corrections to the overview model, gets back a structured JSON recommendation (top_models, budget_hints, pathway_notes), appends to pathway_recommendations.jsonl. JSON-shape constrained so the executor can inherit it mechanically. - loadRecommendation(): at scenario start, pulls newest rec matching this sig (or nearest). scenario.ts: - Reads KB recommendation at startup (alongside prior lessons). - Injects pathway_notes into guidanceFor() executor context. - After retrospective, indexes the run + synthesizes next rec. Cold-start behavior: first run with no history writes a low-confidence "no prior data" rec so the signal that something was attempted is captured. Second run gets "low confidence, 0 neighbors" until a third distinct sig gives the embedder something to compare against — hence the upcoming scenario generator. VERIFIED: - data/_kb/ populated after one scenario run: 1 outcome (sig=4674…, 4/5 ok, 16 turns total), 1 signature, 2 recs (cold + post-run). - Recommendation JSON-parsed cleanly from gpt-oss:20b overview model. PRD Phase 22 added with file layout, cycle description, and the rationale for file-based MVP → Rust port progression that matches how Phase 21 primitives shipped. What's NOT here yet (batched follow-ups per J's request, tested between each): - Lift the k=10 hybrid_search cap to adaptive k=max(count*5, 20) - Scenario generator to bulk-populate KB with varied signatures - Rust re-weighting: push playbook_memory success signal INTO hybrid_search scoring, not just post-hoc boost --- docs/PRD.md | 34 ++- tests/multi-agent/kb.ts | 448 ++++++++++++++++++++++++++++++++++ tests/multi-agent/scenario.ts | 49 +++- 3 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 tests/multi-agent/kb.ts diff --git a/docs/PRD.md b/docs/PRD.md index 3f93509..ab6e3aa 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -421,7 +421,39 @@ T3 checkpoints + cross-day lessons are wired. Lessons archive to `data/_playbook **Status:** TS primitives WIRED. Rust port pending. The escalation path (tree split → bigger-context cloud model → kimi-k2:1t's 1M window → split decision into sub-decisions) is declared in `config/models.json` under `context_management.overflow_policies`. -### Phase 22+: Further horizon +### Phase 22: Internal Knowledge Library (KB) + +Meta-layer over Phase 19 playbook_memory. Playbook memory answers "which WORKERS worked for this event." The KB answers "which CONFIG worked for this playbook signature." Subject changes from workers to the system itself — model choice, budget hints, overflow policies, pathway notes. + +**Files (`data/_kb/`):** +- `signatures.jsonl` — (sig_hash, embedding[], first_seen, last_seen, run_count). Sig = stable hash of the sequence of (kind, role, count, city, state) across events. +- `outcomes.jsonl` — per-run record: {sig, run_id, models, ok/total, turns, citations, per-event summary, elapsed}. +- `pathway_recommendations.jsonl` — AI-synthesized for next run: {confidence, rationale, top_models, budget_hints, pathway_notes, neighbors_consulted}. +- `error_corrections.jsonl` — detected fail→succeed pairs on same sig, diff of what changed. +- `config_snapshots.jsonl` — history of models.json changes + why. + +**Cycle (event-driven, not wall-clock):** +1. Scenario ends → `kb.indexRun()` extracts signature, embeds spec digest, appends outcome. +2. `kb.recommendFor(nextSpec)` finds k-NN signatures via cosine, feeds their outcome history + recent error corrections to the overview model, writes a structured recommendation. +3. Next scenario starts → `kb.loadRecommendation(spec)` pulls the newest rec for this sig, injects `pathway_notes` into `guidanceFor()` alongside prior lessons. + +**Why file-based for MVP:** Phase 19 playbook_memory is already a catalogd dataset. KB is a separate meta-layer; keep it file-based first to iterate without a gateway schema migration. Rust port (and promotion to vectord-indexed corpus for neighbor search at scale) lands once shape stabilizes — mirrors how Phase 21 primitives were TS-first → Rust next sprint. + +**What the overview model gets asked:** +- Target scenario digest +- Top-k neighbor signatures with avg ok rate, best model combo per neighbor +- Recent error corrections (sig, before/after model set) + +**What it outputs (JSON-constrained):** +- confidence (high/medium/low) +- rationale (2-3 sentences) +- top_models {executor, reviewer, overview} +- budget_hints {executor_max_tokens, reviewer_max_tokens, executor_think} +- pathway_notes (concrete pre-run advice) + +**Status (WIRED 2026-04-21):** `tests/multi-agent/kb.ts` holds all primitives. scenario.ts reads rec at start, indexes + recommends at end. Cold start gracefully writes a "low confidence, no history" rec so the second run has a floor to build on. + +### Phase 23+: Further horizon - Specialized fine-tuned models per domain (staffing matcher, resume parser) - Video/audio transcript ingest + multimodal embeddings diff --git a/tests/multi-agent/kb.ts b/tests/multi-agent/kb.ts new file mode 100644 index 0000000..219ade3 --- /dev/null +++ b/tests/multi-agent/kb.ts @@ -0,0 +1,448 @@ +// Phase 22 — Internal Knowledge Library. +// +// Sits on top of the successful-playbook store. Tracks which +// configurations produced which outcomes for which playbook +// signatures, detects fail→succeed error corrections across runs, +// and emits pathway recommendations that the NEXT scenario reads +// at startup. +// +// Event-driven cycle (not wall-clock polling): +// scenario ends → kb.indexRun() → kb.recommendFor(nextSpec) → next +// scenario reads top rec and applies. +// +// File layout under data/_kb/: +// signatures.jsonl — (sig_hash, embedding[], first_seen, last_seen, run_count) +// outcomes.jsonl — append-only per-run: {sig, run_id, models, pathway, ok_events, elapsed_s, errors[]} +// config_snapshots.jsonl — config/models.json hash + env at each ingest +// error_corrections.jsonl — fail→succeed deltas +// pathway_recommendations.jsonl — AI-synthesized suggestions +// +// Why file-based: Phase 19 playbook_memory is already in the +// catalogd+vectord stack. This is a separate meta-layer — keeping +// it file-based first lets us iterate quickly without a gateway +// schema migration. Rust port lands once the shape stabilizes +// (mirrors how Phase 21 primitives are TS-first → Rust next sprint). + +import { mkdir, readFile, writeFile, appendFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { createHash } from "node:crypto"; +import { SIDECAR, generateContinuable } from "./agent.ts"; + +const KB_DIR = "data/_kb"; +const SIGNATURES_FILE = "signatures.jsonl"; +const OUTCOMES_FILE = "outcomes.jsonl"; +const CONFIG_SNAPSHOTS_FILE = "config_snapshots.jsonl"; +const ERROR_CORRECTIONS_FILE = "error_corrections.jsonl"; +const RECOMMENDATIONS_FILE = "pathway_recommendations.jsonl"; + +// What a playbook signature looks like — stable hash of the scenario +// shape that ignores timestamps and specific worker IDs. Two runs with +// the same sequence of (kind, role, count, city, state) get the same +// hash. This is the retrieval key for the KB. +export interface PlaybookSignature { + sig_hash: string; + client: string; + events_digest: string; // human-readable event summary + embedding: number[]; // sidecar embed, for nearest-neighbor + first_seen: string; + last_seen: string; + run_count: number; +} + +// What we record after a run completes. +export interface RunOutcome { + sig_hash: string; + run_id: string; // scenario dir basename + date: string; + models: { + executor: string; + reviewer: string; + overview: string; + overview_cloud: boolean; + }; + ok_events: number; + total_events: number; + total_turns: number; + total_gap_signals: number; + total_citations: number; + per_event: Array<{ + at: string; + kind: string; + role: string; + count: number; + ok: boolean; + turns: number; + error: string | null; + }>; + elapsed_secs: number; + created_at: string; +} + +// The AI-synthesized recommendation written back for future runs. +export interface PathwayRecommendation { + sig_hash: string; + generated_at: string; + generated_by_model: string; + confidence: "high" | "medium" | "low"; + rationale: string; // prose from the recommender + top_models: { // suggested tier assignments + executor?: string; + reviewer?: string; + overview?: string; + }; + budget_hints: { // suggested max_tokens / think flags + executor_max_tokens?: number; + reviewer_max_tokens?: number; + executor_think?: boolean; + }; + pathway_notes: string; // "pre-fetch cert data", "use buffer of 3+" + neighbors_consulted: string[]; // sig_hashes of the playbooks that shaped this rec +} + +// Compute a stable hash from the scenario spec. Two runs with the same +// events list (ignoring SMS drafts / timestamps / exclude lists) get +// the same hash. +export function computeSignature(spec: { + client: string; + events: Array<{ kind: string; role: string; count: number; city: string; state: string }>; +}): string { + const canonical = JSON.stringify({ + client: spec.client, + events: spec.events.map(e => ({ + kind: e.kind, role: e.role, count: e.count, city: e.city, state: e.state, + })), + }); + return createHash("sha256").update(canonical).digest("hex").slice(0, 16); +} + +// Human-readable digest of the spec, used in recommender prompts. +export function specDigest(spec: { + client: string; + events: Array<{ kind: string; role: string; count: number; city: string; state: string }>; +}): string { + return `${spec.client}: ` + spec.events.map(e => + `${e.kind}/${e.role}×${e.count} in ${e.city},${e.state}` + ).join(" | "); +} + +async function ensureKb(): Promise { + await mkdir(KB_DIR, { recursive: true }); +} + +async function readJsonl(file: string): Promise { + try { + const raw = await readFile(join(KB_DIR, file), "utf8"); + return raw.split("\n").filter(l => l.trim()).map(l => JSON.parse(l) as T); + } catch { + return []; + } +} + +// Index a completed scenario run. Extracts signature, computes +// embedding, appends outcome, updates or inserts signature entry. +export async function indexRun( + scenarioDir: string, + spec: { client: string; date: string; events: Array }, + models: { executor: string; reviewer: string; overview: string; overview_cloud: boolean }, + elapsed_secs: number, +): Promise<{ sig_hash: string; outcome: RunOutcome }> { + await ensureKb(); + + const sig_hash = computeSignature(spec); + const digest = specDigest(spec); + + // Read results.json produced by scenario.ts to build the outcome record. + const resultsRaw = await readFile(join(scenarioDir, "results.json"), "utf8"); + const results = JSON.parse(resultsRaw) as any[]; + const outcome: RunOutcome = { + sig_hash, + run_id: scenarioDir.split("/").pop() ?? scenarioDir, + date: spec.date, + models, + ok_events: results.filter(r => r.ok).length, + total_events: results.length, + total_turns: results.reduce((s, r) => s + (r.turns ?? 0), 0), + total_gap_signals: results.reduce((s, r) => s + (r.gap_signals?.length ?? 0), 0), + total_citations: results.reduce((s, r) => s + (r.playbook_citations?.length ?? 0), 0), + per_event: results.map(r => ({ + at: r.event.at, + kind: r.event.kind, + role: r.event.role, + count: r.event.count, + ok: r.ok, + turns: r.turns ?? 0, + error: r.error ?? null, + })), + elapsed_secs, + created_at: new Date().toISOString(), + }; + await appendFile(join(KB_DIR, OUTCOMES_FILE), JSON.stringify(outcome) + "\n"); + + // Embed + upsert signature. Skip if embedding fails — the outcome is + // still persisted, just without neighbor-search hookup. + try { + const resp = await fetch(`${SIDECAR}/embed`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ texts: [digest] }), + }); + if (resp.ok) { + const data: any = await resp.json(); + const embedding: number[] = data.embeddings?.[0] ?? []; + const existing = await readJsonl(SIGNATURES_FILE); + const now = new Date().toISOString(); + const found = existing.find(s => s.sig_hash === sig_hash); + if (found) { + found.last_seen = now; + found.run_count += 1; + } else { + existing.push({ + sig_hash, + client: spec.client, + events_digest: digest, + embedding, + first_seen: now, + last_seen: now, + run_count: 1, + }); + } + // Rewrite the whole file — cheap while KB is small; swap to + // append-with-periodic-compact when row count exceeds ~10k. + await writeFile( + join(KB_DIR, SIGNATURES_FILE), + existing.map(s => JSON.stringify(s)).join("\n") + "\n", + ); + } + } catch { + // Signature indexing failed — outcome is still captured. Next + // indexRun retries. + } + + return { sig_hash, outcome }; +} + +// Cosine similarity for neighbor lookup. Float32-in-memory is fine for +// O(thousands) signatures; swap to vectord HNSW once the corpus grows. +function cosine(a: number[], b: number[]): number { + if (a.length === 0 || a.length !== b.length) return 0; + let dot = 0, na = 0, nb = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; + na += a[i] * a[i]; + nb += b[i] * b[i]; + } + return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-12); +} + +// Find the k nearest-neighbor signatures for a target spec, returning +// neighbor sigs + their outcome history. This is what the recommender +// feeds to the overview model. +export async function findNeighbors(spec: any, k = 5): Promise> { + await ensureKb(); + const digest = specDigest(spec); + const targetHash = computeSignature(spec); + const resp = await fetch(`${SIDECAR}/embed`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ texts: [digest] }), + }); + if (!resp.ok) return []; + const data: any = await resp.json(); + const targetVec: number[] = data.embeddings?.[0] ?? []; + + const sigs = await readJsonl(SIGNATURES_FILE); + const outcomes = await readJsonl(OUTCOMES_FILE); + + const ranked = sigs + .filter(s => s.sig_hash !== targetHash) // don't recommend against self + .map(s => ({ sig: s, similarity: cosine(targetVec, s.embedding) })) + .sort((a, b) => b.similarity - a.similarity) + .slice(0, k); + + return ranked.map(r => ({ + sig: r.sig, + similarity: r.similarity, + outcomes: outcomes.filter(o => o.sig_hash === r.sig.sig_hash), + })); +} + +// Detect fail→succeed pairs within outcomes for the same signature. +// Diff the config/models between them; that's the correction. +export async function detectErrorCorrections(): Promise; +}>> { + const outcomes = await readJsonl(OUTCOMES_FILE); + const bySig = new Map(); + for (const o of outcomes) { + const arr = bySig.get(o.sig_hash) ?? []; + arr.push(o); + bySig.set(o.sig_hash, arr); + } + const corrections: any[] = []; + for (const [sig, runs] of bySig) { + runs.sort((a, b) => a.created_at.localeCompare(b.created_at)); + for (let i = 1; i < runs.length; i++) { + const prev = runs[i - 1]; + const cur = runs[i]; + if (prev.ok_events < prev.total_events && cur.ok_events > prev.ok_events) { + const changed: Record = {}; + for (const k of ["executor", "reviewer", "overview"] as const) { + if (prev.models[k] !== cur.models[k]) { + changed[k] = { from: prev.models[k], to: cur.models[k] }; + } + } + corrections.push({ sig_hash: sig, before: prev, after: cur, models_changed: changed }); + } + } + } + return corrections; +} + +// Generate a pathway recommendation for a target spec. Reads k-NN +// signatures + their outcome history, asks an overview model to +// synthesize "best path forward", writes to recommendations file. +export async function recommendFor( + spec: any, + opts: { overview_model?: string; cloud?: boolean; k?: number } = {}, +): Promise { + await ensureKb(); + const k = opts.k ?? 5; + const overviewModel = opts.overview_model ?? "gpt-oss:20b"; + const cloud = opts.cloud ?? false; + + const neighbors = await findNeighbors(spec, k); + const corrections = await detectErrorCorrections(); + const targetHash = computeSignature(spec); + + if (neighbors.length === 0) { + // Cold start — no history. Write a minimal rec so next run still + // gets "we looked" instead of a missing file. + const rec: PathwayRecommendation = { + sig_hash: targetHash, + generated_at: new Date().toISOString(), + generated_by_model: overviewModel, + confidence: "low", + rationale: "No prior runs in KB — first time this signature is seen. Use default model matrix.", + top_models: {}, + budget_hints: {}, + pathway_notes: "No neighbor history to draw on. Proceed with current config; KB will have data after this run.", + neighbors_consulted: [], + }; + await appendFile(join(KB_DIR, RECOMMENDATIONS_FILE), JSON.stringify(rec) + "\n"); + return rec; + } + + // Build the prompt. Include target spec digest, neighbor digests with + // their outcome stats, and recent error corrections. Ask for + // structured output so we can parse it. + const neighborBlock = neighbors.map(n => { + const best = n.outcomes.reduce((a, b) => + a && a.ok_events / Math.max(1, a.total_events) >= b.ok_events / Math.max(1, b.total_events) ? a : b, + n.outcomes[0]); + const avgOk = n.outcomes.length > 0 + ? (n.outcomes.reduce((s, o) => s + o.ok_events, 0) / n.outcomes.length).toFixed(1) + : "?"; + return `- sig ${n.sig.sig_hash} sim=${n.similarity.toFixed(3)} (${n.sig.events_digest.slice(0, 100)}): ${n.outcomes.length} runs, avg ${avgOk}/${best?.total_events ?? "?"} ok; best models: exec=${best?.models.executor ?? "?"} review=${best?.models.reviewer ?? "?"}`; + }).join("\n"); + + const correctionBlock = corrections.slice(-3).map(c => + `- sig ${c.sig_hash}: ${c.before.ok_events}/${c.before.total_events} → ${c.after.ok_events}/${c.after.total_events}; changed=${JSON.stringify(c.models_changed)}` + ).join("\n") || "(no corrections observed yet)"; + + const prompt = `You are the pathway recommender for a staffing coordinator agent system. A new scenario is about to run. Your job: use history of similar runs to recommend the best configuration. + +TARGET SCENARIO: +${specDigest(spec)} + +${k} NEAREST-NEIGHBOR SIGNATURES FROM HISTORY (by cosine similarity): +${neighborBlock} + +RECENT ERROR CORRECTIONS (fail → succeed deltas on same signature): +${correctionBlock} + +Your output MUST be a valid JSON object with this shape (and nothing else — no prose before or after): +{ + "confidence": "high" | "medium" | "low", + "rationale": "2-3 sentence explanation of what pattern you saw", + "top_models": {"executor": "...", "reviewer": "...", "overview": "..."}, + "budget_hints": {"executor_max_tokens": 800, "reviewer_max_tokens": 600, "executor_think": false}, + "pathway_notes": "concrete pre-run advice — what to pre-fetch, what to avoid, buffer sizes" +} + +Respond with ONLY the JSON object.`; + + let raw = ""; + try { + raw = await generateContinuable(overviewModel, prompt, { + max_tokens: 1200, + shape: "json", + cloud, + max_continuations: 2, + }); + } catch (e) { + return null; + } + + let parsed: any; + try { + const m = raw.match(/\{[\s\S]*\}/); + parsed = m ? JSON.parse(m[0]) : null; + } catch { + parsed = null; + } + if (!parsed) return null; + + const rec: PathwayRecommendation = { + sig_hash: targetHash, + generated_at: new Date().toISOString(), + generated_by_model: overviewModel, + confidence: parsed.confidence ?? "low", + rationale: String(parsed.rationale ?? "").slice(0, 1000), + top_models: parsed.top_models ?? {}, + budget_hints: parsed.budget_hints ?? {}, + pathway_notes: String(parsed.pathway_notes ?? "").slice(0, 2000), + neighbors_consulted: neighbors.map(n => n.sig.sig_hash), + }; + await appendFile(join(KB_DIR, RECOMMENDATIONS_FILE), JSON.stringify(rec) + "\n"); + return rec; +} + +// Read-back at scenario start: find the newest recommendation for this +// exact sig_hash (or nearest neighbor of it). Returns null if nothing +// applicable found. +export async function loadRecommendation(spec: any): Promise { + await ensureKb(); + const targetHash = computeSignature(spec); + const recs = await readJsonl(RECOMMENDATIONS_FILE); + const matching = recs.filter(r => r.sig_hash === targetHash); + if (matching.length === 0) return null; + matching.sort((a, b) => b.generated_at.localeCompare(a.generated_at)); + return matching[0]; +} + +// Record a config snapshot. Called whenever models.json or the active +// model assignments change, so error-correction diffs have history to +// reference beyond the outcomes file. +export async function snapshotConfig( + models_json_hash: string, + active_models: Record, + why: string, +): Promise { + await ensureKb(); + await appendFile( + join(KB_DIR, CONFIG_SNAPSHOTS_FILE), + JSON.stringify({ + at: new Date().toISOString(), + models_json_hash, + active_models, + why, + }) + "\n", + ); +} diff --git a/tests/multi-agent/scenario.ts b/tests/multi-agent/scenario.ts index 61031d6..17ff135 100644 --- a/tests/multi-agent/scenario.ts +++ b/tests/multi-agent/scenario.ts @@ -35,6 +35,7 @@ import { reviewerPrompt, GATEWAY, } from "./agent.ts"; +import { indexRun, recommendFor, loadRecommendation, type PathwayRecommendation } from "./kb.ts"; import { mkdir, writeFile, appendFile } from "node:fs/promises"; import { join } from "node:path"; @@ -154,6 +155,7 @@ interface ScenarioContext { results: EventResult[]; gap_signals: Array<{ event: string; category: string; detail: string }>; prior_lessons: PriorLesson[]; + pathway_rec?: PathwayRecommendation | null; } interface PriorLesson { @@ -580,7 +582,14 @@ CAST(reliability AS DOUBLE) > 0.7.`; ).join("\n") : ""; - return `${schemaLock}\n\nEVENT FOCUS:\n${base}${priorHint}`; + // Phase 22 pathway recommendation — if the KB synthesized a "best + // path" from neighbor runs, inject it as concrete pre-run guidance. + // Keep terse; the full rationale lives in the KB file. + const pathwayHint = ctx.pathway_rec && ctx.pathway_rec.pathway_notes + ? `\n\nKB PATHWAY RECOMMENDATION (synthesized from ${ctx.pathway_rec.neighbors_consulted.length} neighbor runs, confidence=${ctx.pathway_rec.confidence}):\n${ctx.pathway_rec.pathway_notes.slice(0, 600)}` + : ""; + + return `${schemaLock}\n\nEVENT FOCUS:\n${base}${priorHint}${pathwayHint}`; } // =================== Artifact generation =================== @@ -1101,6 +1110,7 @@ async function writeRetrospective(ctx: ScenarioContext): Promise { // =================== Main driver =================== async function main() { + const runStart = Date.now(); const specPath = process.argv[2]; const spec: ScenarioSpec = specPath ? JSON.parse(await Bun.file(specPath).text()) @@ -1112,6 +1122,18 @@ async function main() { const prior_lessons = await loadPriorLessons(spec); + // Phase 22 KB — load any pathway recommendation for this signature. + // The recommender is called at END of prior runs and synthesizes + // configuration + pathway notes from nearest-neighbor history. + // Nothing on first run (cold start); populates over time. + const pathwayRec = await loadRecommendation(spec).catch(() => null); + if (pathwayRec) { + console.log(`▶ KB recommendation loaded: confidence=${pathwayRec.confidence} from ${pathwayRec.neighbors_consulted.length} neighbors`); + if (pathwayRec.pathway_notes) { + console.log(` pathway: ${pathwayRec.pathway_notes.slice(0, 120)}${pathwayRec.pathway_notes.length > 120 ? "…" : ""}`); + } + } + const ctx: ScenarioContext = { spec, out_dir, @@ -1119,6 +1141,7 @@ async function main() { results: [], gap_signals: [], prior_lessons, + pathway_rec: pathwayRec, }; // Initialize output files @@ -1254,6 +1277,30 @@ async function main() { await writeRetrospective(ctx); + // Phase 22 KB — index this run + synthesize recommendation for next + // time this signature (or similar ones) show up. Event-driven cycle: + // run ends → KB updates → next run reads rec at startup. + try { + const elapsed = (Date.now() - runStart) / 1000; + const { sig_hash } = await indexRun(out_dir, spec, { + executor: EXECUTOR_MODEL, + reviewer: REVIEWER_MODEL, + overview: OVERVIEW_MODEL, + overview_cloud: OVERVIEW_CLOUD, + }, elapsed); + console.log(`▶ KB indexed: sig=${sig_hash} (${elapsed.toFixed(1)}s)`); + const newRec = await recommendFor(spec, { + overview_model: OVERVIEW_MODEL, + cloud: OVERVIEW_CLOUD, + k: 5, + }); + if (newRec) { + console.log(`▶ KB recommendation written: confidence=${newRec.confidence} (${newRec.neighbors_consulted.length} neighbors consulted)`); + } + } catch (e) { + console.log(` (KB update skipped: ${(e as Error).message})`); + } + const okCount = ctx.results.filter(r => r.ok).length; if (okCount < ctx.results.length) { console.log(`\n⚠ ${okCount}/${ctx.results.length} events succeeded. See ${out_dir}/report.md for gaps.`);