diff --git a/kits/AGENT/README.md b/kits/AGENT/README.md
new file mode 100644
index 000000000..80d24d653
--- /dev/null
+++ b/kits/AGENT/README.md
@@ -0,0 +1,255 @@
+# Agent Failure Investigator
+
+**A forensic diagnostic tool for AI agents.** Upload a failed agent trace — from **LangGraph, OpenAI Agents SDK, CrewAI, AutoGen, Lamatic, or the native format** — and get a structured failure report with clickable evidence, a failure-propagation graph, a reconstructed timeline, a remediation playbook, and an exportable investigation document.
+
+```
+Upload trace.json → Auto-detect format → Convert → Investigate
+```
+
+| Supported input formats | |
+|---|---|
+| ✓ LangGraph | `astream_events` export |
+| ✓ OpenAI Agents SDK | trace + spans export |
+| ✓ CrewAI | crew/tasks/tool-usage export |
+| ✓ AutoGen | chat history export |
+| ✓ Lamatic | flow execution nodes |
+| ✓ Native | `docs/trace-schema.md` |
+
+Every adapter maps a framework's run log onto one canonical trace schema; the rule engine never knows which framework the trace came from.
+
+Every AI team asks the same question: *why did the agent fail?* Today the answer means reading logs manually, re-reading prompts manually, and diffing tool calls manually. Agent Failure Investigator automates that investigation.
+
+```
+===================== FAILURE REPORT =====================
+Failure Category TOOL FAILURE
+Confidence 87% (evidence-weighted, capped at 95)
+Evidence [R11] book_table timed out after 10000ms
+ [R13] Fallback branch activated after the failure
+Root Cause The failure began in infrastructure, not in the
+ model. The flow continued to generation as if the
+ tool had succeeded, converting an infrastructure
+ error into a user-facing false statement.
+Contributing Factors Hallucination (+30) — unsupported claims in answer
+Recommended Fixes Add retries with backoff; make fallback honest
+Preventive Actions Alert on p95 tool latency; add circuit breaker
+==========================================================
+```
+
+## Why the design is deterministic-first
+
+The core of this tool is **not** an LLM. It is a **deterministic rule engine** (`rules/`): 13 pure functions over the trace, each producing evidence with references into the raw data, each contributing weighted points to one of five failure categories:
+
+| Category | Detected by (examples) |
+|---|---|
+| Hallucination | R14 tool output ignored · R22 claims unsupported by any source |
+| Tool Failure | R11 timeout · R12 error · R13 fallback after failure |
+| Prompt Ambiguity | R31 contradictory instructions · R32 vague quantifiers · R33 output instability |
+| Wrong Tool Selection | R41 better-matching tool available · R42 no re-plan after irrelevant result |
+| RAG Failure | R21 empty retrieval before factual answer · R23 low relevance scores · R25 no retry |
+
+The category with the most evidence points becomes the primary **Failure Category**; confidence is computed from evidence weight (`35 + 0.8 × points`, capped at 95 — an investigator reports evidence, never certainty). The engine also performs **conflict resolution**: root-cause rules suppress their downstream symptoms, so "the agent ignored the wrong tool's output" is attributed to *Wrong Tool Selection*, not misdiagnosed as hallucination.
+
+The LLM enters only at the last step, and only optionally: it rewrites the Root Cause section as fluent prose from the engine's findings. It receives the fired rules as fixed facts and cannot add, remove, or reweigh evidence. An investigator that hallucinated its own findings would be useless — **diagnosis is deterministic; language is not.**
+
+## Architecture
+
+
+
+The pipeline in one sentence: a raw framework export is claimed and translated by an adapter into one canonical trace schema; 13 pure rules produce evidence with references into the raw data; the engine resolves conflicts, weighs evidence into a category and a confidence, and reconstructs the timeline; the report layer turns that verdict into an investigation document, a propagation graph, a remediation playbook, and exports — with LLM narration as a strictly optional last-mile step that can rephrase but never decide.
+
+
+Text version of the diagram (for terminal readers)
+
+```
+ trace.json (any supported framework)
+ │
+ ▼
+┌──────────────────────┐
+│ Format auto-detect │ js/adapters.js — 6 adapters, each with
+│ + Adapter │ claim(doc) and translate(doc)
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Normalizer │ canonical trace schema (docs/trace-schema.md)
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Rule Engine │ rules/ — one plugin file per failure class,
+│ (pluggable) │ each rule: pure test(trace) → evidence + refs
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Conflict Resolver │ js/engine.js — root causes suppress symptoms
+│ + Evidence Builder │ scoring, confidence, timeline reconstruction
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Report Generator │ js/report.js + js/advisor.js + js/exporter.js
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ UI │ propagation graph · ECG timeline · evidence
+│ │ · remediation playbook · A/B compare · export
+└──────────────────────┘
+```
+
+
+
+The rule engine is a plugin system: dropping a new file into `rules/` that calls `definePlugin({ id, category, points, title, fix, prevention, test })` adds a detector — no engine change, no build step.
+
+```
+rules/
+ core.js categories, registry, shared text analysis
+ hallucination.js R14 · R22
+ tool_failure.js R11 · R12 · R13
+ rag.js R21 · R23 · R25
+ prompt.js R31 · R32 · R33
+ wrong_tool.js R41 · R42
+```
+
+## Quickstart
+
+No build step, no dependencies, no API key required.
+
+```
+git clone
+open index.html # or just double-click it
+```
+
+Pick one of the five preloaded cases (one per failure category) and press **Run investigation** — or press **Upload trace.json** and drop in a raw export from LangGraph, OpenAI Agents SDK, CrewAI, AutoGen, or Lamatic; the format is detected and converted automatically. You can also paste a native-schema trace as JSON (schema in `docs/trace-schema.md`).
+
+To run the regression tests and the benchmark:
+
+```
+node tests/run-tests.js
+node bench/run.js 100 # accuracy benchmark (any N: 100 / 1000 / 10000)
+node bench/scale.js # latency vs trace size (100 / 1,000 / 10,000 events)
+```
+
+The suite asserts that all five sample cases classify into their expected category, that a healthy trace produces **no** diagnosis, that confidence stays in range, that every piece of evidence carries well-formed references into the raw trace, that each of the six adapters detects and converts its fixture correctly, and that the comparison and remediation layers behave.
+
+## Benchmarks
+
+Two benchmarks, both seeded and reproducible on any machine with `node` (numbers below: Node v22, single core, no warm cluster — this runs in a browser tab, so single-threaded cold performance is the honest measurement).
+
+### 1 — Accuracy at dataset scale (`bench/run.js`)
+
+`bench/run.js N` forges a seeded, labeled dataset of *N* traces across all five failure classes plus healthy runs (~28% adversarially mutated to sit near rule thresholds) and measures the engine against the labels. The class mix is proportional, so results are comparable across scales:
+
+| Traces | Accuracy | False positives | False negatives | Misclassified | Avg / trace | p95 / trace |
+|---:|---:|---:|---:|---:|---:|---:|
+| 100 | 95.0% | 0.0% | 0.0% | 5.0% | 0.25 ms | 0.76 ms |
+| 1,000 | 94.4% | 0.0% | 0.0% | 5.6% | 0.12 ms | 0.14 ms |
+| 10,000 | 94.1% | 0.0% | 0.0% | 5.9% | 0.08 ms | 0.10 ms |
+
+Accuracy is stable across scales (the ±1% drift is the adversarial mutation rate playing out over more samples, not degradation), and **no healthy trace is ever flagged and no failure is ever missed entirely** at any scale — every error is a wrong *category*, not a wrong *verdict*. Per-category recall at N=100: Tool Failure 100% · Hallucination 100% · Prompt Ambiguity 100% · RAG Failure 89% · Wrong Tool 77%. The misses are by construction — mutated traces where relevance scores hover at the threshold or the query genuinely overlaps both tools — and they end up misrouted to a *neighboring* category rather than missed. (Avg time per trace *decreasing* with N is JIT warm-up, not magic.)
+
+### 2 — Latency vs trace size (`bench/scale.js`)
+
+Real agent runs are not 8 events long. `bench/scale.js` takes 60 labeled traces, pads each with diagnostically neutral traffic (INFO logs, intermediate assistant turns, heartbeat tool calls — the noise a long-running agent actually produces) up to a target size, then measures the **full investigation** (rules + conflict resolution + scoring + timeline reconstruction) and asserts the verdict is identical to the verdict on the un-padded trace:
+
+| Events / trace | Avg | p50 | p95 | Max | Throughput | Verdicts stable |
+|---:|---:|---:|---:|---:|---:|---:|
+| 100 | 0.24 ms | 0.17 ms | 0.61 ms | 0.79 ms | ~4,200 traces/s | 60/60 |
+| 1,000 | 1.47 ms | 0.96 ms | 5.85 ms | 6.39 ms | ~680 traces/s | 60/60 |
+| 10,000 | 11.83 ms | 9.03 ms | 32.85 ms | 85.98 ms | ~85 traces/s | 60/60 |
+
+Two takeaways: latency grows **near-linearly** with trace size (rules are single-pass over the event arrays), and **classification is invariant to trace size** — a 10,000-event trace with the same failure buried inside gets the same diagnosis as an 8-event one, at every size, in every run. A 10,000-event investigation still completes in ~12 ms — fast enough to run on every failed production run, not just sampled ones.
+
+Both benchmarks regenerate deterministically:
+
+```
+node bench/run.js 10000 # → bench/results.json
+node bench/scale.js # → bench/scale-results.json (100 / 1,000 / 10,000 events)
+node bench/scale.js 100 50000 # custom sizes
+```
+
+## Explainability graph & remediation
+
+Every investigation renders a failure-propagation graph — `User → Prompt → Planner → Retriever → Tool → LLM → Answer` — with the stage that originated the failure highlighted in red and every downstream stage shown as contaminated. Below the fixes, a **remediation playbook** turns the diagnosis into concrete engineering patterns (retry policy, circuit breaker, groundedness gate, query reformulation, mutually-exclusive tool specs, …), each with a one-line rationale.
+
+## Compare two runs
+
+The **Compare** panel takes a Trace A (before the fix) and a Trace B (after), runs both investigations, and reports which rules were resolved, which persist, which are new, plus deltas on category, confidence, tool errors, and total tool latency — a regression test for agent fixes.
+
+## Export
+
+**Export Markdown** downloads the full investigation (timeline, evidence, root cause, recommendations, prevention, remediation playbook) as a `.md` file; **Export PDF** opens a print-formatted document ready to save as PDF.
+
+## Optional LLM narration
+
+Paste an Anthropic API key into the report panel and press **Compose with Claude** to have the Root Cause narrated as prose. The key lives in memory only and is never stored. If the call fails, the tool silently keeps the rule-based narrative — the report never depends on network access.
+
+## What's in a trace
+
+A trace is a single JSON object capturing one agent run (full schema with field-by-field docs in `docs/trace-schema.md`):
+
+```json
+{
+ "system_prompt": "...",
+ "conversation": [{ "role": "user", "ts": "10:33:01", "content": "..." }],
+ "available_tools":[{ "name": "...", "description": "..." }],
+ "tool_calls": [{ "ts": "...", "tool": "...", "input": {}, "status": "success|error|timeout", "duration_ms": 0, "output": "..." }],
+ "retrieved_docs": [{ "id": "...", "source": "...", "score": 0.0, "content": "..." }],
+ "logs": [{ "ts": "...", "level": "INFO|WARN|ERROR", "event": "...", "message": "..." }],
+ "final_response": { "ts": "...", "content": "..." }
+}
+```
+
+This shape deliberately mirrors what agent platforms already log: the request lifecycle from prompt to response, with tools, retrieval, and infrastructure events in between.
+
+## Future Lamatic Integration
+
+This tool was designed so that its input maps one-to-one onto data Lamatic already has. A native integration would:
+
+* **Read Flow Logs** — pull a failed run directly from Lamatic's real-time logs and traces instead of pasting JSON, using the flow's request lifecycle as the trace.
+* **Analyze Tool Calls** — map node executions (tool nodes, timeouts, retries, fallback branches) onto the rule engine's tool-failure and wrong-tool rules.
+* **Analyze Retrieval** — read vector search results and relevance scores from the RAG nodes to power the RAG-failure and groundedness rules.
+* **Generate Failure Report** — attach the failure report to the run in the Logs view, so "why did this run fail?" is answered where the run lives, and *Recommended Fixes* link straight to the node that needs editing in the flow builder.
+
+The rule catalog is intentionally a plain array: teams could ship their own rules per flow (e.g., "a booking confirmation must contain a confirmation id") the same way they ship prompts.
+
+## Repository layout
+
+```
+index.html app shell (zero build step)
+css/style.css diagnostic console theme
+rules/ pluggable rule engine — one file per failure class
+js/adapters.js format auto-detection + 6 framework adapters
+js/engine.js rule runner, scoring, conflict resolution, timeline
+js/graph.js failure-propagation graph
+js/advisor.js remediation playbooks
+js/exporter.js Markdown / PDF investigation export
+js/compare.js before/after trace comparison
+js/report.js report composer (offline) + optional Claude narration
+js/app.js UI: import, case picker, ECG timeline, report, viewer
+js/traces.js 5 sample failure cases
+bench/forge.js seeded trace synthesizer (+ neutral-event inflation)
+bench/run.js accuracy benchmark at dataset scale
+bench/scale.js latency-vs-trace-size benchmark
+docs/architecture.svg architecture diagram (embedded above)
+docs/trace-schema.md canonical trace format documentation
+tests/run-tests.js regression tests (node, no dependencies)
+```
+
+## Known Limitations & Future Work
+
+This system is **deterministic by design** — and that design has known, deliberate limits. Stating them precisely matters more than overstating what 13 rules can do.
+
+### What the current system cannot do
+
+* **Lexical, not semantic.** The groundedness and tool-affinity rules operate on token overlap and pattern shapes. Token overlap is not entailment, and a regex is not a semantic claim-checker: *"the parcel reached its destination"* and *"delivered"* are the same fact to a human and different strings to R22. Paraphrased hallucinations that reuse the source's vocabulary can slip through; correct answers phrased in fresh vocabulary can be flagged as unsupported.
+* **Rule coverage is finite.** The engine detects the five failure classes it has rules for. Failure modes outside the catalog — looping/planning pathologies, context-window truncation, multi-agent coordination failures, prompt injection — currently produce either no diagnosis or a partial one.
+* **Confidence is evidence weight, not calibrated probability.** `35 + 0.8 × points (cap 95)` is a transparent, monotone score — it is *not* a statistically calibrated probability. An 87 does not mean "correct 87% of the time"; it means "a lot of independent evidence fired."
+* **The benchmark is synthetic.** The forged dataset (including its adversarial mutations) is generated by the same author as the rules. 94–95% accuracy on it demonstrates internal consistency and regression safety, not real-world recall. Numbers on production traces will be lower and messier — the benchmark's job is to keep the engine honest as it evolves, not to be a leaderboard claim.
+* **Single-trace scope.** The engine diagnoses one run at a time. It does not yet aggregate across runs ("this tool times out on 4% of traffic, always between 14:00–15:00") — the fleet-level view is where the per-trace verdicts become operationally decisive.
+
+Consequently: **the report is a strong lead for an engineer, not a verdict.** Every rule is cheap, explainable and auditable — that is the point of the design — but an investigator's output should be read as evidence, which is exactly why confidence is capped at 95.
+
+### Planned next steps
+
+1. **Semantic layer behind the same rule interface.** Embedding-based groundedness (claim ↔ source cosine similarity) and semantic tool-affinity as *additional* rules registered through the same `definePlugin` contract. The architecture anticipates this: rules already return evidence + refs, so a semantic rule slots in without touching the engine. The deterministic rules stay as the cheap, auditable first pass; embeddings become a second opinion, not a replacement.
+2. **LLM-as-rule, sandboxed like the narrator.** For claim verification that embeddings can't settle, an optional LLM-backed rule whose output is constrained to the same `{ evidence, refs }` shape — subject to the same invariant that stochastic components may add *candidate* evidence but the deterministic scorer still decides.
+3. **Calibration.** Once real labeled traces exist, fit the confidence mapping to observed precision (isotonic regression is enough) so 80 means 80.
+4. **Cross-run aggregation.** Persist verdicts and mine them: recurring rule patterns per flow, tool-latency trends, regression detection tied into the existing A/B compare.
+5. **Per-flow custom rules.** The rule catalog is a plain array precisely so teams can ship domain rules ("a booking confirmation must contain a confirmation id") the way they ship prompts.
diff --git a/kits/AGENT/agent-failure-investigator.zip b/kits/AGENT/agent-failure-investigator.zip
new file mode 100644
index 000000000..a7300dea6
Binary files /dev/null and b/kits/AGENT/agent-failure-investigator.zip differ
diff --git a/kits/AGENT/agent-failure-investigator/README.md b/kits/AGENT/agent-failure-investigator/README.md
new file mode 100644
index 000000000..80d24d653
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/README.md
@@ -0,0 +1,255 @@
+# Agent Failure Investigator
+
+**A forensic diagnostic tool for AI agents.** Upload a failed agent trace — from **LangGraph, OpenAI Agents SDK, CrewAI, AutoGen, Lamatic, or the native format** — and get a structured failure report with clickable evidence, a failure-propagation graph, a reconstructed timeline, a remediation playbook, and an exportable investigation document.
+
+```
+Upload trace.json → Auto-detect format → Convert → Investigate
+```
+
+| Supported input formats | |
+|---|---|
+| ✓ LangGraph | `astream_events` export |
+| ✓ OpenAI Agents SDK | trace + spans export |
+| ✓ CrewAI | crew/tasks/tool-usage export |
+| ✓ AutoGen | chat history export |
+| ✓ Lamatic | flow execution nodes |
+| ✓ Native | `docs/trace-schema.md` |
+
+Every adapter maps a framework's run log onto one canonical trace schema; the rule engine never knows which framework the trace came from.
+
+Every AI team asks the same question: *why did the agent fail?* Today the answer means reading logs manually, re-reading prompts manually, and diffing tool calls manually. Agent Failure Investigator automates that investigation.
+
+```
+===================== FAILURE REPORT =====================
+Failure Category TOOL FAILURE
+Confidence 87% (evidence-weighted, capped at 95)
+Evidence [R11] book_table timed out after 10000ms
+ [R13] Fallback branch activated after the failure
+Root Cause The failure began in infrastructure, not in the
+ model. The flow continued to generation as if the
+ tool had succeeded, converting an infrastructure
+ error into a user-facing false statement.
+Contributing Factors Hallucination (+30) — unsupported claims in answer
+Recommended Fixes Add retries with backoff; make fallback honest
+Preventive Actions Alert on p95 tool latency; add circuit breaker
+==========================================================
+```
+
+## Why the design is deterministic-first
+
+The core of this tool is **not** an LLM. It is a **deterministic rule engine** (`rules/`): 13 pure functions over the trace, each producing evidence with references into the raw data, each contributing weighted points to one of five failure categories:
+
+| Category | Detected by (examples) |
+|---|---|
+| Hallucination | R14 tool output ignored · R22 claims unsupported by any source |
+| Tool Failure | R11 timeout · R12 error · R13 fallback after failure |
+| Prompt Ambiguity | R31 contradictory instructions · R32 vague quantifiers · R33 output instability |
+| Wrong Tool Selection | R41 better-matching tool available · R42 no re-plan after irrelevant result |
+| RAG Failure | R21 empty retrieval before factual answer · R23 low relevance scores · R25 no retry |
+
+The category with the most evidence points becomes the primary **Failure Category**; confidence is computed from evidence weight (`35 + 0.8 × points`, capped at 95 — an investigator reports evidence, never certainty). The engine also performs **conflict resolution**: root-cause rules suppress their downstream symptoms, so "the agent ignored the wrong tool's output" is attributed to *Wrong Tool Selection*, not misdiagnosed as hallucination.
+
+The LLM enters only at the last step, and only optionally: it rewrites the Root Cause section as fluent prose from the engine's findings. It receives the fired rules as fixed facts and cannot add, remove, or reweigh evidence. An investigator that hallucinated its own findings would be useless — **diagnosis is deterministic; language is not.**
+
+## Architecture
+
+
+
+The pipeline in one sentence: a raw framework export is claimed and translated by an adapter into one canonical trace schema; 13 pure rules produce evidence with references into the raw data; the engine resolves conflicts, weighs evidence into a category and a confidence, and reconstructs the timeline; the report layer turns that verdict into an investigation document, a propagation graph, a remediation playbook, and exports — with LLM narration as a strictly optional last-mile step that can rephrase but never decide.
+
+
+Text version of the diagram (for terminal readers)
+
+```
+ trace.json (any supported framework)
+ │
+ ▼
+┌──────────────────────┐
+│ Format auto-detect │ js/adapters.js — 6 adapters, each with
+│ + Adapter │ claim(doc) and translate(doc)
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Normalizer │ canonical trace schema (docs/trace-schema.md)
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Rule Engine │ rules/ — one plugin file per failure class,
+│ (pluggable) │ each rule: pure test(trace) → evidence + refs
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Conflict Resolver │ js/engine.js — root causes suppress symptoms
+│ + Evidence Builder │ scoring, confidence, timeline reconstruction
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ Report Generator │ js/report.js + js/advisor.js + js/exporter.js
+└──────────┬───────────┘
+ ▼
+┌──────────────────────┐
+│ UI │ propagation graph · ECG timeline · evidence
+│ │ · remediation playbook · A/B compare · export
+└──────────────────────┘
+```
+
+
+
+The rule engine is a plugin system: dropping a new file into `rules/` that calls `definePlugin({ id, category, points, title, fix, prevention, test })` adds a detector — no engine change, no build step.
+
+```
+rules/
+ core.js categories, registry, shared text analysis
+ hallucination.js R14 · R22
+ tool_failure.js R11 · R12 · R13
+ rag.js R21 · R23 · R25
+ prompt.js R31 · R32 · R33
+ wrong_tool.js R41 · R42
+```
+
+## Quickstart
+
+No build step, no dependencies, no API key required.
+
+```
+git clone
+open index.html # or just double-click it
+```
+
+Pick one of the five preloaded cases (one per failure category) and press **Run investigation** — or press **Upload trace.json** and drop in a raw export from LangGraph, OpenAI Agents SDK, CrewAI, AutoGen, or Lamatic; the format is detected and converted automatically. You can also paste a native-schema trace as JSON (schema in `docs/trace-schema.md`).
+
+To run the regression tests and the benchmark:
+
+```
+node tests/run-tests.js
+node bench/run.js 100 # accuracy benchmark (any N: 100 / 1000 / 10000)
+node bench/scale.js # latency vs trace size (100 / 1,000 / 10,000 events)
+```
+
+The suite asserts that all five sample cases classify into their expected category, that a healthy trace produces **no** diagnosis, that confidence stays in range, that every piece of evidence carries well-formed references into the raw trace, that each of the six adapters detects and converts its fixture correctly, and that the comparison and remediation layers behave.
+
+## Benchmarks
+
+Two benchmarks, both seeded and reproducible on any machine with `node` (numbers below: Node v22, single core, no warm cluster — this runs in a browser tab, so single-threaded cold performance is the honest measurement).
+
+### 1 — Accuracy at dataset scale (`bench/run.js`)
+
+`bench/run.js N` forges a seeded, labeled dataset of *N* traces across all five failure classes plus healthy runs (~28% adversarially mutated to sit near rule thresholds) and measures the engine against the labels. The class mix is proportional, so results are comparable across scales:
+
+| Traces | Accuracy | False positives | False negatives | Misclassified | Avg / trace | p95 / trace |
+|---:|---:|---:|---:|---:|---:|---:|
+| 100 | 95.0% | 0.0% | 0.0% | 5.0% | 0.25 ms | 0.76 ms |
+| 1,000 | 94.4% | 0.0% | 0.0% | 5.6% | 0.12 ms | 0.14 ms |
+| 10,000 | 94.1% | 0.0% | 0.0% | 5.9% | 0.08 ms | 0.10 ms |
+
+Accuracy is stable across scales (the ±1% drift is the adversarial mutation rate playing out over more samples, not degradation), and **no healthy trace is ever flagged and no failure is ever missed entirely** at any scale — every error is a wrong *category*, not a wrong *verdict*. Per-category recall at N=100: Tool Failure 100% · Hallucination 100% · Prompt Ambiguity 100% · RAG Failure 89% · Wrong Tool 77%. The misses are by construction — mutated traces where relevance scores hover at the threshold or the query genuinely overlaps both tools — and they end up misrouted to a *neighboring* category rather than missed. (Avg time per trace *decreasing* with N is JIT warm-up, not magic.)
+
+### 2 — Latency vs trace size (`bench/scale.js`)
+
+Real agent runs are not 8 events long. `bench/scale.js` takes 60 labeled traces, pads each with diagnostically neutral traffic (INFO logs, intermediate assistant turns, heartbeat tool calls — the noise a long-running agent actually produces) up to a target size, then measures the **full investigation** (rules + conflict resolution + scoring + timeline reconstruction) and asserts the verdict is identical to the verdict on the un-padded trace:
+
+| Events / trace | Avg | p50 | p95 | Max | Throughput | Verdicts stable |
+|---:|---:|---:|---:|---:|---:|---:|
+| 100 | 0.24 ms | 0.17 ms | 0.61 ms | 0.79 ms | ~4,200 traces/s | 60/60 |
+| 1,000 | 1.47 ms | 0.96 ms | 5.85 ms | 6.39 ms | ~680 traces/s | 60/60 |
+| 10,000 | 11.83 ms | 9.03 ms | 32.85 ms | 85.98 ms | ~85 traces/s | 60/60 |
+
+Two takeaways: latency grows **near-linearly** with trace size (rules are single-pass over the event arrays), and **classification is invariant to trace size** — a 10,000-event trace with the same failure buried inside gets the same diagnosis as an 8-event one, at every size, in every run. A 10,000-event investigation still completes in ~12 ms — fast enough to run on every failed production run, not just sampled ones.
+
+Both benchmarks regenerate deterministically:
+
+```
+node bench/run.js 10000 # → bench/results.json
+node bench/scale.js # → bench/scale-results.json (100 / 1,000 / 10,000 events)
+node bench/scale.js 100 50000 # custom sizes
+```
+
+## Explainability graph & remediation
+
+Every investigation renders a failure-propagation graph — `User → Prompt → Planner → Retriever → Tool → LLM → Answer` — with the stage that originated the failure highlighted in red and every downstream stage shown as contaminated. Below the fixes, a **remediation playbook** turns the diagnosis into concrete engineering patterns (retry policy, circuit breaker, groundedness gate, query reformulation, mutually-exclusive tool specs, …), each with a one-line rationale.
+
+## Compare two runs
+
+The **Compare** panel takes a Trace A (before the fix) and a Trace B (after), runs both investigations, and reports which rules were resolved, which persist, which are new, plus deltas on category, confidence, tool errors, and total tool latency — a regression test for agent fixes.
+
+## Export
+
+**Export Markdown** downloads the full investigation (timeline, evidence, root cause, recommendations, prevention, remediation playbook) as a `.md` file; **Export PDF** opens a print-formatted document ready to save as PDF.
+
+## Optional LLM narration
+
+Paste an Anthropic API key into the report panel and press **Compose with Claude** to have the Root Cause narrated as prose. The key lives in memory only and is never stored. If the call fails, the tool silently keeps the rule-based narrative — the report never depends on network access.
+
+## What's in a trace
+
+A trace is a single JSON object capturing one agent run (full schema with field-by-field docs in `docs/trace-schema.md`):
+
+```json
+{
+ "system_prompt": "...",
+ "conversation": [{ "role": "user", "ts": "10:33:01", "content": "..." }],
+ "available_tools":[{ "name": "...", "description": "..." }],
+ "tool_calls": [{ "ts": "...", "tool": "...", "input": {}, "status": "success|error|timeout", "duration_ms": 0, "output": "..." }],
+ "retrieved_docs": [{ "id": "...", "source": "...", "score": 0.0, "content": "..." }],
+ "logs": [{ "ts": "...", "level": "INFO|WARN|ERROR", "event": "...", "message": "..." }],
+ "final_response": { "ts": "...", "content": "..." }
+}
+```
+
+This shape deliberately mirrors what agent platforms already log: the request lifecycle from prompt to response, with tools, retrieval, and infrastructure events in between.
+
+## Future Lamatic Integration
+
+This tool was designed so that its input maps one-to-one onto data Lamatic already has. A native integration would:
+
+* **Read Flow Logs** — pull a failed run directly from Lamatic's real-time logs and traces instead of pasting JSON, using the flow's request lifecycle as the trace.
+* **Analyze Tool Calls** — map node executions (tool nodes, timeouts, retries, fallback branches) onto the rule engine's tool-failure and wrong-tool rules.
+* **Analyze Retrieval** — read vector search results and relevance scores from the RAG nodes to power the RAG-failure and groundedness rules.
+* **Generate Failure Report** — attach the failure report to the run in the Logs view, so "why did this run fail?" is answered where the run lives, and *Recommended Fixes* link straight to the node that needs editing in the flow builder.
+
+The rule catalog is intentionally a plain array: teams could ship their own rules per flow (e.g., "a booking confirmation must contain a confirmation id") the same way they ship prompts.
+
+## Repository layout
+
+```
+index.html app shell (zero build step)
+css/style.css diagnostic console theme
+rules/ pluggable rule engine — one file per failure class
+js/adapters.js format auto-detection + 6 framework adapters
+js/engine.js rule runner, scoring, conflict resolution, timeline
+js/graph.js failure-propagation graph
+js/advisor.js remediation playbooks
+js/exporter.js Markdown / PDF investigation export
+js/compare.js before/after trace comparison
+js/report.js report composer (offline) + optional Claude narration
+js/app.js UI: import, case picker, ECG timeline, report, viewer
+js/traces.js 5 sample failure cases
+bench/forge.js seeded trace synthesizer (+ neutral-event inflation)
+bench/run.js accuracy benchmark at dataset scale
+bench/scale.js latency-vs-trace-size benchmark
+docs/architecture.svg architecture diagram (embedded above)
+docs/trace-schema.md canonical trace format documentation
+tests/run-tests.js regression tests (node, no dependencies)
+```
+
+## Known Limitations & Future Work
+
+This system is **deterministic by design** — and that design has known, deliberate limits. Stating them precisely matters more than overstating what 13 rules can do.
+
+### What the current system cannot do
+
+* **Lexical, not semantic.** The groundedness and tool-affinity rules operate on token overlap and pattern shapes. Token overlap is not entailment, and a regex is not a semantic claim-checker: *"the parcel reached its destination"* and *"delivered"* are the same fact to a human and different strings to R22. Paraphrased hallucinations that reuse the source's vocabulary can slip through; correct answers phrased in fresh vocabulary can be flagged as unsupported.
+* **Rule coverage is finite.** The engine detects the five failure classes it has rules for. Failure modes outside the catalog — looping/planning pathologies, context-window truncation, multi-agent coordination failures, prompt injection — currently produce either no diagnosis or a partial one.
+* **Confidence is evidence weight, not calibrated probability.** `35 + 0.8 × points (cap 95)` is a transparent, monotone score — it is *not* a statistically calibrated probability. An 87 does not mean "correct 87% of the time"; it means "a lot of independent evidence fired."
+* **The benchmark is synthetic.** The forged dataset (including its adversarial mutations) is generated by the same author as the rules. 94–95% accuracy on it demonstrates internal consistency and regression safety, not real-world recall. Numbers on production traces will be lower and messier — the benchmark's job is to keep the engine honest as it evolves, not to be a leaderboard claim.
+* **Single-trace scope.** The engine diagnoses one run at a time. It does not yet aggregate across runs ("this tool times out on 4% of traffic, always between 14:00–15:00") — the fleet-level view is where the per-trace verdicts become operationally decisive.
+
+Consequently: **the report is a strong lead for an engineer, not a verdict.** Every rule is cheap, explainable and auditable — that is the point of the design — but an investigator's output should be read as evidence, which is exactly why confidence is capped at 95.
+
+### Planned next steps
+
+1. **Semantic layer behind the same rule interface.** Embedding-based groundedness (claim ↔ source cosine similarity) and semantic tool-affinity as *additional* rules registered through the same `definePlugin` contract. The architecture anticipates this: rules already return evidence + refs, so a semantic rule slots in without touching the engine. The deterministic rules stay as the cheap, auditable first pass; embeddings become a second opinion, not a replacement.
+2. **LLM-as-rule, sandboxed like the narrator.** For claim verification that embeddings can't settle, an optional LLM-backed rule whose output is constrained to the same `{ evidence, refs }` shape — subject to the same invariant that stochastic components may add *candidate* evidence but the deterministic scorer still decides.
+3. **Calibration.** Once real labeled traces exist, fit the confidence mapping to observed precision (isotonic regression is enough) so 80 means 80.
+4. **Cross-run aggregation.** Persist verdicts and mine them: recurring rule patterns per flow, tool-latency trends, regression detection tied into the existing A/B compare.
+5. **Per-flow custom rules.** The rule catalog is a plain array precisely so teams can ship domain rules ("a booking confirmation must contain a confirmation id") the way they ship prompts.
diff --git a/kits/AGENT/agent-failure-investigator/bench/forge.js b/kits/AGENT/agent-failure-investigator/bench/forge.js
new file mode 100644
index 000000000..0b5dca102
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/bench/forge.js
@@ -0,0 +1,169 @@
+let seed = 1337;
+const rng = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
+const pick = arr => arr[Math.floor(rng() * arr.length)];
+const between = (lo, hi) => lo + Math.floor(rng() * (hi - lo + 1));
+
+const CITIES = ["Berlin", "Beirut", "Tokyo", "Lagos", "Lima", "Oslo", "Doha", "Porto"];
+const ORDERS = () => `A-${between(10000, 99999)}`;
+const CLOCK = () => `${String(between(9, 18)).padStart(2, "0")}:${String(between(0, 59)).padStart(2, "0")}:${String(between(0, 59)).padStart(2, "0")}`;
+
+const skeleton = () => {
+ const t = CLOCK();
+ return {
+ meta: { agent: pick(["support-bot", "booking-bot", "hr-bot", "sales-bot"]), flow_id: `flow_${between(1000, 9999)}`, model: pick(["gpt-4o-mini", "claude-haiku", "llama-3-70b"]), session: `s${between(100, 999)}`, date: "2026-07-08" },
+ system_prompt: "You are a support agent. Use the tools provided and base every answer on tool results and retrieved documents.",
+ conversation: [{ role: "user", ts: t, content: "" }],
+ available_tools: [
+ { name: "get_order_status", description: "Look up live shipping status, carrier and ETA for an order id." },
+ { name: "knowledge_base_search", description: "Search internal policy documents, SLA terms and refund rules." }
+ ],
+ tool_calls: [], retrieved_docs: [], logs: [], final_response: { ts: t, content: "" }
+ };
+};
+
+const FORGES = {
+ TOOL_FAILURE() {
+ const t = skeleton();
+ t.conversation[0].content = `Where is my order ${ORDERS()}? It's late.`;
+ const doomed = rng() < 0.5;
+ t.tool_calls.push({ ts: t.conversation[0].ts, tool: "get_order_status", input: { order_id: "x" }, status: doomed ? "timeout" : "error", duration_ms: doomed ? between(8000, 15000) : between(100, 900), output: "" });
+ if (rng() < 0.7) t.logs.push({ ts: t.conversation[0].ts, level: "WARN", event: "flow.fallback", message: "fallback branch activated after tool failure" });
+ t.final_response.content = `Your order will arrive by 2026-07-${between(10, 28)} via the standard carrier.`;
+ return t;
+ },
+ HALLUCINATION() {
+ const t = skeleton();
+ t.conversation[0].content = `Where is my order ${ORDERS()}?`;
+ t.tool_calls.push({ ts: t.conversation[0].ts, tool: "get_order_status", input: { order_id: "x" }, status: "success", duration_ms: between(90, 400), output: `{"status":"in_transit","carrier":"DHL","eta":"2026-07-12","last_scan":"${pick(CITIES)} hub"}` });
+ t.final_response.content = `Great news — your package was delivered on July ${between(1, 6)}th and signed for at ${between(8, 11)}:00. A refund of $${between(20, 90)} was also issued per Section ${between(2, 7)}.${between(1, 9)}.`;
+ return t;
+ },
+ RAG_FAILURE() {
+ const t = skeleton();
+ t.conversation[0].content = "What is our refund SLA for enterprise customers?";
+ const empty = rng() < 0.55;
+ t.tool_calls.push({ ts: t.conversation[0].ts, tool: "knowledge_base_search", input: { q: "refund sla" }, status: "success", duration_ms: between(60, 300), output: empty ? "[]" : "results below" });
+ if (empty) {
+ t.logs.push({ ts: t.conversation[0].ts, level: "WARN", event: "retrieval.empty", message: "0 chunks above threshold" });
+ } else {
+ const n = between(2, 4);
+ for (let i = 0; i < n; i++) t.retrieved_docs.push({ id: `doc-${i}`, source: "handbook.md", score: rng() * 0.35, content: "General onboarding notes about office equipment and travel booking procedures." });
+ }
+ t.final_response.content = `Enterprise refunds are processed within ${between(3, 21)} business days, per Section ${between(1, 9)}.${between(1, 9)} of the policy.`;
+ return t;
+ },
+ WRONG_TOOL() {
+ const t = skeleton();
+ t.conversation[0].content = "What does our internal policy say about SLA credits and refund rules for enterprise accounts?";
+ t.tool_calls.push({ ts: t.conversation[0].ts, tool: "get_order_status", input: { order_id: "none" }, status: "success", duration_ms: between(80, 500), output: `{"status":"unknown","carrier":null,"note":"no shipment found"}` });
+ t.final_response.content = "Our policy offers generous credits for any missed commitments.";
+ return t;
+ },
+ PROMPT_AMBIGUITY() {
+ const t = skeleton();
+ t.system_prompt = "Always respond in a single sentence. Provide comprehensive, detailed answers with examples. Format output as appropriate, escalate if needed, add caveats when necessary, etc.";
+ t.conversation[0].content = "Summarize our shipping policy.";
+ t.tool_calls.push({ ts: t.conversation[0].ts, tool: "knowledge_base_search", input: { q: "shipping" }, status: "success", duration_ms: between(60, 250), output: "Standard shipping takes 5 to 9 business days per the policy document." });
+ t.retrieved_docs.push({ id: "doc-1", source: "shipping-policy.md", score: 0.82, content: "Standard shipping takes 5 to 9 business days. Carrier delays are outside our control." });
+ if (rng() < 0.6) t.logs.push({ ts: t.conversation[0].ts, level: "WARN", event: "output.validation", message: "response shape varies across recent runs" });
+ t.final_response.content = "Standard shipping takes 5 to 9 business days; carrier delays are outside our control.";
+ return t;
+ },
+ HEALTHY() {
+ const t = skeleton();
+ const city = pick(CITIES);
+ t.conversation[0].content = `Where is my order ${ORDERS()}?`;
+ const payload = `{"status":"in_transit","carrier":"DHL","eta":"2026-07-12","last_scan":"${city} hub"}`;
+ t.tool_calls.push({ ts: t.conversation[0].ts, tool: "get_order_status", input: { order_id: "x" }, status: "success", duration_ms: between(90, 400), output: payload });
+ t.retrieved_docs.push({ id: "doc-1", source: "shipping-policy.md", score: 0.84, content: "Standard shipping takes 5 to 9 business days. Track parcels with the carrier tracking id." });
+ t.final_response.content = `Your order is in_transit with DHL, last scan at the ${city} hub, eta 2026-07-12. Standard shipping takes 5 to 9 business days.`;
+ t.logs.push({ ts: t.conversation[0].ts, level: "INFO", event: "tool.result", message: "200 OK" });
+ return t;
+ }
+};
+
+const GRIME = {
+ HALLUCINATION(t) {
+ t.final_response.content = `Your order is in_transit with DHL. It was actually delivered on July ${between(1, 6)}th per Section ${between(2, 7)}.${between(1, 9)} of the carrier terms.`;
+ },
+ RAG_FAILURE(t) {
+ if (t.retrieved_docs.length) t.retrieved_docs.forEach(d => { d.score = 0.42 + rng() * 0.16; });
+ else t.logs = t.logs.filter(l => l.event !== "retrieval.empty");
+ },
+ PROMPT_AMBIGUITY(t) {
+ t.system_prompt = "Always respond in a single sentence. Provide comprehensive, detailed answers. Escalate if needed.";
+ t.logs = t.logs.filter(l => l.event !== "output.validation");
+ },
+ WRONG_TOOL(t) {
+ t.conversation[0].content = "Can you check the SLA credit situation on my recent order shipment?";
+ },
+ TOOL_FAILURE(t) {
+ t.logs = t.logs.filter(l => !/fallback/.test(l.event));
+ },
+ HEALTHY(t) {
+ t.tool_calls[0].duration_ms = between(4000, 7000);
+ t.logs.push({ ts: t.conversation[0].ts, level: "INFO", event: "flow.callback", message: "post-response webhook fired" });
+ t.system_prompt += " Escalate to a human if needed.";
+ }
+};
+
+function forgeDataset(total) {
+ // Proportional mix (percent of total) so the class balance is identical
+ // at N=100, N=1,000 and N=10,000 and results stay comparable across scales.
+ const RATIOS = [
+ ["TOOL_FAILURE", 0.18], ["HALLUCINATION", 0.18], ["RAG_FAILURE", 0.18],
+ ["WRONG_TOOL", 0.13], ["PROMPT_AMBIGUITY", 0.13]
+ ];
+ const mix = RATIOS.map(([label, r]) => [label, Math.round(total * r)]);
+ mix.push(["HEALTHY", total - mix.reduce((s, [, n]) => s + n, 0)]);
+ const dataset = [];
+ mix.forEach(([label, n]) => {
+ for (let i = 0; i < n; i++) {
+ const trace = FORGES[label]();
+ if (rng() < 0.28) GRIME[label](trace);
+ dataset.push({ label: label === "HEALTHY" ? null : label, trace });
+ }
+ });
+ for (let i = dataset.length - 1; i > 0; i--) {
+ const j = Math.floor(rng() * (i + 1));
+ [dataset[i], dataset[j]] = [dataset[j], dataset[i]];
+ }
+ return dataset;
+}
+
+/* ---------- trace-size scaling (bench/scale.js) ---------- */
+
+/** Count every event in a trace: messages + tool calls + docs + logs + final. */
+function countEvents(t) {
+ return (t.conversation || []).length + (t.tool_calls || []).length +
+ (t.retrieved_docs || []).length + (t.logs || []).length +
+ (t.final_response ? 1 : 0);
+}
+
+/**
+ * Pad a forged trace up to `targetEvents` total events with *diagnostically
+ * neutral* traffic — the kind of noise a real long-running agent produces:
+ * INFO logs (~86%), intermediate assistant turns (~9%) and successful
+ * auxiliary heartbeat tool calls with bland outputs (~5%). None of these
+ * fire or unfire any rule, so the label stays valid: the benchmark asserts
+ * the verdict is unchanged at every size.
+ */
+const NOISE_EVENTS = ["node.enter", "node.exit", "llm.token_usage", "memory.checkpoint", "http.request", "http.response", "queue.dequeue", "cache.hit"];
+function inflateTrace(trace, targetEvents) {
+ const t = JSON.parse(JSON.stringify(trace));
+ let need = targetEvents - countEvents(t);
+ while (need-- > 0) {
+ const r = rng();
+ const ts = CLOCK();
+ if (r < 0.86) {
+ t.logs.push({ ts, level: "INFO", event: pick(NOISE_EVENTS), message: `step ${between(1, 9999)} completed in ${between(1, 40)}ms` });
+ } else if (r < 0.95) {
+ t.conversation.push({ role: "assistant", ts, content: `Working on it — step ${between(1, 999)} of the plan.` });
+ } else {
+ t.tool_calls.push({ ts, tool: "heartbeat_ping", input: { seq: between(1, 9999) }, status: "success", duration_ms: between(1, 30), output: `{"ok":true}` });
+ }
+ }
+ return t;
+}
+
+module.exports = { forgeDataset, inflateTrace, countEvents };
diff --git a/kits/AGENT/agent-failure-investigator/bench/results.json b/kits/AGENT/agent-failure-investigator/bench/results.json
new file mode 100644
index 000000000..8457a5afe
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/bench/results.json
@@ -0,0 +1,32 @@
+{
+ "generated": "2026-07-14T16:35:29.096Z",
+ "traces": 100,
+ "accuracy": "95.0%",
+ "false_positives": "0.0%",
+ "false_negatives": "0.0%",
+ "misclassified": "5.0%",
+ "avg_ms": 0.25,
+ "p95_ms": 0.76,
+ "per_category": {
+ "RAG_FAILURE": {
+ "total": 18,
+ "hit": 16
+ },
+ "TOOL_FAILURE": {
+ "total": 18,
+ "hit": 18
+ },
+ "PROMPT_AMBIGUITY": {
+ "total": 13,
+ "hit": 13
+ },
+ "HALLUCINATION": {
+ "total": 18,
+ "hit": 18
+ },
+ "WRONG_TOOL": {
+ "total": 13,
+ "hit": 10
+ }
+ }
+}
\ No newline at end of file
diff --git a/kits/AGENT/agent-failure-investigator/bench/run.js b/kits/AGENT/agent-failure-investigator/bench/run.js
new file mode 100644
index 000000000..5447e27b6
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/bench/run.js
@@ -0,0 +1,69 @@
+const fs = require("fs");
+const path = require("path");
+const vm = require("vm");
+const { performance } = require("perf_hooks");
+const { forgeDataset } = require("./forge");
+
+const ROOT = path.join(__dirname, "..");
+const sandbox = { console, module: { exports: {} } };
+vm.createContext(sandbox);
+[
+ "rules/core.js", "rules/tool_failure.js", "rules/hallucination.js",
+ "rules/rag.js", "rules/prompt.js", "rules/wrong_tool.js", "js/engine.js"
+].forEach(f => vm.runInContext(fs.readFileSync(path.join(ROOT, f), "utf8"), sandbox, { filename: f }));
+
+const investigate = vm.runInContext("runInvestigation", sandbox);
+const N = Number(process.argv[2]) || 100;
+const dataset = forgeDataset(N);
+
+let correct = 0, falsePositive = 0, falseNegative = 0, misclassified = 0;
+const perCategory = {};
+const timings = [];
+
+dataset.forEach(({ label, trace }) => {
+ const t0 = performance.now();
+ const verdict = investigate(trace).primary;
+ timings.push(performance.now() - t0);
+
+ if (label) {
+ perCategory[label] = perCategory[label] || { total: 0, hit: 0 };
+ perCategory[label].total++;
+ if (verdict === label) { correct++; perCategory[label].hit++; }
+ else if (verdict === null) falseNegative++;
+ else misclassified++;
+ } else {
+ verdict === null ? correct++ : falsePositive++;
+ }
+});
+
+const pct = n => ((n / dataset.length) * 100).toFixed(1) + "%";
+const avgMs = (timings.reduce((a, b) => a + b, 0) / timings.length).
+ toFixed(2);
+const sorted = [...timings].sort((a, b) => a - b);
+const p95 = sorted[Math.floor(sorted.length * 0.95)].toFixed(2);
+
+const rows = [
+ ["Traces", dataset.length],
+ ["Accuracy", pct(correct)],
+ ["False positives", pct(falsePositive)],
+ ["False negatives", pct(falseNegative)],
+ ["Misclassified", pct(misclassified)],
+ ["Avg time / trace", avgMs + " ms"],
+ ["p95 time / trace", p95 + " ms"]
+];
+
+console.log("\n BENCHMARK — deterministic rule engine\n " + "─".repeat(42));
+rows.forEach(([k, v]) => console.log(` ${String(k).padEnd(20)} ${v}`));
+console.log(" " + "─".repeat(42) + "\n Per-category recall:");
+Object.entries(perCategory).forEach(([k, s]) =>
+ console.log(` ${k.padEnd(20)} ${s.hit}/${s.total} (${((s.hit / s.total) * 100).toFixed(0)}%)`));
+console.log("");
+
+fs.writeFileSync(path.join(__dirname, "results.json"), JSON.stringify({
+ generated: new Date().toISOString(), traces: dataset.length,
+ accuracy: pct(correct), false_positives: pct(falsePositive),
+ false_negatives: pct(falseNegative), misclassified: pct(misclassified),
+ avg_ms: Number(avgMs), p95_ms: Number(p95),
+ per_category: perCategory
+}, null, 2));
+console.log(" results written to bench/results.json\n");
diff --git a/kits/AGENT/agent-failure-investigator/bench/scale-results.json b/kits/AGENT/agent-failure-investigator/bench/scale-results.json
new file mode 100644
index 000000000..861e5d2ba
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/bench/scale-results.json
@@ -0,0 +1,37 @@
+{
+ "generated": "2026-07-14T16:38:56.052Z",
+ "node": "v22.22.2",
+ "traces_per_size": 60,
+ "results": [
+ {
+ "events_per_trace": 100,
+ "traces": 60,
+ "avg_ms": 0.24,
+ "p50_ms": 0.18,
+ "p95_ms": 0.56,
+ "max_ms": 2.16,
+ "traces_per_second": 4229,
+ "verdicts_stable": "60/60"
+ },
+ {
+ "events_per_trace": 1000,
+ "traces": 60,
+ "avg_ms": 1.2,
+ "p50_ms": 0.95,
+ "p95_ms": 3.09,
+ "max_ms": 5.55,
+ "traces_per_second": 833,
+ "verdicts_stable": "60/60"
+ },
+ {
+ "events_per_trace": 10000,
+ "traces": 60,
+ "avg_ms": 12.19,
+ "p50_ms": 9.3,
+ "p95_ms": 29.97,
+ "max_ms": 63.17,
+ "traces_per_second": 82,
+ "verdicts_stable": "60/60"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/kits/AGENT/agent-failure-investigator/bench/scale.js b/kits/AGENT/agent-failure-investigator/bench/scale.js
new file mode 100644
index 000000000..3d274afd3
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/bench/scale.js
@@ -0,0 +1,93 @@
+/**
+ * Trace-size scaling benchmark.
+ *
+ * node bench/scale.js # sizes 100, 1000, 10000 events
+ * node bench/scale.js 100 50000 # custom sizes
+ *
+ * For each size it forges 60 labeled traces (10 per class, healthy included),
+ * pads each one with diagnostically neutral events (INFO logs, assistant
+ * turns, heartbeat tool calls) up to the target size, then measures the full
+ * investigation — rules + conflict resolution + scoring + timeline — and
+ * asserts the verdict is IDENTICAL to the verdict on the un-padded trace.
+ * So the numbers below are latency *and* a robustness proof: accuracy does
+ * not degrade as traces grow.
+ *
+ * Results are written to bench/scale-results.json.
+ */
+const fs = require("fs");
+const path = require("path");
+const vm = require("vm");
+const { performance } = require("perf_hooks");
+const { forgeDataset, inflateTrace, countEvents } = require("./forge");
+
+const ROOT = path.join(__dirname, "..");
+const sandbox = { console, module: { exports: {} } };
+vm.createContext(sandbox);
+[
+ "rules/core.js", "rules/tool_failure.js", "rules/hallucination.js",
+ "rules/rag.js", "rules/prompt.js", "rules/wrong_tool.js", "js/engine.js"
+].forEach(f => vm.runInContext(fs.readFileSync(path.join(ROOT, f), "utf8"), sandbox, { filename: f }));
+const investigate = vm.runInContext("runInvestigation", sandbox);
+
+const SIZES = process.argv.slice(2).map(Number).filter(Boolean);
+if (!SIZES.length) SIZES.push(100, 1000, 10000);
+const TRACES_PER_SIZE = 60;
+const stat = arr => {
+ const s = [...arr].sort((a, b) => a - b);
+ return {
+ avg: arr.reduce((a, b) => a + b, 0) / arr.length,
+ p50: s[Math.floor(s.length * 0.50)],
+ p95: s[Math.floor(s.length * 0.95)],
+ max: s[s.length - 1]
+ };
+};
+
+// base corpus: verdicts on the small, un-padded traces are ground truth
+const base = forgeDataset(TRACES_PER_SIZE);
+base.forEach(item => { item.baseVerdict = investigate(item.trace).primary; });
+
+console.log("\n SCALING BENCHMARK — engine latency vs trace size");
+console.log(" " + "─".repeat(74));
+console.log(" " + ["events/trace", "avg ms", "p50 ms", "p95 ms", "max ms", "traces/s", "verdicts"].map((h, i) => h.padEnd(i ? 10 : 14)).join(""));
+console.log(" " + "─".repeat(74));
+
+const results = [];
+for (const size of SIZES) {
+ const timings = [];
+ let stable = 0;
+ for (const item of base) {
+ const big = inflateTrace(item.trace, size);
+ if (countEvents(big) !== size) throw new Error("inflation mismatch");
+ // warm-up pass so JIT noise doesn't pollute the smallest size
+ investigate(big);
+ const t0 = performance.now();
+ const verdict = investigate(big).primary;
+ timings.push(performance.now() - t0);
+ if (verdict === item.baseVerdict) stable++;
+ }
+ const s = stat(timings);
+ const throughput = Math.round(1000 / s.avg);
+ const stability = `${stable}/${base.length}`;
+ results.push({
+ events_per_trace: size, traces: base.length,
+ avg_ms: +s.avg.toFixed(2), p50_ms: +s.p50.toFixed(2),
+ p95_ms: +s.p95.toFixed(2), max_ms: +s.max.toFixed(2),
+ traces_per_second: throughput, verdicts_stable: stability
+ });
+ console.log(" " + [
+ String(size).padEnd(14), s.avg.toFixed(2).padEnd(10), s.p50.toFixed(2).padEnd(10),
+ s.p95.toFixed(2).padEnd(10), s.max.toFixed(2).padEnd(10),
+ String(throughput).padEnd(10), stability
+ ].join(""));
+}
+console.log(" " + "─".repeat(74));
+console.log(" 'verdicts' = investigations whose category is unchanged after padding");
+console.log(" the trace with neutral events — accuracy must not degrade with size.\n");
+
+fs.writeFileSync(path.join(__dirname, "scale-results.json"), JSON.stringify({
+ generated: new Date().toISOString(),
+ node: process.version,
+ traces_per_size: TRACES_PER_SIZE,
+ results
+}, null, 2));
+console.log(" results written to bench/scale-results.json\n");
diff --git a/kits/AGENT/agent-failure-investigator/css/style.css b/kits/AGENT/agent-failure-investigator/css/style.css
new file mode 100644
index 000000000..732353cb0
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/css/style.css
@@ -0,0 +1,442 @@
+/* Agent Failure Investigator — diagnostic console theme.
+ Category colors are functional coding, not decoration:
+ each failure category keeps one hue everywhere in the UI. */
+
+:root {
+ --bg: #0e1216;
+ --panel: #151b21;
+ --panel-2: #1a2229;
+ --line: #26313a;
+ --text: #e7ecea;
+ --dim: #8fa0a6;
+
+ --red: #ff5d5d;
+ --amber: #ffb224;
+ --green: #43d69b;
+ --blue: #5ca8ff;
+ --violet: #b18cff;
+ --teal: #3ac6c0;
+
+ --cat-hallucination: var(--violet);
+ --cat-tool: var(--red);
+ --cat-prompt: var(--amber);
+ --cat-wrongtool: var(--blue);
+ --cat-rag: var(--teal);
+
+ --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
+ --display: "Space Grotesk", system-ui, sans-serif;
+}
+
+* { box-sizing: border-box; margin: 0; padding: 0; }
+
+html { scroll-behavior: smooth; }
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+ * { animation: none !important; transition: none !important; }
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--display);
+ font-size: 15px;
+ line-height: 1.55;
+ min-height: 100vh;
+}
+
+.wrap { max-width: 1180px; margin: 0 auto; padding: 32px 24px 80px; }
+
+/* ---------- header ---------- */
+
+header { margin-bottom: 36px; }
+
+.eyebrow {
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ color: var(--dim);
+ text-transform: uppercase;
+}
+
+h1 {
+ font-size: clamp(28px, 4.5vw, 44px);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ margin: 6px 0 8px;
+}
+h1 .accent { color: var(--red); }
+
+.tagline { color: var(--dim); max-width: 640px; }
+
+/* ---------- case picker ---------- */
+
+.section-title {
+ font-family: var(--mono);
+ font-size: 12px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--dim);
+ margin: 28px 0 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.section-title::after { content: ""; flex: 1; height: 1px; background: var(--line); }
+
+#cases {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
+ gap: 10px;
+}
+
+.case-card {
+ text-align: left;
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ padding: 14px 14px 12px;
+ color: var(--text);
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ font-family: var(--display);
+ transition: border-color .15s, transform .15s;
+}
+.case-card:hover { border-color: var(--dim); transform: translateY(-1px); }
+.case-card:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
+.case-card.selected { border-color: var(--red); background: var(--panel-2); }
+
+.case-id { font-family: var(--mono); font-size: 10px; letter-spacing: .12em; color: var(--red); }
+.case-label { font-size: 14px; font-weight: 600; line-height: 1.35; }
+.case-hint { font-family: var(--mono); font-size: 11px; color: var(--dim); }
+
+.picker-actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; align-items: center; }
+
+button.primary {
+ background: var(--red);
+ color: #17090b;
+ border: none;
+ font-family: var(--display);
+ font-weight: 700;
+ font-size: 15px;
+ padding: 11px 22px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: filter .15s;
+}
+button.primary:hover:not(:disabled) { filter: brightness(1.1); }
+button.primary:disabled { opacity: .35; cursor: not-allowed; }
+button.primary:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
+
+button.ghost {
+ background: transparent;
+ color: var(--dim);
+ border: 1px solid var(--line);
+ font-family: var(--mono);
+ font-size: 12px;
+ padding: 10px 16px;
+ border-radius: 8px;
+ cursor: pointer;
+}
+button.ghost:hover { color: var(--text); border-color: var(--dim); }
+button.ghost:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
+
+#custom-area { margin-top: 12px; }
+#custom-json {
+ width: 100%;
+ min-height: 160px;
+ background: var(--panel);
+ color: var(--text);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ font-family: var(--mono);
+ font-size: 12px;
+ padding: 12px;
+ resize: vertical;
+}
+#custom-json:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
+
+.hidden { display: none !important; }
+
+/* ---------- results ---------- */
+
+#results { margin-top: 40px; }
+
+.panel {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ padding: 20px;
+}
+
+/* timeline */
+#timeline-scroll { overflow-x: auto; padding-bottom: 6px; }
+.tl-ts { font-family: var(--mono); font-size: 10px; fill: var(--dim); }
+
+#timeline-labels {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+ margin-top: 10px;
+}
+.tl-chip {
+ display: inline-flex;
+ gap: 8px;
+ align-items: baseline;
+ font-family: var(--mono);
+ font-size: 12px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ padding: 5px 9px;
+ background: var(--panel-2);
+}
+.tl-chip-ts { color: var(--dim); font-size: 10px; }
+.tl-fail { border-color: var(--red); color: var(--red); }
+.tl-warn { border-color: var(--amber); color: var(--amber); }
+.tl-ok { color: var(--green); }
+.tl-tool { color: var(--blue); }
+.tl-arrow { color: var(--dim); font-size: 12px; }
+
+/* report grid */
+.report-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
+ gap: 16px;
+ margin-top: 16px;
+}
+@media (max-width: 880px) { .report-grid { grid-template-columns: 1fr; } }
+
+.report-head {
+ display: flex;
+ align-items: center;
+ gap: 22px;
+ flex-wrap: wrap;
+ padding-bottom: 18px;
+ border-bottom: 1px solid var(--line);
+ margin-bottom: 18px;
+}
+
+.stamp {
+ --stamp-color: var(--red);
+ font-family: var(--mono);
+ font-weight: 700;
+ font-size: clamp(16px, 2.4vw, 22px);
+ letter-spacing: 0.12em;
+ color: var(--stamp-color);
+ border: 2px solid var(--stamp-color);
+ border-radius: 6px;
+ padding: 8px 16px;
+ transform: rotate(-2deg);
+ box-shadow: 0 0 0 1px rgba(0,0,0,.4) inset;
+}
+
+.confidence-block { min-width: 200px; flex: 1; }
+.confidence-line { display: flex; align-items: baseline; gap: 10px; }
+#confidence { font-family: var(--mono); font-size: 26px; font-weight: 700; }
+.confidence-label { font-family: var(--mono); font-size: 11px; letter-spacing: .12em; color: var(--dim); text-transform: uppercase; }
+.confidence-track { height: 6px; background: var(--panel-2); border-radius: 3px; margin-top: 6px; overflow: hidden; }
+#confidence-bar { display: block; height: 100%; width: 0; border-radius: 3px; transition: width .5s ease; }
+#confidence-note { font-family: var(--mono); font-size: 10px; color: var(--dim); margin-top: 5px; }
+
+.field-title {
+ font-family: var(--mono);
+ font-size: 11px;
+ letter-spacing: .14em;
+ text-transform: uppercase;
+ color: var(--dim);
+ margin: 18px 0 8px;
+}
+.field-title:first-of-type { margin-top: 0; }
+
+.evidence-row {
+ display: flex;
+ gap: 10px;
+ align-items: baseline;
+ padding: 9px 0;
+ border-bottom: 1px dashed var(--line);
+ flex-wrap: wrap;
+}
+.evidence-row:last-child { border-bottom: none; }
+.evidence-text { flex: 1; min-width: 220px; font-size: 14px; }
+.evidence-refs { display: flex; gap: 6px; }
+
+.rule-chip {
+ font-family: var(--mono);
+ font-size: 11px;
+ border: 1px solid var(--dim);
+ color: var(--dim);
+ border-radius: 4px;
+ padding: 1px 6px;
+ white-space: nowrap;
+}
+.rule-chip.small { font-size: 10px; }
+
+.ref-link {
+ font-family: var(--mono);
+ font-size: 10px;
+ background: var(--panel-2);
+ color: var(--blue);
+ border: 1px solid var(--line);
+ border-radius: 4px;
+ padding: 2px 7px;
+ cursor: pointer;
+}
+.ref-link:hover { border-color: var(--blue); }
+
+#root-cause { font-size: 14.5px; }
+.mode-tag { font-family: var(--mono); font-size: 10px; color: var(--dim); margin-top: 6px; }
+
+.contrib { display: flex; gap: 10px; align-items: baseline; padding: 6px 0; flex-wrap: wrap; }
+.contrib-dot { width: 9px; height: 9px; border-radius: 50%; align-self: center; flex-shrink: 0; }
+.contrib-name { font-weight: 600; font-size: 14px; }
+.contrib-pts { font-family: var(--mono); font-size: 12px; color: var(--dim); }
+.contrib-rules { font-size: 12.5px; color: var(--dim); flex-basis: 100%; padding-left: 19px; }
+
+ul.actions { list-style: none; }
+ul.actions li {
+ padding: 8px 0 8px 0;
+ border-bottom: 1px dashed var(--line);
+ font-size: 14px;
+ display: flex;
+ gap: 9px;
+ align-items: baseline;
+}
+ul.actions li:last-child { border-bottom: none; }
+
+.muted { color: var(--dim); font-size: 13px; }
+
+/* rule engine panel */
+.score-bars { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
+.score-row { display: grid; grid-template-columns: 130px 1fr 34px; gap: 10px; align-items: center; opacity: .65; }
+.score-row.primary { opacity: 1; }
+.score-name { font-size: 12.5px; }
+.score-row.primary .score-name { font-weight: 700; }
+.score-track { height: 7px; background: var(--panel-2); border-radius: 4px; overflow: hidden; }
+.score-fill { display: block; height: 100%; border-radius: 4px; transition: width .5s ease; }
+.score-pts { font-family: var(--mono); font-size: 12px; text-align: right; color: var(--dim); }
+
+.panel-subtitle {
+ font-family: var(--mono); font-size: 11px; letter-spacing: .12em;
+ text-transform: uppercase; color: var(--dim); margin: 4px 0 10px;
+}
+
+.fired-rule { padding: 8px 0; border-bottom: 1px dashed var(--line); }
+.fired-rule:last-child { border-bottom: none; }
+.fired-head { display: flex; gap: 8px; align-items: baseline; }
+.fired-title { flex: 1; font-size: 13px; }
+.fired-pts { font-family: var(--mono); font-size: 12px; font-weight: 700; }
+
+/* LLM mode */
+.llm-row { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
+#api-key {
+ flex: 1;
+ min-width: 220px;
+ background: var(--panel-2);
+ color: var(--text);
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ font-family: var(--mono);
+ font-size: 12px;
+ padding: 9px 12px;
+}
+#api-key:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
+.llm-note { font-family: var(--mono); font-size: 10.5px; color: var(--dim); margin-top: 6px; }
+
+/* trace viewer */
+#trace-viewer { display: flex; flex-direction: column; gap: 16px; }
+.trace-section-title {
+ font-family: var(--mono); font-size: 11px; letter-spacing: .12em;
+ text-transform: uppercase; color: var(--dim); margin-bottom: 6px;
+}
+.trace-line {
+ font-family: var(--mono);
+ font-size: 12px;
+ padding: 5px 10px;
+ border-left: 2px solid var(--line);
+ white-space: pre-wrap;
+ word-break: break-word;
+ color: #c9d4d2;
+}
+.line-error { border-left-color: var(--red); color: var(--red); }
+.line-warn { border-left-color: var(--amber); color: var(--amber); }
+
+.flash { animation: flash 1.6s ease; }
+@keyframes flash {
+ 0% { background: rgba(255, 178, 36, .28); }
+ 100% { background: transparent; }
+}
+
+/* toast */
+#toast {
+ position: fixed;
+ bottom: 24px;
+ left: 50%;
+ transform: translate(-50%, 12px);
+ background: var(--panel-2);
+ border: 1px solid var(--amber);
+ color: var(--text);
+ font-family: var(--mono);
+ font-size: 12.5px;
+ padding: 10px 18px;
+ border-radius: 8px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity .25s, transform .25s;
+ max-width: 90vw;
+}
+#toast.show { opacity: 1; transform: translate(-50%, 0); }
+
+footer {
+ margin-top: 56px;
+ padding-top: 18px;
+ border-top: 1px solid var(--line);
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--dim);
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+footer a { color: var(--dim); }
+
+.fw-strip { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 6px 0 22px; }
+.fw-strip-label { font-size: 12px; color: var(--dim); letter-spacing: 0.06em; text-transform: uppercase; }
+.fw-badge { font-family: "IBM Plex Mono", monospace; font-size: 12px; padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px; color: var(--green, #3ecf8e); background: rgba(62, 207, 142, 0.06); }
+
+.upload-label { display: inline-flex; align-items: center; cursor: pointer; }
+.upload-label.small { font-size: 12px; padding: 4px 10px; margin-left: 6px; }
+.format-pill { font-family: "IBM Plex Mono", monospace; font-size: 12px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--blue, #5aa9ff); color: var(--blue, #5aa9ff); }
+
+#graph-scroll { overflow-x: auto; padding: 8px 0; }
+.gnode-label { font-family: "Space Grotesk", sans-serif; font-size: 15px; font-weight: 600; }
+.gnode-tag { font-family: "IBM Plex Mono", monospace; font-size: 10px; letter-spacing: 0.12em; }
+.gnode-bad rect { animation: pulse-bad 1.6s ease-in-out infinite; }
+@keyframes pulse-bad { 50% { stroke-opacity: 0.45; } }
+
+.remedy { display: flex; gap: 12px; align-items: baseline; padding: 8px 0; border-bottom: 1px dashed var(--line); }
+.remedy:last-child { border-bottom: none; }
+.remedy-tag { font-family: "IBM Plex Mono", monospace; font-size: 12px; white-space: nowrap; padding: 3px 9px; border: 1px solid var(--green, #3ecf8e); color: var(--green, #3ecf8e); border-radius: 5px; }
+.remedy-secondary .remedy-tag { border-color: var(--dim); color: var(--dim); }
+.remedy-why { font-size: 13.5px; color: var(--fg); opacity: 0.85; }
+
+.export-row { display: flex; gap: 10px; margin-top: 18px; }
+
+.cmp-pickers { display: flex; align-items: flex-end; gap: 18px; flex-wrap: wrap; margin-bottom: 16px; }
+.cmp-pickers select { background: transparent; color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 7px 10px; font-family: "IBM Plex Mono", monospace; font-size: 12.5px; max-width: 320px; }
+.cmp-vs { font-family: "Space Grotesk", sans-serif; font-weight: 700; color: var(--dim); padding-bottom: 8px; }
+.cmp-table { width: 100%; border-collapse: collapse; margin: 6px 0 18px; font-size: 13.5px; }
+.cmp-table th { text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); padding: 6px 10px; border-bottom: 1px solid var(--line); }
+.cmp-table td { padding: 8px 10px; border-bottom: 1px dashed var(--line); font-family: "IBM Plex Mono", monospace; }
+.cmp-badge { font-size: 11px; padding: 2px 9px; border-radius: 999px; border: 1px solid; }
+.cmp-better { color: var(--green, #3ecf8e); border-color: var(--green, #3ecf8e); }
+.cmp-worse { color: var(--red, #ff4d4d); border-color: var(--red, #ff4d4d); }
+.cmp-same { color: var(--dim); border-color: var(--line); }
+.cmp-changed { color: var(--amber, #f2b544); border-color: var(--amber, #f2b544); }
+.cmp-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 18px; }
+.cmp-rule { font-size: 13px; padding: 6px 0; border-bottom: 1px dashed var(--line); }
+.cmp-solved { color: var(--green, #3ecf8e); }
+.cmp-new { color: var(--red, #ff4d4d); }
+.cmp-open { color: var(--amber, #f2b544); }
diff --git a/kits/AGENT/agent-failure-investigator/docs/adapters.md b/kits/AGENT/agent-failure-investigator/docs/adapters.md
new file mode 100644
index 000000000..41b16638c
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/docs/adapters.md
@@ -0,0 +1,38 @@
+# Import adapters
+
+`js/adapters.js` holds one adapter per framework. An adapter is two functions:
+
+| Function | Contract |
+|---|---|
+| `claim(doc)` | Cheap structural sniff — returns true if the JSON looks like this framework's export. Adapters are tried in order; the first claim wins. |
+| `translate(doc)` | Best-effort mapping onto the canonical trace schema (`docs/trace-schema.md`). Missing sections stay empty; the rule engine tolerates that. |
+
+## Detection signatures
+
+| Format | Claimed when |
+|---|---|
+| Native | Object with `final_response` or `tool_calls` (and no `spans`) |
+| LangGraph | Array (or `{events}`) of `astream_events` items: `event: "on_*"` or `metadata.langgraph_node` |
+| OpenAI Agents SDK | `spans[]` where items carry `span_data.type` (`agent`, `function`, `generation`, `guardrail`, `handoff`) |
+| CrewAI | `tasks[]` plus a `crew` or agents with `role`/`goal` |
+| AutoGen | `chat_history[]` (or bare message array) with `name` / `tool_calls` / `function_call` fields |
+| Lamatic | `nodes[]` plus `flowId` / `executionId` / `workflowId` |
+
+## Field mapping highlights
+
+- **Tool calls**: LangGraph `on_tool_start/end/error` pairs, OpenAI `function` spans, CrewAI `tool_calls`/`tools_used`, AutoGen `tool_calls` → `role:"tool"` replies, Lamatic `toolNode`/`apiNode`/`codeNode` executions. Error text containing "timeout" maps to `status:"timeout"`, anything else failing maps to `status:"error"`.
+- **Retrieval**: LangGraph `on_retriever_end` documents and Lamatic `ragNode`/`vector` node outputs become `retrieved_docs` with their scores.
+- **System prompt**: `system` messages (LangGraph, OpenAI, AutoGen), agent `role/goal/backstory` (CrewAI), `config.systemPrompt` (Lamatic).
+- **Final response**: the last assistant generation / task output / `llmNode` text.
+- **Logs**: guardrails, handoffs, node lifecycle, span errors, and fallback markers land in `logs` so the timeline and fallback rules see them.
+- **Timestamps**: ISO strings and epoch numbers are folded to `HH:MM:SS`; ordering is what matters to the engine.
+
+## Adding a framework
+
+```js
+new Adapter("myframework", "My Framework",
+ doc => Array.isArray(doc.runs) && doc.runs.some(r => r.step_type),
+ doc => { /* return an object in the canonical schema */ });
+```
+
+Register it before the stricter formats if its signature could shadow theirs, add a fixture to `tests/run-tests.js`, done.
diff --git a/kits/AGENT/agent-failure-investigator/docs/architecture.svg b/kits/AGENT/agent-failure-investigator/docs/architecture.svg
new file mode 100644
index 000000000..9b83678d4
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/docs/architecture.svg
@@ -0,0 +1,162 @@
+
diff --git a/kits/AGENT/agent-failure-investigator/docs/trace-schema.md b/kits/AGENT/agent-failure-investigator/docs/trace-schema.md
new file mode 100644
index 000000000..00e29a62c
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/docs/trace-schema.md
@@ -0,0 +1,39 @@
+# Trace schema
+
+A trace is one agent run, captured as a single JSON object. Every field the rule engine reads is documented here; unknown fields are ignored, and every section except `final_response` may be empty.
+
+## Top-level fields
+
+| Field | Type | Purpose |
+|---|---|---|
+| `meta` | object | Display only: `agent`, `flow_id`, `model`, `session`, `date`. |
+| `system_prompt` | string | The agent's instructions. Read by the prompt-ambiguity rules (R31, R32). |
+| `conversation` | array | Messages: `{ role: "user"\|"assistant", ts, content }`. The last user message defines the task for tool-selection rules (R41, R42). |
+| `available_tools` | array | `{ name, description }`. The full toolbox the agent could have used — required for wrong-tool detection. |
+| `tool_calls` | array | See below. |
+| `retrieved_docs` | array | `{ id, source, score, content }`. Retrieval results that reached the context. Read by RAG rules (R21, R23) and the groundedness rule (R22). |
+| `logs` | array | `{ ts, level: "INFO"\|"WARN"\|"ERROR", event, message }`. Infrastructure events. Fallbacks, empty-retrieval warnings and output-instability warnings are recognized by event name (`flow.fallback`, `retrieval.empty`, `output.validation`). |
+| `final_response` | object | `{ ts, content }`. The answer the user actually received — the "body" of the investigation. |
+
+## Tool call fields
+
+```json
+{
+ "ts": "10:33:02",
+ "tool": "get_order_status",
+ "input": { "order_id": "A-58291" },
+ "status": "success",
+ "duration_ms": 214,
+ "output": "{\"status\":\"in_transit\",\"eta\":\"2026-07-12\"}"
+}
+```
+
+`status` must be one of `success`, `error`, `timeout`. `output` is a string (stringify structured results). Timeouts and errors feed the tool-failure rules (R11, R12); successful outputs feed the groundedness corpus for R14 and R22.
+
+## Timestamps
+
+`ts` is a `HH:MM:SS` string. Timestamps only need to sort correctly relative to each other — the timeline is reconstructed by ordering, not by wall-clock math (except timeout endpoints, computed from `duration_ms`).
+
+## Design note
+
+The schema mirrors the request lifecycle that agent platforms already log: prompt in, tools and retrieval in the middle, response out. Mapping a platform's native run log onto this shape is an adapter, not a redesign — which is the point.
diff --git a/kits/AGENT/agent-failure-investigator/index.html b/kits/AGENT/agent-failure-investigator/index.html
new file mode 100644
index 000000000..7210626f6
--- /dev/null
+++ b/kits/AGENT/agent-failure-investigator/index.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+ Agent Failure Investigator
+
+
+
+
+
+
+
+
+
Diagnostic tooling for agent developers
+
Agent Failure Investigator
+
+ Every AI team asks the same question: why did the agent fail?
+ Load a trace — conversation, logs, tool calls, retrieved docs, final response —
+ and get a deterministic failure report with evidence you can click.
+