<!-- analyzed-at: d9afbaa @ 2026-07-02 | model: fable-5 -->
# agentfootprint — feature-work map

Agent framework layered on footprintjs: every runner (Agent, LLMCall, compositions, patterns) is a footprintjs chart built ONCE at construction and executed on a fresh `FlowChartExecutor` per run. Engine seams (stage kinds, $-methods, engine handlers) live UPSTREAM in footprintjs and are closed here. This file maps this repo's seams and blast radius — **trust the code** where any doc disagrees.

## Before you design it: it may already exist

**Read this table before proposing any new capability.** Everything below already
ships. The failure it exists to stop is real and expensive: a reader searches for
the words THEY would use, finds nothing, and designs a feature the library has had
for releases. That has happened repeatedly — the typed-HITL ask, element bindings
by role and name, and the artifact-kind renderer were each re-proposed after they
shipped.

Keyed by **what you would call it**, not by what it is called here. If your idea is
not in this table, search `src/index.ts` for the nearest noun before writing code.

| If you are about to build… | It is | Where | Since |
|---|---|---|---|
| guiding an evidence repair to ask for missing context without inventing values | `recoveryInstruction` + `EvidenceRecoveryContext` | `src/core/agent/evidence/` | 9.96.0 |
| checking a typed final answer against scoped evidence before streaming or memory delivery — optional host-authored checks with enforce/observe modes, canonical JSON and bounded read-only artifacts | `answerValidation` + `AnswerValidationError` | `src/answer-validation/` | 9.95.0 |
| turning a written operational procedure (a runbook, a triage playbook) into ONE agent tool whose every answer is EVIDENCE — coverage folded up from the inner tools it calls, rule name+version, verdict rows with GENERATED meanings, and the recorded walk as an artifact ticket, never bytes | `runbookAsTool` — dials: `procedure` (factory, invoked per call with `ctx.tools`) + `resultKind` (`'verdict/*'` arms the rowset projection; anything else ships spine + the chart's `report`) + `rules {name, version}` (default absent ⇒ `rule_version: 'undeclared'`) + `verdicts {decider, maxRows}` (default 50; `verdict_meanings` = statically declared branches + this run's rule labels + the DEFAULT branch's label, which reaches evidence only when the chart calls `decide(s, rules, {branch, label})` — fp ≥9.16.1, the default is chosen by NO rule so nothing else can name it, and inside a generated fan-out branch the static walk is blind too; an undeclared or blank label stays ABSENT from the map, never invented from a branch id, and there is deliberately NO caller-supplied meanings map) + `presentation` (`'prose'` default = ship `table` + `VERDICT_RENDER_NOTE` "output it VERBATIM"; `'panel'` = the HOST renders the rowset, so NO `table` key at all + `PANEL_RENDER_NOTE` "the rows are already on the reader's screen — do not reproduce them"; the rowset half is byte-identical across modes, `table` stays RESERVED in both, an unknown value THROWS at definition) + `walk {cap, recording}` (cap default 500; over-cap ⇒ control-flow projection, declared · `recording: true` or `{label, maxBytes}`, default OFF — ALSO files the inner chart's own `{snapshot, events, structure}` under `recording/run` and puts its ref on the spine as `walk.recording_ref`, because the ROW projection cannot be drawn; snapshot read from the REDACTED mirror so one `redact` means the same for both; over `maxBytes` (`DEFAULT_RECORDING_MAX_BYTES` = 5,000,000) it is REFUSED not truncated; every absence — no store / over size / unserializable / store threw — is STATED in `walk.recording_note`, and the four `recording_*` fields are absent entirely when the dial is off) + `composedOf` (drift-checked at agent BUILD) + kept `recorders`/`keepRecord`/`keepRecordLimit`/`redact`; reserved state keys `verdicts`/`coverage`/`report` (the `report` bag lands BESIDE the spine, never over it — a report field spelling `af_coverage`/`af_provenance`/`rule_version`/`walk`/`report_note` or a live projection key is discarded and NAMED in `result.report_note`; precedence is explicit in `report.ts`, never spread order); inner `absent()` passes through VERBATIM unless the call said `allowAbsent`; walk kind `recording/chart-walk` + `walk_segment` discriminant | `src/core/runbook/` | 9.76.0, recording 9.79.0 |
| speaking MCP from a BROWSER — the SDK is browser-clean (its client + streamableHttp bundle at `platform:'browser'` with zero `node:` edges and never pull in `client/stdio.js`); the one barrier was that `lazyRequire` gets CALLED there, so the fix is to let the caller supply what the loader would have found | `mcpClient({ sdk })` (`McpSdk` = the two SDK modules you imported statically; the library STILL builds the transport, so headers/fetch/gateway vending/`retryOnThrottle`/`_meta` all keep working) · `mcpClient({ connection })` (`McpConnection` = listTools/callTool/close, deliberately NO `connect` — you already did; the only arm that reaches the SDK's `jsonSchemaValidator`, i.e. CSP) + `McpConnectionOptions`; `refuseConflictingOptions` REFUSES at construction every option a transport would have consumed, naming where it moved (a knob naming a behaviour that no longer happens is the defect class); `transportUrl` resolves a relative `url` against `globalThis.location.href` (absolute takes the identical branch, Node refuses by name); `sdkLoadFailure` classifies the seven load sites so a browser is never told to install a package it has (resolution failure ⇒ byte-identical historical message); `retryingFetch` + `ThrottleFetch` promoted out of `@internal` so the connection arm keeps its 429 handling. stdio keeps `lazyRequire` FOREVER — it spawns a subprocess. ZERO packaging change: no subpath, no `browser` condition (TypeScript is blind to it), peer stays optional (no literal dynamic `import()`). Fenced by `test/lib/mcp/browserGraph.test.ts` | `src/lib/mcp/` | 9.81.0 |
| calling ANOTHER registered tool from inside a tool's `execute` — composition over the agent's own dispatch map instead of importing the module and building a second query stack | `agentToolDispatch` + `ctx.tools` (ToolDispatch has/call) — sees static and skill-carried tools, NEVER ToolProvider-delivered ones (no build-time list, the 9.72.0 caveat); inner calls get the outer facts with hasArtifacts false and a derived toolCallId, `needs` resolved fail-closed non-interactively, `checkIn` and `wants` tools refused by name, no nested dispatch; declare ingredients via `composedOf` (and `gates` for a pausing procedure) — both travel MCP `_meta` | `src/core/agent/toolDispatch.ts` | 9.76.0 |
| a tool returning numbers WITH the caveats that make them honest — interval/aggregation grain, is-it-a-counter, when the world was measured, which ground was NOT covered — as typed data the model reads compactly and the record keeps whole | `semantic()` + `tools.semantics_declared` (model sees `semanticsForModel` projection; `coverage` field absorbed by the coverage()/absent() channel) | `src/lib/semantics/` | 9.53.0 |
| a build gate that refuses a triage/inventory tool that forgot its caveats, by tool name and field name | `checkSemantics` + `defineTool({ resultClass })` (`'triage'`/`'inventory'`, the closed set) + bin `agentfootprint-check-semantics` | `src/lib/semantics/check.ts` | 9.53.0 |
| an agent that runs out of `maxIterations` mid-task handing back a HALF-SENTENCE as its answer — and nothing saying the budget ran out | `wrapUpAtMaxIterations` (default ON) + `WRAP_UP_INSTRUCTION` + `agent.budget_exhausted` + `stoppedEarly.wrappedUp` | `src/core/agent/stages/wrapUp.ts` | 9.56.0 |
| the conversation outgrowing the context window — dropping, summarizing or token-budgeting old turns, every removal on the record (the three refusal rows below all guard this window) | `.window()` + `slidingWindow` / `summarizeOldest` (= `.compaction()`) / `tokenBudget` + `keepRecentTurns` (default 6) + `thresholdTokens` (required, deliberately no default) + `retain: 'conversation'` (default) / `'discard'` | `src/core/agent/window/` | 7.17.0 |
| keeping the user's own request in context while the window shrinks — the task's anchor, un-droppable by every window strategy including one you wrote | `'current-request'` refusal + `currentRequestIndexOf` + `WindowStrategyInput.currentRequestIndex` | `src/core/agent/window/currentRequest.ts` | 9.55.0 |
| the window evicting the EVIDENCE while keeping the task — the tool result carrying the only valid ids leaves, and the model invents one that has never existed | `keepLastToolResults` (default 2; `0`/`false` disables) + `'last-tool-result'` refusal + `toolResultPinsOf` + `WindowRecord.observations` (what was kept, and its exact char cost) | `src/core/agent/window/lastToolResult.ts` | 9.57.0 |
| telling the model WHICH tools' results a drop took, so it re-calls instead of reconstructing from memory — and filing the same fact on the record even when no notice was authored | `droppedToolNames` + the notice's tool sentence + `WindowRecord.droppedObservations` | `src/core/agent/window/toolNames.ts` | 9.57.0 |
| an instruction that SAYS a run-time number rather than only gating on it ("you are on action 25 of 30") — with the library owning absence, so never "23 of undefined" and never a fabricated zero | `promptTemplate` + the closed `TEMPLATE_FACTS` vocabulary (`action`/`actionBudget`/`actionsRemaining`) + `skipped: 'unknown-fact'` + `Injection.templated` | `src/lib/injection-engine/promptTemplate.ts` | 9.57.0 |
| an injection predicate that can see how much of the action budget is left | `InjectionContext.maxIterations` / `.iterationsRemaining` + `iterationsRemainingOf` (the ONE denominator, shared with the cache decision) | `src/lib/iterationBudget.ts` | 9.57.0 |
| report progress from inside a tool — "hop 3 of 12 done", said mid-call while a long-running `execute` is still working (the record was otherwise atomic: tool_start, silence, tool_end) | `ctx.progress` + `agentfootprint.stream.tool_progress` | `src/core/tools.ts` | 9.52.0 |
| show tool progress to the user — a mid-call report reaching the live status line / chat bubble, not just the record (`message` shown verbatim, capped at 120 chars with the cut stated; otherwise an honest generic line, never a payload dump) | `selectStatus` + `progressMessageOf` + `tool.progress` templates | `src/recorders/observability/status/statusTemplates.ts` | 9.54.0 |
| declaring which skills connect — the SkillMap as one named thing (the agent that mounts it is the SkillWalker; there is deliberately no walker class) | `defineSkillMap` + `SkillMap` (permanent reference-equal aliases of `skillGraph`/`SkillGraph`) | `src/lib/injection-engine/skillGraph.ts` | 9.51.0 |
| guarding a skill transition on state or a tool result's fields — a route condition as DATA (comparable, drawable, evidence-recorded), not an opaque predicate | `guard:` + `compileGuard` (ops `'eq'`/`'ne'`/`'gt'`/`'gte'`/`'lt'`/`'lte'`/`'in'`/`'notIn'`) + `guard-unsatisfiable` | `src/lib/injection-engine/skillGuard.ts` | 9.51.0 |
| seeing the skill map in a recording — the author's nodes + edges (guards included) as DATA, never parsed from prose or inferred from fired hops | `skill.graph_declared` + `buildSkillGraphDeclared` | `src/core/agent/skillGraphDeclared.ts` | 9.50.0 |
| "where could the run go next?" — the reachable skill set at every cursor move, typed on the move itself | `cursorMove.reachable` + `reachableSkills` | `src/lib/injection-engine/buildInjectionEngineSubflow.ts` | 9.50.0 |
| putting the ASSEMBLED system prompt in the recording — the exact string the model read (opt-in; default OFF is a privacy decision) | `recordSystemPrompt` | `src/core/agent/types.ts` | 9.50.0 |
| a typed HITL prompt — let a person pick from a list, choose a range, use a real control instead of typing prose | `AskComponent` + `componentId` (consumer-registered vocabulary, opaque here) + inline `props` + `propsRef` (artifact claim ticket for the big half) | `src/core/askComponent.ts` | 9.24.0 |
| carrying WHAT the person chose back, with what they could see | `DecisionValue` (`kind` / `value` / `from?` + `coverage {seen, total, filter?}`) | `src/core/checkin.ts` | 9.47.0 |
| archiving a finished run — filing it, attaching it to a bug report, feeding it to an analysis tool | `persistRecording` + `RecordingEnvelope` + `run.complete` (required) + `privacy.mode` (`'full'` today; `'structure-only'`/`'redacted'` refuse by name) | `src/recorders/observability/recordingEnvelope.ts` | 9.48.0 |
| packaging a run for a HUMAN to file — a zip whose evidence is the archive envelope, plus host facts and a readable transcript | `exportBugReport` + `include` (the consent seam; left-out units are counted in `manifest.excluded`) + `warnOverBytes` (default 20 MB) | `src/lib/bug-report/build.ts` | — |
| writing archived runs somewhere — a directory, one JSON file per run | `fileRecordingSink` + `RecordingSink` | `src/recorders/observability/fileRecordingSink.ts` | 9.48.0 |
| declaring an agent's whole setup as one named, versioned thing — a preset, a template, a blueprint, "the support agent we all use" | `defineAgentRecipe` + `AgentBuilder.recipe()` | `src/recipes/` | 9.48.0 |
| naming your own branches and nodes — which prefix is reserved, and telling framework plumbing from consumer structure in a trace | `RESERVED_SUBFLOW_PREFIX` + `isReservedSubflowSegment` | `src/conventions.ts` | 9.49.0 |
| keeping a large tool result out of the model's context | `artifacts` (bare store, or `{ store, placement: { maxInlineChars }, recordings }` — `placement` omitted ⇒ never placed; `recordings: true` / `{ label }`, default off) + `wants` + `placement` | `src/artifacts/` | 9.21.0 |
| the ticket placement just minted being REFUSED by your own `wants` argument as a kind mismatch — because the mint says `tool-result/<toolName>` and your consumer says `dataset/rows` | `Tool.resultKind` + `placedResultKind(toolName, declared?)` (the mint speaks the consumer's vocabulary; the exact-match matcher is untouched) | `src/artifacts/placement.ts` | 9.70.0 |
| letting the UI draw an artifact without the model naming a component | `registerArtifactComponent` (in the `agentfootprint-lens` package) | — | — |
| finding which tools the model keeps writing by hand | `agentfootprint.tools.code_run` + `codeShape` | `src/core/codeRunnerTool.ts` | 9.46.0 |
| a skill wrongly activated by a keyword staying loaded for the whole turn — suspending/parking a map's prompt + tools when its contribution goes unused, WITHOUT touching its cursor, and re-engaging it on evidence | `.maps({ renewalGrace })` (default 3) + `MountedMap.nonParkable` + `advanceEngagement` + `agentfootprint.map.engaged/parked` + skip reason `'parked'` | `src/maps/` | 9.58.0 |
| a value that must say how it knows itself — an unknown count that can never render as zero | `Claim<T>` (`known`/`unknown`/`notApplicable`) | `src/lib/claim/claim.ts` | 9.58.0 |
| asking a debugging model "what did this run contradict itself about, and why?" — the Context Integrity findings, joined to the step that filed them | `find_context_errors` | `src/lib/trace-toolpack/traceToolpack.ts` | 9.61.0 |
| the model inventing an id or a reading that no tool ever returned — a deterministic fabrication detector on the final answer, with a posture for how hard it pushes back | `.namesAndNumbersFromEvidence()` + `posture` (`'assist'` record+flag, the default / `'guard'` one revision then ship flagged / `'rails'` refuse instead) + `shapes` + `exempt` + `minDigits` (default 4) + `nudge` (default off) | `src/core/agent/evidence/` | 9.35.0 |
| counting a HUMAN-VERIFIED value as ground for the choice-seam check — the person clicked a row, the app verified the cells against the artifact, and the id the model takes from that selection is not fabricated; the source label travels onto the record | `externalGrounds` + `ExternalGround` + `external_ground_used` | `src/integrity/unsupported-argument/check.ts` | 9.72.0 |
| a lookup tool that answers "nothing found" for EVERY id because a filter broke, and an agent reporting that absence as fact — the run itself produced the id, the lookup came back empty, and that PAIR is worth a look | `noticeEmptyLookups` + `readLookupResult` + `EMPTY_LOOKUP_CEILING` (dial default OFF and needs a tool declaring `argumentsFrom` too — two halves; kind `empty-lookup` at seam `write`, always `advisory: true`; empty = a zero-length ARRAY or an `absent()` envelope, every other shape files `not-applicable` and no finding; the ceiling is quoted verbatim into every message) | `src/integrity/empty-lookup/` | 9.77.0 |
| an agent answering a NEW question out of an OLD turn's tool results — grounded, four turns stale, and the evidence gate approving it (`Tool calls 0` and "all 7 values were found in what the tools returned"). ALSO the bug half: the gate's two sentences claimed the flagged values "appear in no tool result FROM THIS TURN" while its index walked every `role:'tool'` turn — a boundary asserted and never measured; both now say "no tool result this run read" | `noticePriorTurnEvidence` + `PRIOR_TURN_EVIDENCE_CEILING` + `priorTurnEvidenceOf` + `AnswerGroundingReading` (dial default OFF and needs `.namesAndNumbersFromEvidence()` too — two halves, and the second is structural: the gate owns the extractor that decides which tokens are values; kind `prior-turn-evidence` at seam `claim`, always `advisory: true`; `EvidenceCorpus.values` became a `Map<form, turn>` stamped in the walk that was already happening, a TURN = each `role:'user'` message `isLibraryAuthoredTurn` did NOT write (counting the gate's own correction would file against every revised answer); fires when ≥1 value is grounded and NOT ONE came from the turn being answered — ONE current-turn value files nothing, which is what keeps an honest follow-up quiet and is the design's falsification test; the zero-tool-call turn is the SAME kind with a stronger witness, not a second kind; corpus is the LIVE WINDOW so ordinals are window-relative and the distance is a FLOOR, while the boundary stays exact (`'current-request'` is un-droppable); memory/RAG values are exempt from grounding and invisible — it can under-report, never over-report; the ceiling is quoted verbatim into every message) | `src/integrity/prior-turn-evidence/` | 9.83.0 |
| a tool's ROWS quietly disagreeing with what the tool promised — a LUN 0 stored as `""` because it is falsy, a numeric column arriving as quoted strings, a declared column present in no row at all | `Tool.resultColumns` + `checkColumnTypes` + `COLUMN_TYPE_CEILING` + `readRowset` (dial `'off'` (default) / `'warn'` (file findings, model reads the rows unchanged) / `'enforce'` (refuse the rows, `resultCeiling`'s teaching-sentence idiom, delivered status `'invalid'`) — the `toolArgsValidation` trio, this seam's mirror; needs a tool declaring `resultColumns` too — two halves; types `number`/`string`/`boolean`/`date`, bare word or `{type, nullable}`, deliberately NO `'unknown'`; OPEN — unlisted columns allowed and unjudged; TWO kinds at seam `write` — `column-type-mismatch` (there, wrong type) vs `missing-column` (declared, in no row); rowset = an ARRAY OF PLAIN OBJECTS with ≥1 row, everything else incl. the ZERO-ROW result files `not-applicable` (empty is `empty-lookup`'s subject); travels MCP `_meta`; the ceiling is quoted verbatim into every message) | `src/integrity/column-types/` | 9.78.0 |
| a model head-mathing a total from tool-result numbers while a compute tool sits unused on the wire — a LATE line each iteration naming the staged refs and the `wants` tool that spends them (recency working FOR the instruction), plus the revise correction naming the same route | `nudge: true` (default off) + `stagedRefsNudgeLine` + `findStagedRefs` + `grounding_nudged` | `src/core/agent/stagedRefs.ts` | 9.75.0 |
| a tool answering "I looked and found nothing", routably | `absent` + `looked_for` + `checked`/`not_checked`/`cannot_cover` + `tryInstead` (delivered status `'absent'`, routable by `onToolStatus`) | `src/core/agent/coverage/absent.ts` | 9.43.0 |
| stating what a clean answer does NOT rule out | `coverage` + the declaration `{ checked, not_checked?, cannot_cover? }` | `src/core/agent/coverage/ledger.ts` | 9.43.0 |
| minting one of those shapes from a tool that is NOT JavaScript — the canonical note sentences and reserved marker keys as DATA, so a Python/Go/Rust sidecar reads a file instead of regex-scraping `dist/esm` (which a consumer really did) | `canonical-notes.json` at the package root + the `./canonical-notes.json` exports entry, GENERATED from the built barrel by `scripts/gen-canonical-notes.mjs` | `scripts/gen-canonical-notes.mjs` | 9.70.0 |
| proving a session store really honours the port | `runSessionLifecycleConformance` + `declared` skips (by case name WITH the reason; a declared case still RUNS) + outcomes `'passed'`/`'not-applicable'`/`'declared'`/`'failed'` | `src/hosting/conformance/` | 9.37.0 |
| deciding who owns a contested session write | `resolveSessionOwner` | `src/hosting/sessionOwnership.ts` | 9.37.0 |
| making a caller-supplied id safe for a backend | `encodeIdentityField` | `src/memory/identity/encode.ts` | 9.37.0 |
| scoping a skill's tools so they reach the model only while it is active | `toolsFromActiveSkill` + a no-arg posture (default off) + the stamp `autoActivate: 'currentSkill'` (a default, never an override) | `src/core/agent/toolsFromActiveSkill.ts` | 9.36.0 |
| subscribing to every event in one domain at once | `DomainWildcard` | `src/events/dispatcher.ts` | 9.4.0 |
| pausing a run for a person and resuming it later | `checkInApproved` / `checkInDeclined` (`{ by, note?, value? }`; a decline is NOT an abort — the model gets a "declined by human" tool result and adapts in-loop) | `src/core/checkin.ts` | 7.5.0 |
| a SECOND source judging every tool result — a calibrated classifier asked what a result is worth for the declared proposition (never why the tool was called), its distribution and cost on the record beside the model's own standing, never merged, never served in its place (policy A) | `.findings({ judge })` + `Classifier` / `typesafe` / `mockClassifier` (`agentfootprint/classify`) + `JudgmentRow` / `JudgmentErrorRow` + `classifierScorer` (the scored entry choice, `agentfootprint/skill-graph`) | `src/classify/` | 9.104.0 |
| a classifier's reading of WHICH TOOL beside every model call — the ranking, the pick and the cost on the record before the call, the model's call and the agreement after it; optionally the served list narrowed to the top-N plus the doors, on the committed record the receipt hashes and the served view rebuilds; a miss recorded and the full wire served next call, never a refusal, never an override | `.toolChoice({ classifier, serve, alwaysServe })` + `ToolChoiceRow` + `ToolChoiceOutcomeRow` + `ToolChoiceErrorRow` + `ALWAYS_SERVED_TOOLS` + `recordToolChoice` + `narrowServed` (events `agentfootprint.tool_choice.*`, bench `npm run bench:tool-choice`) | `src/core/agent/toolChoice/` | 9.105.0 |
| a MAP of the domain the model can reason over when a result holds nothing — what each term IS (meaning, unit), how terms RELATE, which SOURCE holds a term (with the author's coverage sentence) and which registered TOOL reads it from there; declared once, validated against the tool registry at build, on the record as one run constant, served as one request-only system piece per call, never a fetch path and never inferred from | `defineOntology` + `.ontology(map)` (`AgentOptions.ontology`) + `AgentState.ontology` + `ontologyPiece` / `ONTOLOGY_INSTRUCTION` (`agentfootprint/ontology`) + `agentfootprint.ontology.served` | `src/ontology/` | 9.106.0 |
| a value the model USED — in its answer, or as an argument of a later call — that came ONLY from results it had itself declared open, noise or ruled-out (a theorem tower on a set-aside lemma): declared standings joined to the evidence corpus's carriers, no inference, no judge; one row per value per moment through the one writer; served back under `contingent (read off the record):`; cache reads summed on the record beside the input tokens | `ContingentRow` / `ContingentCarrier` + `agentfootprint.findings.contingent` + `findingsInstructionFor` (`FINDINGS_CONTINGENT_LINE`, registered under BOTH `.findings()` and `.namesAndNumbersFromEvidence()`, never one) + `EvidenceCorpus.carriers` / `EvidenceVerdict.grounded` + `AgentState.totalCacheReadTokens` | `src/core/agent/findings/contingent.ts` | 9.110.0 |

**One law before you add a mapping.** Any function turning caller data into a KEY,
a namespace, a filename or an index entry must be injective, and its collision
check ships in the SAME change — not the release that discovers it. Six defects of
that exact shape were fixed between 9.37.0 and 9.46.0. Use `encodeIdentityField`
rather than writing a seventh. The ONE acceptable alternative is refuse-by-domain —
assert a safe charset and refuse everything else by name, as `fileRecordingSink`
does for internal runIds — because a refusal cannot collide. What is never
acceptable is a third thing: a fold that quietly rewrites.

**Maintaining this table:** a new public capability adds a row.
`test/docs/capability-index.test.ts` fails if a row names a file or symbol that
does not exist, so the table can go out of date by OMISSION but can never lie
about what it names.

## Module map
Entry points — SEVENTEEN doors, and `package.json` `exports` is exhaustive, so a path not on this list does not resolve at all: `.` core API · `/providers` everything you plug a backend into (mock/anthropic/openai/bedrock/browser*, embedders, staticTools/gatedTools/skillScopedTools/mcpClient, thinking handlers, code runners) · `/memory` (defineMemory, MEMORY_TYPES, InMemoryStore, mockEmbedder, redis/agentcore/bedrockAgentMemory stores) · `/rag` index-time loaders/splitters/indexCorpus · `/cache` prompt caching (+ registerCacheStrategy; side-effectful, hence its own door) · `/observe` the whole watching story (recorders, recordRun, strategies + attach*, vendor sinks, run-autopsy finders, toSSE, status, locales) · `/events` typed event system · `/context` the injection engine (defineInjection/Skill/Fact/Instruction/Steering, decideSkill) · `/resilience` provider decorators + the reliability RULES · `/hosting` nodeHost/httpHost/standingAgent + session stores · `/security` permissions + tool credentials (it absorbed the old identity door) · `/reliability` the retained alias, kept ONLY because it is the sole home of the gate's `CircuitOpenError` · `/skill-graph` the framework-neutral routing layer · `/recipes` the declared unit of agent CONFIGURATION (defineAgentRecipe → `.recipe()`; authoring-time vocabulary, so it stays off the main barrel) · `/maps` the mount kernel's vocabulary (9.58.0: Claim<T> + the engagement lease machine + its renewal feed — pure data and pure functions, no run entry point; mounted via `.maps()`) · `/classify` the calibrated-classifier port (9.104.0: `Classifier`, `typesafe`, `mockClassifier`, `ClassifierError` — a backend that SCORES declared candidates; spent by `.findings({ judge })` and by `classifierScorer` on `/skill-graph`) · `/ontology` the declared map (9.106.0: `defineOntology`, `ontologyHash`, `ontologyPiece`, `ONTOLOGY_INSTRUCTION`, the shapes — what each term IS, how terms RELATE, which SOURCE holds a term and which registered TOOL reads it; pure data and pure functions, no run entry point, no fetch path; mounted via `.ontology()`, served as one request-only system piece per call; 9.112.0 `fromSkos`/`readSkos`/`toSkos` — the SKOS JSON-LD adapter onto the same spec, no second validation path). 9.0.0 removed sixteen older paths (`/llm-providers`, `/injection-engine`, `/tool-providers`, `/strategies`, `/observability-providers`, `/identity`, `/stream`, `/thinking`, `/status`, `/locales`, `/debug`, `/debug/finders`, `/memory-providers`, `/embedders`, `/hosting-providers`, `/observability/contextError/finders`) — it removed PATHS, not code; each name still ships through the door that absorbed it. This list is pinned against `package.json` by test/api-conformance/documented-doors.test.ts, and the removed sixteen by subpath-exports.test.ts. **Main barrel does NOT export** mock/browser*/defineMemory/defineSkill/skillGraph/mcpClient/InMemoryStore — import from the doors above.

| src/ | one job |
|---|---|
| answer-validation/ | optional final-answer contract, scoped bounded artifact reads, canonical JSON and explicit check dispositions; the Agent route owns invocation and final capture owns delivery |
| core/ | primitives: Agent.ts (ReAct runner), LLMCall.ts, RunnerBase.ts (dispatcher + attach + enable.*), tools.ts (defineTool), pause.ts, runCheckpoint.ts |
| core/agent/evidence/ | (9.35.0) the evidence gate — `.namesAndNumbersFromEvidence()`. Pure + deterministic BY LAW (no model, no embedding: a guard that needed a bigger model to police a smaller one inverts the library's thesis). normalize (one spelling per value, both sides) → extract (which tokens are DATA — conservative, justified clause by clause) → evidenceIndex (STRUCTURAL walk of `role:'tool'` results; the exempt corpus is user message + user/system turns + `systemPromptInjections`, MINUS the library's own frames — see frames.ts, the laundering bug) → gate (resolve/judge/sentences) → errors (`UnsupportedValuesError`). Fabrication detector, NOT a correctness judge: a false claim built from real values passes |
| core/agent/coverage/ | the two FIELD-INVENTED result primitives. `absent()` = an absence that names its own coverage (checked / not checked / cannot cover) and says a retry returns the same; `coverage(result, …)` = the ledger of what a clean verdict does NOT rule out. Both RECOGNIZED (reserved `af_absent`/`af_coverage` keys, the effects-envelope strictness law), not conventions. Pure; the moving parts are `declareCoverage` in stages/toolCalls.ts, the whitelist in evidence/evidenceIndex.ts, and `prepareFinalWithLimitsStage` |
| core/agent/ | chart assembly: buildAgentChart / buildDynamicAgentChart (picked by reactMode, Agent.ts:1145-1147), buildToolRegistry, stages/ (seed, callLLM, route, toolCalls, prepareFinal, breakFinal, reliabilityExecution), window/ (the `.window()` strategy family), middleware/ (the `.toolMiddleware()` / `.messageMiddleware()` chains — outcomes are a CLOSED union with no `result` arm; three call sites walk it via runChain.ts: toolCalls dispatch, seed `'input'`, route `'output'`, plus mcpServe) |
| core/slots/ | the 3 context-slot subflow builders + thinking subflow — intentionally NOT exported |
| core-flow/ | Sequence/Parallel/Conditional/Loop — RunnerBase subclasses with own charts |
| patterns/ | Debate/MapReduce/Reflection/SelfConsistency/Swarm/ToT — pure composition of runners, no new control flow |
| adapters/ | hexagonal ports (types.ts = ALL port interfaces — one exception: `RecordingSink` lives beside `recordRun` in recorders/observability/, per "anything that saves a run goes through it") + vendor impls (llm/, memory/, identity/, observability/). memory/sqliteVector.ts (8.9.0) = the only FULL MemoryStore we ship with `search` besides InMemoryStore — exact cosine over a resident Float32Array matrix, hydrated per namespace on first search and dropped on any write to it |
| recorders/core/ | bridges footprintjs events → typed EventDispatcher (ContextRecorder, EmitBridge, typedEmit) — auto-attached by Agent.createExecutor; most factories also exported via `/observe` for manual wiring (EmitBridge itself stays internal) |
| recorders/observability/ | consumer recorders over the typed stream (RunStepRecorder, FlowchartRecorder, Status, Trace replay) + `recordRun` — THE producer of a recording `{snapshot, events, structure}` (the shape lens's `observeRecording` consumes; `structure` = `getSpec().buildTimeStructure`, which no snapshot carries). Anything that saves a run goes through it |
| rag/ | (8.10.0, door `/rag`) index-TIME: `DocumentLoader` adapters (text/markdown/html zero-dep, pdf via lazy `unpdf`) + `Splitter` factories + `indexCorpus` — a REAL footprintjs chart whose commit log IS the indexing report. `defineRAG` deliberately stays on the MAIN barrel (run-time wiring); this door is the half that runs once, before any agent exists |
| ontology/ | (9.106.0) the declared map — `defineOntology` (validate, detach, freeze, `ontologyHash` through the receipt's `stableJson`) + `serve.ts · ontologyPiece` (the ONE composer, called by `callLLM` and by `servedView.viewOf`; every line quotes the declaration under a constant header that quotes the context contract's three meanings) + `instruction.ts` (the always-on ask). Behind the `/ontology` door; `Agent.buildChart` checks every `via` tool name against `registryByName` at build; `seed` writes `AgentState.ontology` once; the grouped chart crosses the key into `sf-llm-call` under `hasOntology`. 9.112.0 `fromSkos.ts`/`toSkos.ts`/`skosJsonLd.ts` = the SKOS adapter (a customer's concept scheme → `OntologySpec` → `defineOntology`, and back); `define.ts`/`serve.ts`/`instruction.ts`/`score.ts`/`types.ts` untouched by it, pinned by test/ontology/fromSkos.test.ts |
| lib/ | first-party sub-libraries: injection-engine/, context-bisect/ (localizeContextBug, toBacktrackTrace + sliceToBacktrackTrace — the atui board serializers), influence-core/, trace-toolpack/ (selfExplain; 6 tools incl. variable-first `backtrack(variable, element?)`), context-ledger/ (which pieces EARNED their tokens — post-run offers/uses/outcomes bookkeeping + demote-never-starve gates `ledgerToolGate`/`ledgerEntryScorer`/`ledgerGated`; grouped-mode folds sf-llm-call inner logs, unmeterable runs → undefined; /observe), mcp/, rag/, tool-lint/ |
| memory/ | store/ (MemoryStore port) + pipeline presets + stages + beats/facts + causal/ (dev-only, TOP_K+search()-only) + wire/mountMemoryPipeline + retrieval/ (8.8.0: the `RetrievalStrategy` seam + `RetrievalEvidence`, the record a retrieval leaves — `topK()` is what every earlier release did unnamed) |
| maps/ | (9.58.0, door `/maps`) the mount kernel: claim/ (`Claim<T>` honesty primitive) + engagement/ (lease machine `advanceEngagement` + renewal feed `renewalEvidenceOf` + vocabulary). Pure data/functions; the ONE framework hook is `ctx.parkedIds` in the evaluator (the `leaseActiveIds` mirror), fed by the Evaluate stage; state rides `AgentState.mapEngagement` as a TOP-LEVEL array (the StepPointerCarrier law) |
| events/ | EventDispatcher (wildcard subs), registry (EVENT_NAMES, AgentfootprintEventMap), payloads |
| recipes/ | (9.48.0, door `/recipes`) the declared unit of agent CONFIGURATION — `defineAgentRecipe` + `AgentBuilder.recipe()`. PURE and tiny: id/version validators, the provenance value, the refusal sentences. The mutation lives in `AgentBuilder.recipe()` (application stack + two provenance maps); the manifest rows ride `RunManifestSources.recipes`. The door publishes the AUTHORING vocabulary only (`defineAgentRecipe`, `InvalidAgentRecipeError`, 4 types) — every validator and refusal formatter is internal, imported by module path |
| conventions.ts | THE builder↔recorder protocol: SUBFLOW_IDS/STAGE_IDS (internal), INJECTION_KEYS/stageRole/milestoneFor (exported, Lens-facing) |

Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `debug/finders.ts` re-exports them — only the old subpath is deprecated), while real recorders live in `recorders/observability/`; `identity` appears twice (src/identity.ts = tool credentials; memory/identity/ = tenant scoping — unrelated); `resilience` (provider decorators) ≠ `reliability` (in-loop rules gate).

## Core state & flow
- `AgentState` (core/agent/types.ts:287) — THE chart state; every stage gets `TypedScope<AgentState>`. Mutability conventions documented ON the type (:276-286). Everything in scope must survive structuredClone → functions stay in closures, errors stringified, injections projected to POJOs.
- **Two runId namespaces**: typed-event `meta.runId` (Agent's makeRunId, RunnerBase.ts:55) vs footprintjs `traversalContext.runId` — never correlate across them.
- **Event path**: stage `typedEmit` → `scope.$emit` → footprintjs emit channel → `EmitBridge.onEmit` (drops if no dispatcher listener! EmitBridge.ts:44) → `buildEventMeta` → dispatcher → `agent.on()` listeners. Fires MID-STAGE, **before that stage's commit** — correlate events↔commits by runtimeStageId, never arrival order.
- Executor defaults DIVERGE from footprintjs: `readTracking:'summary'`, `commitValues:'delta'` (Agent.ts:779-782) — read commit values via `commitValueAt`, never `bundle.overwrite[key]`.
- Recorders never read AgentState directly; slot subflows write `INJECTION_KEYS` convention keys, `ContextRecorder.onWrite` resolves the slot from the write's own runtimeStageId (parallel-safe).
- $break is the only clean-stop channel; structured fail context rides scope fields (`policyHalt*`, `reliabilityFail*`), decoded post-run by `Agent.finalizeResult` (Agent.ts:884) into typed errors.

## Extension points
- **Tool**: `defineTool` (core/tools.ts:155); shape `Tool = {schema, needs?, execute(args, ctx)}` (:23). `ctx.progress(payload)` (9.52.0) is the tool's OWN emit door — a `Pick<ToolExecutionContext,'progress'>` closure (`toolProgress`, stages/toolCalls.ts, spread at BOTH dispatch sites beside `toolArtifacts`/`sessionContext`) that typedEmits `stream.tool_progress` mid-stage; framework stamps toolCallId/toolName/iteration, author owns `payload` only. **Consumed by the live-status projection since 9.54.0** (`selectStatus`, status/statusTemplates.ts): a top-level string `message` renders VERBATIM (trimmed, cut at `PROGRESS_MESSAGE_LIMIT`=120 with the cut stated), anything else renders the generic `tool.progress.generic` line with a per-`toolCallId` count — never a payload dump. Ladder falls through to `tool.<name>`/`tool`, so a pre-9.54.0 template map cannot blank a bubble. Commentary gained `stream.tool_progress` (teaching voice, no payload) to match Lens's `humanizeToolProgressTeaching`. ALWAYS present and never fatal (try/catch + dev warn) — the streamless doors (`mcpServe`, traceToolpack `OFFLINE_CONTEXT`) supply the no-op, which is the whole list of places a new required ctx field must also land. Register `AgentBuilder.tool()` (AgentBuilder.ts:184); merged with auto `read_skill` in buildToolRegistry (:65; same-reference skill tools dedupe, any other name collision THROWS at build). Chart-as-tool: `flowchartAsTool` (core/flowchartAsTool.ts:203). MCP: `mcpClient(...).tools()`.
- **ToolProvider** (per-iteration visibility): `list(ctx): Tool[]` (tool-providers/types.ts:121); max ONE per agent (AgentBuilder.ts:238 throws on second); combinators staticTools/gatedTools/skillScopedTools chain decorator-style.
- **LLM provider**: `LLMProvider = {name, complete, stream?}` (adapters/types.ts:230) passed as `AgentOptions.provider`. `provider.name` keys THREE auto-resolutions: cache strategy (cache/strategyRegistry.ts:40), thinking handler (thinking/registry.ts:43), Lens labels.
- **Recorder, 3 layers**: (1) raw footprintjs CombinedRecorder via `agent.attach()` (RunnerBase.ts:474 — NOT idempotent); (2) typed stream `agent.on(type|'*')`; (3) new built-in = factory taking `{dispatcher, getRunContext}`, registered in the attach block inside `Agent.run()` (Agent.ts:807-845), barreled in src/observe.ts.
- **New typed event (3-step)**: payload interface in events/payloads.ts + entry in `AgentfootprintEventMap` (registry.ts:198) + append to `ALL_EVENT_TYPES` (registry.ts:488, count-asserted by tests). New DOMAIN also needs a bridge attach in Agent.createExecutor or emits never reach the dispatcher — AND a hand-edit to `DomainWildcard` (dispatcher.ts:67-90; still missing validation/reliability). **9.4.0 worked example of BOTH halves missing at once**: `credential.*` had payloads, registry entries and live emit sites since 6.11.0 with no bridge and no wildcard, so `agent.on('agentfootprint.credential.failed')` observed nothing for eight minors — the silence in which an identity adapter failed 100% of its calls. Fixed by `credentialRecorder` (recorders/core/CredentialRecorder.ts) + the wildcard arm.
- **Strategy (vendor sink)**: shapes in strategies/types.ts (Observability :130, Cost :169, LiveStatus :201, Lens :234); attach by INSTANCE only — `agent.enable.observability({ strategy })` (strategies/attach.ts). There is no by-name/`vendor` path: `strategies/registry.ts` declared one for years, nothing ever called it, and it was deleted rather than finished. Do NOT model this on `registerCacheStrategy` (cache/strategyRegistry.ts), which is real — a cache vendor self-registers on side-effect import, so a name is the only handle a consumer has there. New vendor = export from observability-providers.ts, NOT a new subpath.
- **Window refusal rule** (the 9.55.0 / 9.57.0 seam, and it is FOUR files, never a strategy): a member on `WindowRefusalReason` (window/types.ts) + a branch in `refusalFor` (window/turns.ts) + a small PURE resolver beside `currentRequest.ts` (`lastToolResult.ts` is the 9.57.0 one) + one line in `stages/window.ts` building the guard. Strategies need no edit — `planRemoval` arrives PRE-BOUND in `WindowStrategyInput`, which is why a rule added here reaches the three shipped strategies AND any a consumer wrote before it existed. Two things to weigh: every pin becomes a potential MID-window blocker and `turns.ts` caps the span at the first blocker (a blocker BEFORE `from` is stepped over, one after it ends the span); and any pin needs a bound — `keepLastToolResults` spends its ceiling INSIDE `planRemoval`, where `candidateCount` is known, so a pin already inside `keepRecentTurns` costs nothing. The stage owns the STAND-DOWN (two consecutive zero-removal records naming the reason ⇒ omit the pins for one visit, recorded as `observations.standDown`) because only the stage can read `scope.compactions`. Record fields are stamped by the STAGE, not by a strategy — same reason.
- **Memory store**: implement `MemoryStore` (memory/store/types.ts:113; `search?` REQUIRED for causal memory); pass to `defineMemory({store})`. Memory TYPE/STRATEGY unions are CLOSED (define.types.ts:57/74 — new one edits defineMemory dispatch + a pipeline builder).
- **Document loader** (8.10.0): implement `DocumentLoader` (rag/types.ts) — `{name, extensions, load}`. `loadDocuments` routes by extension, caller-supplied loaders FIRST, so overriding a built-in is passing yours ahead of it rather than editing `DEFAULT_LOADERS`. A loader MUST NOT rewrite text after offsets are conceivable: the HTML stripper replaces tags with EQUAL-LENGTH whitespace for exactly this reason.
- **Splitter** (8.10.0): implement `Splitter` (`{name, split(doc) → SplitPiece[]}`), a factory function like the window/retrieval families. THE invariant — `doc.text.slice(charStart, charEnd) === piece.text` — is VERIFIED by `splitDocuments`, not trusted. All offset arithmetic lives once in `splitters/shared.ts`; a strategy that does its own is how the invariant breaks.
- **Durable store** (8.9.0): `sqliteVectorStore` follows `hosting/sqliteSessions` line for line — lazy `node:sqlite`, WAL read-back on `journalMode`, STRICT tables, schema-identity + schema-version refusals, `':memory:'` refused. TWO things it adds that have no precedent there: `putMany`/`putIfVersion`/`forget` wrap in a transaction (sqliteSessions has none), and the EMBEDDER FINGERPRINT (`'<id>@<dims>'`, one per namespace in `af_index_meta`) is refused at write AND query. `SqliteUnavailableError` is now ONE class in `lib/sqliteUnavailable.ts` re-exported by both doors — a second class of that name is a duplicate type the build refuses.
- **Retrieval rule** (8.8.0): implement `RetrievalStrategy` (memory/retrieval/types.ts) — `select(pool) → verdict[]`, one verdict per candidate, order preserved; it never touches the store and never embeds. Pass as `defineRAG({retrieval})`. `topK()` is the only shipped one; rerank/MMR are named-but-deferred adapters behind the same interface. `TopKStrategy` is a UNION whose arms exclude (`{topK,threshold}` vs `{retrieval}`) — refused in the type AND at runtime, because two spellings of one rule can disagree.
- **Injection/skill**: `Injection = {id, flavor, trigger, inject}` (lib/injection-engine/types.ts:161); trigger is a closed 4-variant union (:30 — new kind edits evaluator.ts:40-74 switch). Factories defineSkill etc.; skill graph via `skillGraph()` (skillGraph.ts:392) with pluggable `EntryScorer` (entryScorer.ts:64). `SkillGraphConfig` is a UNION (flat arm `start`/`steps` vs tree arm) — the contradictions it encodes are ALSO refused at build (`.tree()` + `.entry()`/`.route()`, a non-leaf in `skills[]` under a tree, two skills claiming one id, a second `.skillGraph()` on one agent).
- **MCP gateway fetch seam (9.32.0)**: `GatewayTransportOptions.fetch?: FetchLike` → `McpGatewayTransport.fetch` → `createVendingFetch(t, t.fetch)` (mcpClient.ts). Composition ORDER is the feature: the credential is vended and applied FIRST, then the consumer's fetch runs — so an mTLS agent / DPoP signer sees the final headers and has the last word while per-request vending survives. Zero vendor code lands here, ever; the secrecy-invariant test extends over the injected fetch. Absent ⇒ `createVendingFetch`'s default global fetch, byte-identical.
- **Ports table** (wired via AgentOptions): PermissionChecker (adapters/types.ts:403), PricingTable (:412), CredentialProvider (identity/types.ts:89), CacheStrategy (cache/types.ts:151, registerCacheStrategy), ThinkingHandler (thinking/types.ts:114 — auto-wire scans HARDCODED SHIPPED_THINKING_HANDLERS, registry.ts:26), ReliabilityConfig (reliability/types.ts:183), OutputSchemaParser (core/outputSchema.ts:62, duck-typed).
- **Run input** (8.18.0): `src/core/runInput.ts` is THE door — `normalizeRunInput(input, 'Agent.run')` adapts a bare string to `{ message }` and refuses everything else with `InvalidRunInputError` (incl. empty/whitespace). Called at the top of every runner's `run()` (Agent, LLMCall, Sequence, Parallel, Conditional, Loop, LlmRouter); `Runner.run`/`RunnerBase.run` declare `TIn | string` so implementations stay assignable (a widened param on a METHOD breaks the bivariance that made `Runner<object,…>` accept them). A new runner adds one line, not a second policy.
- **Message content integrity** (8.18.0): a turn with no text is stopped at its SOURCE, never at the crash line (`buildMessagesSlot` `truncate`). Sources: `validators.safeStringify` (now total; `undefined`→`NO_TOOL_VALUE`), toolCalls' pauseHere-resume branch (`PauseAnswerRequiredError`), `runMessageChain` (non-text `allow` AND `ask` → denial), `Agent.assertDeliverableRoles` (declared injections), `validateCheckpoint` (per-message), `callLLM` (provider chunk contract). `buildMessagesSlot.assertComposable` is the NET — it names position/role/origin and deliberately does not coerce.
- **Output contract** (7.26, reshaped 8.18.0 — enforcement mounts whenever a parser exists; `retries: 0` = judge-don't-re-ask, retry BRANCH still gated on `retries > 0`; unmet → `scope.outputContractUnmet` + `agent.outputContractUnmet()` + `agentfootprint.agent.output_contract_unmet` + one warn; a pre-chain judge attributes `brokenBy` and STOPS the re-asking when an `act({output})` rewrite broke a passing answer): `.outputSchema(parser, opts)` — `retries>0` mounts the THIRD Route branch (`STAGE_IDS.OUTPUT_RETRY`, same `{loopTo}` as tool-calls) + swaps the decider for `buildEnforcingDecider` (stages/route.ts); `strategy:'tool-forced'` builds the synthetic tool (core/agent/outputEnforcement.ts) that callLLM appends at REQUEST assembly and normalizes back into `content` inside `singleProviderCall` — it never touches the tools slot, the registry or the dispatcher. TWO build refusals (agent has tools; no derivable jsonSchema) + ONE run-start refusal (`provider.carriesForcedToolChoice`; ABSENCE = NO, the opposite of carriesInMessages). A new wire capability = adapters/types.ts + 6 adapters + 3 resilience wrappers (withFallback publishes the AND).
- **Finder** (context-error localization): conform to `Finder` (observability/contextError/finders/types.ts:92); NO registry by design — one file + barrel line. Pluggable `InfluenceScorer` via `localizeContextBug({scorer})` (lib/context-bisect/localize.ts:340).
- **New composition/pattern**: extend RunnerBase, expose `getSpec(): FlowChart`, compose others' specs — no engine change.
- **Identity verifier (9.26.0)**: `IdentityVerifier` = `{ verify(token) → VerifiedIdentity }` (hosting/identityVerification.ts) wired at `standingAgent({ identity: { verify, allowAnonymous? } })`. Runs ONCE at the top of the handler, BEFORE `identityForRequest` composes scope, and its answer is shared by all three doors (turn / artifact / session-op) — a later door cannot be the lenient one. Extraction is `bearerToken(headers)` (the ONE reader; every dialect normalizes onto `authorization: Bearer`). Refusals carry `IdentityFailureClass` and NEVER the token (`sdkFailure` law); a key set that could not be FETCHED is `VerifierUnavailableError`/503, not 401. One adapter: `jwksIdentity` (adapters/identity/jwks.ts) over `jose` (optional peer, ESM-only ⇒ dynamic-import-then-lazyRequire, structural `JoseBackend` seam), mapping by `err.code` STRING not `instanceof` (the classes are not on jose's main entry; codes are pinned by test/adapters/identity/jwks.test.ts against the real package).
- **Admission policy (9.26.0)**: `AdmissionPolicy` = `{ decide(ctx) → 'allow' | {queue:true} | {refuse} }` (hosting/admission.ts) at `standingAgent({ admission })`, consulted once per turn BEFORE lane selection. Fed by `spendLedger()` — an in-process rolling window keyed by `spendKeyFor(identity)`, filled from `stream.llm_end` (tokens) + `cost.tick` (usd, ABSENT when no pricing table). Turns counted at ADMISSION, not completion. `{queue:true}` threads `queueAnyway` into `serialize` (bypasses the `'reject'` collision check only). Built-in: `turnsPerHour`. Absent ⇒ no ledger, no listeners, zero delta.
- **Session-history ops (9.26.0)**: a SECOND domain on the wire's `op` field. `hosting/wireOps.ts` is the ONE list (`WIRE_OPS`/`isWireOp`/`refuseUnknownWireOp`); each domain reader DECLINES the other's ops and both raise the same unknown-op refusal, so ordering between `readArtifactWireOp` and `readSessionWireOp` is not load-bearing. `SessionLifecycle` gained OPTIONAL `listByUser?`/`ownerOf?` (feature-detected; absent ⇒ `SessionIndexUnavailableError`, never an empty list). Ownership is DERIVED by `envelopeOwner(envelope)` at persist time — `persist` takes no owner — and WRITE-ONCE in both stores (memory: `!owners.has(id)`; sqlite: `owner = COALESCE(<table>.owner, excluded.owner)` — reversing that argument order transfers sessions by writing). Cross-user transcript = `SessionNotFoundError`, identical to missing. Both ops REQUIRE `identity` configured. **The ORDINARY TURN door asks the same question** (`mayOpenSession` in standingAgent, right after `hydrate` and before the strict readers, covering the `decision`/resume path too): with a verifier configured, `envelopeOwner(stored) === verified?.userId` or the same `SessionNotFoundError` — anything less made the transcript 404 decoration in front of a door that hydrated the same conversation into a model AND re-stamped its owner. No verifier ⇒ the check does not run (zero delta). Consequences stated in the option doc: pre-verification sessions name no owner and cannot be continued at a verifying door, and a refused turn does reveal that an id is taken. sqliteSessions added `owner`/`message_count` columns + an index WITHOUT a schema bump (older readers name their four columns explicitly; `SESSIONS_COLUMNS` stays the original four so a 9.25 file still opens, migrated by ALTER TABLE).
- **Ingress record (9.32.0)**: `standingAgent({ onIngressDecision })` — ONE record per DOOR decision, filed when the reply reaches its terminal. The funnel is ONE place: `handler` wraps the host's reply once (`beginIngress`/`IngressNote.watch`, hosting/ingressRecord.ts) and `answerRequest` is the old body verbatim, so no exit path reports and none added later can forget. Optional terminals are spread CONDITIONALLY on the wrapper (`awaiting`/`artifact`/`sessions`) — defining one the host lacks would tell the composer it can describe a pause it cannot and kill the `PauseNotCarriedError` path. The record carries classes + identifiers only: no token, no header, no claim set, **no error message** (only `code`/`name` — the `sdkFailure` law). `userId` is the PROVEN id, never a claimed one. It covers what a consumer's own `verify`/`decide` cannot see (missing bearer, `VerifierUnavailableError`, cross-owner 404) and deliberately NOT a body the transport refused first (`InvalidWireOpError` is raised inside `httpHost`'s `readRequest`, before the handler exists — stated on the type, never implied). **It is a STREAM, not `auditExport()`'s chain**, and saying otherwise would make it the failure it closes. Zero-cost unset: no wrapper object, the host's own reply.

- **Recordings as artifacts (9.26.0)**: `artifacts: { store, recordings: true | { label } }`. `Agent.startRunRecording()` calls the SAME `recordRun` (before `createExecutor` — `attach()` collects for the executor not yet built); `fileRunRecording` mints AFTER `finalizeResult`, awaited, every failure contained to `artifacts.refused`. Pure half in artifacts/recordingArtifact.ts; payload is the recording's JSON TEXT (a live snapshot handed to an in-process store would be a live view into a finished run). No new wire op — `artifact-get` serves it.
- **Code staging-in (9.26.0)**: `CodeSession.stageInputs?(inputs) → StagedCodeInput[]` — OPTIONAL, feature-detected via `canStageCodeInputs`, and its contract is TWO promises: the payloads are readable at the returned paths, AND every later `execute` exposes the manifest as `STAGED_INPUTS_ENV` (`AF_STAGED_INPUTS`, `name → path`). `CodeInput.name` is the MANIFEST KEY (the wants arg name, so a static description can name it) and `fileName` is the on-disk name — separate fields so the two cannot drift. `codeRunnerTool({ wants })` composes the schema properties + the description clause and refuses BY NAME on a non-staging runner. Implemented by localCodeRunner only.
- **Repeated-call nudge (9.26.0)**: `core/agent/repeatedCall.ts` (pure `noteRepeatedCall` + `repeatedCallLedgers()`) + the ONE batch-loop hook in toolCalls. Fingerprints (FNV-1a) of stable-stringified args and the tool's OWN delivered result — never values. **The counters are NOT tracked state**: they live in a bounded run-keyed map held by `buildToolCallsHandler` and keyed by `deps.currentRun().runId`, so a turn that repeats nothing is byte-identical in state, commit log, narrative and recordings (a scope key would have changed all four for every agent that merely upgraded); the repeat itself rides `agentfootprint.tools.repeated_call`, the emit channel per-attempt facts belong on. A resume mints a new runId ⇒ counting restarts. Fires at the SECOND identical landing, once. `AgentOptions.repeatedCallNudge: false` disables (threaded value-conditionally; ON is the default). Deliberately NOT applied on the pause-resume dispatch paths.
- **Evidence gate (9.35.0)** — `.namesAndNumbersFromEvidence({ posture, shapes, exempt, minDigits })`: every name/number in the final answer must appear in a `role:'tool'` result. Postures reuse the routing VOCABULARY (`assist`|`guard`|`rails`) as a SEPARATE option — routing authority ≠ evidence discipline, and overloading `skillGraphCascade.strictness` would deny "strict routing, loose evidence". Wiring is the stepNudge blast radius verbatim: `ResolvedEvidenceGate` (builder-resolved, refusals at the CALL SITE) → Agent ctor trailing param → `buildRouteDeciderStage`'s 4th arg (`judgeEvidence` runs LAST of the three judges — schema > steps > evidence — and NOT on a denied or schema-exhausted answer) → `evidenceRecheckStage` branch (`{loopTo}`, mounted only for a revising posture) → `STAGE_IDS.EVIDENCE_RECHECK` + BOUNDARY_LOCAL_IDS + milestoneFor. TWO deps flags, and the second is the one a reader misses: `evidenceRecheckStage` (branch) AND `hasEvidenceGate` (bubbles `systemPromptInjections` out of sf-llm-call in the GROUPED chart — without it the gate flags the app's own prompt). Per-check facts ride `agentfootprint.agent.evidence_checked` (emit channel); only the terminal verdict is committed (`unsupportedValues`), because the boundary raises off it. `UnsupportedValuesError` joins the TERMINAL-typed-error list in `run()`'s catch (a verdict is not a crash — no retry handle for a wall). **Grounded numbers (9.75.0)** rides the SAME dial: `nudge: true` arms the staged-refs nudge — `toolWantsOf` harvests `Tool.wants` beside the `toolGrounding` harvest in `Agent.buildChart` (same ToolProvider blindness), callLLM appends ONE request-only late line when a placed ticket's kind matches a SERVED `wants` tool (judged on `registeredToolSchemas`, so wrap-up's withheld surface arms nothing; history untouched ⇒ never in the exempt corpus), recorded as `agent.grounding_nudged`; and the recheck names the same refs+spender whenever a `wants` tool exists, NOT gated on `nudge`. Since 9.96.0 `evidence/recovery.ts` · `buildEvidenceRecovery` composes request-only system guidance; `evidenceRecoveryPiece` carries it into the next request without synthetic conversation turns. Optional `recoveryInstruction` adds bounded synchronous guidance; `buildEvidenceCorrection` remains a legacy compatibility helper. Token extraction, exemptions and the single-revision budget are unchanged; this is not semantic validation or streaming draft suppression.
- **Coverage primitives (this release)** — `absent()` / `coverage()`, both copied from FIELD USE. THE argument is the direction of the error: a *nothing-found* misread as an *outage* costs an investigation, an *outage* misread as *nothing-found* declares a system healthy that was never checked — so the two must not share a shape. Blast radius, and it is the `resultCeiling` radius verbatim: `readCoverageResult` called by `declareCoverage` at BOTH execute boundaries in toolCalls.ts (batch loop + `resolveCredentialAndExecute`), on the UNWRAPPED content and BEFORE the ceiling. FOUR downstream changes and no more: (1) `ToolResultStatus` gained a SEVENTH word `'absent'` — routable by `onToolStatus`, because folding it into `'failure'` is the confusion itself and into `'success'` leaves nothing to route on; (2) two events (`tools.absent`, `tools.coverage_declared`); (3) tracked `AgentState.coverageDeclared` — a limit is a fact about the ANSWER, not about an attempt, which is why it is state and the repeated-call counters are not; (4) the evidence corpus indexes an absence's COVERAGE ONLY (`coverage/evidence.ts` — a failed lookup is the cheapest laundering machine, and `absent()` would have made it cheaper: this is frames.ts's argument on the tool side). Deliberately UNCHANGED: no `error: true`, no retry, no refusal, no gate flag. Survival into the answer is `.limitsTravelWithTheAnswer()` → `attachCoverageLimits` dep → BOTH builders swap the final branch's first stage for `prepareFinalWithLimitsStage` (same id, same position). It APPENDS rather than judges: a check for "did the model state its limits?" needs a second model to decide what counts, which is what evidence/README.md forbids.
- **Out-of-budget wrap-up (9.56.0)** — `wrapUpAtMaxIterations` (AgentOptions, default ON, `repeatedCallNudge`'s opt-out grammar): the FOURTH Route branch, and the SchemaRetry mechanism verbatim — `STAGE_IDS.WRAP_UP` + same `{loopTo}`, so the last call is one ordinary turn with its own `iteration_start`/`llm_start`/`cost.tick`. Two things are its own: (1) the tools are WITHHELD at REQUEST ASSEMBLY in callLLM (`scope.wrapUpAsked`, the `schemaTool` seam's mirror — the schema tool still rides, so an output contract survives), which is what makes the call terminal BY CONSTRUCTION rather than by a rule, and is why it is exempt from `maxIterations`; (2) the CONDITIONAL MOUNT is on the agent having a TOOL SURFACE (`registryByName.size > 0 || externalToolProvider`) — a limit only cuts a turn short when tool calls were pending, so a toolless agent's chart must not grow a box that can never run. `decideBranch` treats a spent wrap-up as cut-short for every downstream judge (`toolCalls.length > 0 || scope.wrapUpAsked`), or a step nudge / evidence revision would loop past the limit that fired. `wrapUpAsked` is deliberately NOT seeded — a turn that finishes inside its budget commits the exact key set it always did. The record is FOUR channels: `stoppedEarly` (now with `wrappedUp`, corrected on the pass after so `answerWasEmpty` describes the answer the caller GOT), `cost.limit_hit` (unchanged), the new `agent.budget_exhausted {action: 'wrapped-up'|'cut-short'}`, and an optional `turn_end.stoppedEarly` projection.
- **Closed seams**: Agent chart internals (AgentChartDeps not exported — extend via injections/tools/memory/thinking, never by adding a ReAct stage); ContextSlot (3 slots fixed); ProviderKind factory; dormant ports with no consumer (ContextSourceAdapter, EmbeddingProvider, RiskDetector — adapters/types.ts only); reserved tool names under selfExplain — 8.16.0 made the list DERIVED: `TRACE_TOOL_NAMES` (traceToolpack.ts, 11 names since 9.61.0: run_overview/find_context_errors/find_in_trace/trace_node/trace_slice/backtrack/who_wrote/get_value/inspect_tool_call/inspect_tool_run/read_narrative) is what AgentBuilder.ts:~1560 reserves in inline mode (`explain_run` in delegate mode), so a NEW toolpack tool joins ONE list and the reservation follows — but it must ALSO join the lazy template's mounted set (lazyToolpack builds over `{narrative: [], events: []}` so the catalog shape is fixed at build time) and the count assertion in test/lib/trace-toolpack/selfExplainAgent.test.ts. **8.17.0 `inspect_tool_run`** is the descent THROUGH the tool boundary: `flowchartAsTool({ keepRecord: true })` files each invocation's record in a bounded LRU store (lib/trace-toolpack/innerRunRecords.ts) keyed by the executing `ctx.toolCallId`, riding the `Tool` under the `INNER_RUN_RECORDS` registry symbol; `AgentBuilder.build()` → `collectInnerRuns(registry, injections)` → `SelfExplainSource.getInnerRuns` → `TraceToolpackArtifacts.innerRuns`. The inner views are the pack ITSELF re-run over `openRecording(record.recording)` — do not add a second query implementation. Provider-delivered tools are NOT collected (no build-time list); inner runtimeStageIds are a SEPARATE namespace and only `inspect_tool_run` accepts them. **9.61.0 `find_context_errors`** is the Context Integrity read-out: it reads `agentfootprint.integrity.context_error` + `…disposition` off the artifacts' EVENT TAIL (never re-running a check), joins each finding to the step it was filed at, and mounts UNCONDITIONALLY — a tool that vanished with the tail could not say the evidence channel is ABSENT, which is the one sentence this family forbids collapsing into "no errors found".

## Change-impact map
- **conventions.ts** (STAGE_IDS/SUBFLOW_IDS/INJECTION_KEYS) → chart builders that mount by id, ContextRecorder slot attribution, localizer loop-head detection (lib/context-bisect/trajectory.ts:17-33), `stageRole`/`milestoneFor` (Lens contract), BoundaryRecorder. Renaming an id is the whole blast radius.
- **BoundaryRecorder wiring is THREE connections, all at record time**: `runner.attach` (boundaries), `.subscribe(runner)` (what's inside them), `{getCommitCount}` (where each sits on the commit axis). The third fails SILENTLY — every event stamps `commitIdxBefore: 0`, `boundaryIndex` stays empty by design, and an offline step strip has nothing to place. Unrecoverable after the run (the commit log never records WHEN a boundary was crossed). Wired by `attachFlowchart` (which `enable.flowchart`/`enable.localObservability` both go through) and by `recordRun`; a new entry point must pass all three.
- **indexCorpus fan-out** (8.10.0) → `maxBranches` on `addParallelForEach` TRUNCATES surplus items rather than queueing them, so the chart fans out over a WINDOW (`take-window` → `embed` → `tally-window` → `more-batches-decider` `{loopTo: 'take-window'}`) that can never exceed the ceiling. A single fan-out over all batches would silently index only the first `maxConcurrentBatches` — pinned by the 12-batches-through-a-window-of-2 test. `embedded` is summed from each branch's `written`, never from the plan's queue, and the fan-out is `failFast: true` because a half-indexed corpus keeps answering.
- **Embedder fingerprint** (8.9.0) → `Embedder.id` (optional; every shipped embedder sets one, and NONE include dims — the store appends `@<dims>` itself, so an id carrying its own size double-stamps) + `indexDocuments` defaulting `embedderId` to it + `SqliteVectorStore.reconcileFingerprint` (the only comparison site). Rule: dimensions ALWAYS decide, model ids decide only when BOTH sides named themselves — refusing on an absent name would block the majority of callers who never pass `embedderId`.
- **Retrieval record** (8.8.0) → FOUR stages write one object in sequence: `loadRelevant` (candidates+scores+threshold verdicts) → `pickByBudget` (re-marks admitted→over-budget/over-max-entries) → `formatDefault` (`promptFragment` + `promptPosition`) → the read mount's outputMapper lifts it to root as `retrievalEvidence_<id>`. `memoryRecallInjections` then splits ONE recall into one ActiveInjection PER CHUNK — guarded by a byte-equality check (`fragments.join('\n\n') === systemContent`) that falls back to the single injection rather than change the prompt. `rank` (score order) and `promptPosition` (picker order) are DIFFERENT and both load-bearing: joining fragments in rank order reproduces the right bytes in a sequence the model never saw.
- **AgentState** → all 8 stages/ files, both builders' mappers, memory-wire STRING-TYPED keys ('runIdentity'/'turnNumber'/… buildAgentChart.ts:177-180 — not refactor-safe), finalizeResult's `reliabilityFail*`/`policyHalt*` reads (rename silently kills the typed errors).
- **events/** → 120 typed events across 27 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
- **Run-configuration manifest (9.41.0)** → `agentfootprint.agent.run_configured`, the JOIN KEY that turns N runs into N labelled ARMS: one event naming the adapters/strategies in play (provider+model, reactMode, each memory's declared strategy/retrieval/embedder, window, graph posture+classifier, evidence posture, artifacts-present). Composed by the PURE `core/agent/runManifest.ts`, dispatched from `Agent.emitRunManifest()` at the END of `createExecutor` — the ONE funnel `run()` AND `resume()` share, both of which mint a fresh runId. Direct `dispatcher.dispatch` with a STATED pseudo-stage (`run-configured#0`), the `emitToolSessionReport` precedent — there is no stage yet, and `minimalMeta()` would make the one joinable-by-design event unjoinable. TWO laws, both tested: NAMES ONLY (a store is reported PRESENT and unnamed rather than identified by a directory/endpoint — `MemoryStore` and `ArtifactStore` declare no id), and ABSENT means "not configured", never a guessed `'default'`. Graph presence is read off `skillGraphNextSkill`, NOT `skillGraphCascade` (a 9.16-style mount sets no cascade and would read as "no graph"). `MemoryDefinition` gained `strategy`/`retrieval`/`embedderId` for it — declared names the compiled pipeline had closed over, the `store`-in-the-open precedent.
- **adapters/types.ts LLMMessage/LLMRequest** → 62 importers: tool_use round-trip (toolCalls.ts:115-135), wire assembly (callLLM.ts:150-160), providers, cache strategies, security/extractSequence, reliability loop.
- **Cache** → strategy registration is a MODULE SIDE EFFECT (src/index.ts:15-17); an entry point skipping that import silently falls back to NoOp. Resolved once per Agent at construction (Agent.ts:347).
- **Injection engine eval semantics** → Evaluate stage cursor keystone (buildInjectionEngineSubflow.ts:214-222), Route stage MIRRORS slot filters (keep in sync with buildSystemPromptSlot), read_skill gate (toolCalls.ts:747). The gate's allowed set is TWO kinds of id (8.4.0): a HOP (`deps.allowedSkillIds` = `graph.reachableSkills(cursor)` — activates AND moves the cursor) ∪ an OPEN skill (`deps.openSkillIds` = `Agent.openSkillIds()`, Agent.ts:1052 — a registered skill whose trigger is `llm-activated` AND that the graph declares no incoming edge to; activates, never moves the cursor). Both clauses are load-bearing: the trigger test is "read_skill can really activate this", the edge test keeps a bare model edge `.route(a,m)` from-gated. **8.5.0** extends the SAME trigger test three ways: a decision `tree()` returns `[]` from `reachableSkills` (leaves compile to `rule` triggers, so a pick could only be accepted then dropped — the gate refuses with a tree-specific message, `deps.skillGraphIsTree`, derived in AgentBuilder from `graph.nodes.some(kind==='predicate')`); `surfaceMode:'tool-only'` is refused at agent build for any skill whose trigger is not `llm-activated` (skillBodyDelivery.ts — the read_skill tool result is the only channel that mode has, and the graph never calls read_skill); and `read_skill`'s DESCRIPTION is rebuilt per iteration from `reachableSkills(cursor) ∪ open` (Agent.readSkillOfferFor → buildToolsSlot's `readSkillFor`). **The read_skill ENUM must stay the full catalog** — `toolArgValidation` defaults to `'enforce'` and runs at toolCalls.ts:570, BEFORE the gate at :765, and sets `error = true` which the gate skips; narrowing the enum silently retires the teaching refusal, `skill.rejected`, routeRecorder's rejection hops and the rejected-cap governor.
- **`pendingSkillPick`** (the model's accepted read_skill pick, 8.3.0) → SIX sites in lockstep: InjectionContext (injection-engine/types.ts) + AgentState (core/agent/types.ts) + seed init + toolCalls (clear at handler top, set on an accepted HOP — gated on `deps.allowedSkillIds` AND on the pick being in the graph's reachable set, which is what makes a pick "validated"; an accepted OPEN skill activates without ever being written here) + BOTH chart builders' inputMappers (buildAgentChart.ts, and TWO in buildDynamicAgentChart.ts — the grouped chart crosses an extra boundary) + the subflow args. Miss one and the pick silently no-ops again, which is the exact bug 8.3.0 fixed. `graph.nextSkill` honours it AFTER declared edges (D1 > D2); every graph-compiled trigger is cursor-aware, so the cursor and the active set cannot disagree.
- **The cursor resolver reports its own winning clause** (8.5.0) → `makeResolveCursor` returns `CursorMove {from,to,by}` and `graph.nextSkill` is its `.to` PROJECTION — never a second implementation. `graph.explainNextSkill` → Agent → `InjectionEngineConfig.explainNextSkill` → the Evaluate stage takes the cursor from `.to` (one consultation) and stamps `cursorMove` on `context.evaluated` → `routeRecorder` reads `.by`. Adding a `by` value means touching `CursorMoveCause`, `RouteOutcome`, `causeOf`'s switch and `formatRouteHop`'s switch — AND `causeOf` is a RUNTIME string switch with a default (the compiler does NOT flag it; only `formatRouteHop` is exhaustive), so a missed case silently falls back to destination-inferred outcomes (9.19.0 added `'tool-proposal'` + `'decider'` to all four). `routing[]` is per-SKILL BUILD-TIME provenance and is NOT the hop cause — conflating them is the 8.5.0 bug.
- **Per-skill brains** (9.19.0) → declared in TWO homes (`defineSkill({provider,model})` metadata — never projected — and `SkillGraphOptions.providers`; conflicts refused naming both), folded ONCE by `skillBrains.foldSkillBrains` at `AgentBuilder.build()` (graph-mounted + node-ids + foreign-provider-needs-model + `afterRefusals ≥ 1` + decider-needs-cascade refusals) → Agent ctor trailing param → `callLLM`'s ONE consult `brainFor(nextSkillCursor ?? currentSkillId, skillEscalated)` (precedence: escalation > skill brain > `.configure()` > build default; per-brain cache strategies resolved at buildChart — same-name keeps the agent's, foreign gets its registry default; `llm_start.brain` additive). Escalation counter lives at toolCalls' TWO `skill.rejected` sites (`noteSkillRefusal` — args_invalid deliberately does NOT count); seed resets gated on `hasEscalation`; the grouped chart threads `skillEscalated` across the sf-llm-call boundary (`AgentChartDeps.hasEscalation`). Decider = RouteTurn arm over `constrainedEnumPick` (extracted from llmClassifier — byte-identical requests); a decider-RESOLVED menu (move OR stay) writes scope `TurnRoute` WITHOUT `offered` while the event keeps the full set (guard's `menuOutstanding` then refuses late picks).
- **Typed tool effects** (9.19.0) → ONE grammar owner `core/agent/toolEffects.ts` (envelope recognizer is STRICT: keys ⊆ {content,effects,status}, every effect kind ∈ the reserved two, status ∈ the closed six — anything else keeps today's bytes). Unwrap at BOTH execute boundaries in toolCalls (batch loop + `resolveCredentialAndExecute`), judge via `applyToolEffects` at every result-finalization site. `pendingToolTransition` is one-shot BY DATA (iteration-stamped; Evaluate honors it exactly once — no clearing writes), committed AT ACCEPTANCE so a mid-batch pause never drops it; the resolver ranks it D1.5 (edges > proposal > pick) and an outrun acceptance emits `reroute_superseded {source:'tool-proposal'}`. Leases (`instructionLeases`) never mutate for validity — `activeLeaseIds` computes per pass against the ADVANCED tenant; the evaluator admits `ctx.leaseActiveIds` as the one framework-tier activation; lease DEATH is permanent via Evaluate's tenure sweep (`pruneLeases` → `nextInstructionLeases`, mapper round trip in BOTH builders — cyclic re-entry must not resurrect a dead lease). Envelope `effects` is REQUIRED (the marker itself; status-only = `effects: []`); a `{content,status}` near-miss stays data + dev-warns (`explainStatusOnlyNearMiss`). `onToolStatus` joins `when`/`onToolReturn` behind the ONE `isDeterministicRoute` predicate (4 former replicas); `status` rides `tool_end` + `toolResults`/`lastToolResult` entries. Chart threading is VALUE-conditional (the `resolvedModel` precedent) in both builders.
- **Chart shape** (stage order/loop target) → trajectory.ts loop-head bucketing, selfExplain, FlowchartRecorder/RunStepRecorder step synthesis, milestoneFor, the `maxIterations * 2 + 10` engine headroom (Agent.ts:634).
- **RunnerBase** → all six runners + enable.* strategies + Agent's crash-checkpoint tracker (subscribes to its own dispatcher, Agent.ts:729-752).
- **Any AWS adapter** (9.4.0) → `test/adapters/aws/awsCommandPin.ts` is THE registry of the SDK command constructors each one dispatches, and its completeness test fails the build for any `src/**` file that LOADS an `@aws-sdk/*` package without a row. Adding/renaming a dispatched command = edit the row. The rule the registry exists to enforce: a bare `@aws-sdk/client-*` **Client is command-based** (`send`/`destroy` only) — per-operation METHOD shortcuts live on the aggregated client (`BedrockAgentCore`), so `client.someOperation(...)` is always wrong here. Three adapters shipped violating this (6.42.0 wrong-service commands, 9.4.0 `EvaluatePolicyCommand` which does not exist, 9.4.0 `agentCoreIdentity`'s method call). Every AWS adapter now has an `_sdk` seam for exactly this test; the AWS SDKs are deliberately NOT devDependencies (six adapters prove their missing-peer-dep refusals by real absence), so the real-module half runs only where they are installed.
- **`EventMeta`** → shape is copied in THREE places in lockstep: `events/types.ts` (the type), `bridge/eventMeta.ts` `RunContext` + `buildEventMeta` (the builder), and the per-runner run-context literal (Agent.createExecutor, LLMCall). A field added to the type alone silently never appears. `sessionId` (9.4.0) rides the `AgentRunOptions` → `currentRunContext` path that `correlationId`/`traceId` already use, and `standingAgent` is the only shipped caller that knows one. **`principal`/`tenant` (9.11.0)** ride the same three sites, sourced ONLY from `runOptions?.identity ?? this.lastRunIdentity` (the EXPLICIT identity) — never `scope.runIdentity`, and `conversationId` is deliberately not carried. FOURTH site for this one: a sink that MAPS rather than serializes must place it by hand (otel turn_start does; xray does not — stated in docs, not implied). `file`/`cloudwatch`/`agentcore`/`audit` all `JSON.stringify(event)` and inherit any meta field free.
- **Permission capability vocabulary** (9.11.0) → `PermissionCapability` = `ToolCapability | 'tool_call' | 'skill_read'`, defined ONCE in adapters/types.ts and imported by events/payloads.ts (`PermissionCheckPayload.capability`) — the two used to be independent copies of the same union. The law is **enforce-when-both-sides-speak**: `Tool.capabilities` (declared, NEVER inferred) × `PermissionChecker.governs` (optional, feature-detected, ABSENCE = NO, `checkerGoverns()` is the one reader). `PermissionPolicy` DERIVES `governs` from its rules bag so "unconfigured" and "never asked" cannot drift. FOUR request sites now: toolCalls' 3 `tool_call` checks + `askCapability` (declared caps + `skill_read`). Memory-pipeline stages still build NO PermissionRequest — say so rather than imply it.
- **Skill visibility** (9.11.0) → the async half rides the tools slot's Discover stage (the ONE awaitable stage there) into a closure-shared `hiddenSkillIds`, read by the sync Compose stage — the `providerToolCache` pattern exactly. `readSkillFor` now takes `{currentSkillId?, hiddenSkillIds?}`; `ReadSkillOffer.grantable` became OPTIONAL (no graph ⇒ plain filtered catalog) and `hiddenIds` removes rows from BOTH sections. The ENUM stays the full catalog (8.5.0's law — narrowing it turns a policy refusal into a generic schema error that `toolArgValidation` raises before any gate). `skill:<id>` targets have ONE owner: `security/skillTarget.ts`.
- **Tool-result cap** (9.11.0) → `core/agent/toolResultCap.ts` + `ToolCallsHandlerDeps.maxToolResultChars` + the `capResults` closure applied at all FIVE `tool_end`-emitting dispatch paths in toolCalls.ts (execute, ask-resume, check-in decision, credential-consent resume, pauseHere answer). Both channels are measured separately — `result` (the event) and `modelResult` (history) — because a middleware may have already shrunk one. A new dispatch path MUST call `capResults` or it silently escapes the ceiling.
- **Refusing result ceiling** (9.20.0) → the PER-TOOL sibling of the cap with the OPPOSITE overflow answer: `Tool.resultCeiling {maxChars, narrowBy?}` (type + `assertResultCeiling` in core/tools.ts; measurement + refusal sentence in `core/agent/resultCeiling.ts`) REFUSES — "No data was returned" + how to narrow — never truncates (truncation reads as complete data ⇒ fabrication). Applied by the `refuseOverCeiling` closure at BOTH execute boundaries (batch loop + `resolveCredentialAndExecute`, covering all four resume paths; the pauseHere answer is a HUMAN's value, deliberately not measured), at the moment the handler's return lands — BEFORE gates/after-tool chain (read_skill-gate precedent). The payload enters NO channel; the record keeps `tools.result_refused {sizeChars, maxChars, narrowBy?, declaredStatus?}`; delivered status = `'invalid'` (routes via `onToolStatus`); an envelope's DECLARED effects are still judged (the effects channel didn't overflow) but its status is overridden; `ceilingRefused` blocks step-pointer advance on every path. Zero-cost: no `resultCeiling` ⇒ one undefined check, byte-identical.
- **Semantic tool results** (9.53.0) → ONE vocabulary leaf `lib/semantics/types.ts` (the toolOutcome precedent; type-only imports of coverage types — absorbed, never duplicated) + ONE rule set `semanticIssues` serving three doors: `semantic()` mint refusals, `readSemantics` strict recognition (any fault ⇒ data path byte-for-byte + dev warn — never half-applied), and `checkSemantics` findings (same codes). Dispatch wiring is the coverage/ceiling radius verbatim: `declareSemantics` closure at BOTH execute boundaries in toolCalls.ts, ordered declareCoverage → declareSemantics → refuseOverCeiling — the coverage funnel (`readCoverageResult`) grew a semantic arm so the envelope's `coverage` field flows the coverage() channel with zero boundary edits, the FULL envelope rides `tools.semantics_declared` (structuredClone-detached) BEFORE the ceiling (caveats survive an oversized refusal), and the ceiling measures the PROJECTION (`semanticsForModel`: drops marker/render/coverage-detail, composes `not_covered` FROM coverage) because that is what the model reads. `Tool.resultClass` ('triage'|'inventory', closed) validated at defineTool (`assertResultClass`, the assertResultCeiling law) and consumed ONLY by the gate. Gate = tool-lint humble-shell verbatim: core in lib/semantics/{check,format,cli}.ts, bin `agentfootprint-check-semantics.mjs`, exit 0/1/2, judges SAMPLE results (mock returns) — never executes tools. Deliberately UNCHANGED: no new scope key (the envelope is per-attempt ⇒ event channel; coverage rows reuse `coverageDeclared`), no status word, no gate flag, pause-answer paths not recognized (human values are not envelopes).
- **Conjunction matcher** (9.20.0) → `SkillMatch` gains `{all: [...]}` (AND of the SYNC arms only), all inside skillMatch.ts: `compileAllArm` (ONE compilation → AND predicate + flat `{kind:'all', parts}` data; intent member = teaching refusal naming the separate-rule alternative; nested `all` FLATTENED — associativity, the drop-stateful-flags normalize-to-truth precedent; empty refused), `compareWithAll` (shadows-ONLY via `partCoversProvably` part coverage — identical regex / keyword superset; plain-earlier-covers-a-part shadows a later all; NO overlap claims — a conjunction can be unsatisfiable and the specific-first layout is a design), `rawCaption` recursion (parts joined ` AND `, escaped once). Checkup/provenance/toMermaid see through it with zero changes of their own (parts are guaranteed leaves). NOTE `rawCaption` is now the EXPORTED `plainMatchCaption` (one grammar, two consumers: the mermaid label escapes it; skillExamples quotes it in prose).
- **Examples on start rules** (SG-G) → `examples?: readonly string[]` on `SkillEntryOptions` + BOTH `SkillStartRule` arms + `EntryDecl`; ALL the logic in skillExamples.ts (validate at the ONE funnel `builder.entry` — the config form translates through it): FOUR codes on `GraphProblemCode` proved by RUNNING the compiled predicates on a cold-start context, in declaration order. **TWO START LAWS, and the check-up may not pick one**: the declaration-order cold walk (unconditional entries claim) vs the turn-start cascade's tier-1 `firstRuleMatch` (CONDITIONAL entries only) — mounted by `.classify()` AND by `continuity: 'conversation'`, an AGENT-MOUNT option invisible at graph build. They differ in exactly one place (whether a default claims), so both are computed and an ERROR is asserted only where they AGREE: `example-shadowed-by-earlier` ERROR (earlier CONDITIONAL claimant, or both laws putting the turn elsewhere) vs `example-shadowed-by-default` WARNING (earlier UNCONDITIONAL claimant — both readings named; erroring here contradicted the live router, which routes that turn to the later rule under `continuity:'conversation'`). SEVERITY FOLLOWS PROVABILITY on `example-misses-own-rule`: ERROR for a data `match` (reads `userMessage` only ⇒ no-match holds under every context) or a THROW (turn 1 really hands it that context); WARNING for an opaque `when` (may be gated on `ctx.history.length > 0` and claim on a later turn — history survives a run, the cursor does not). Every message NAMES the judged context (iteration 1 / phrase / empty history / no cursor) and `describeCondition` has an UNCONDITIONAL arm — printing "its `when` predicate" for an entry that declares none was a message describing a graph nobody wrote. `example-unclaimed` WARNING. Ordering checks gated on the SAME `!exclusiveEntries || hasClassifier` premise as the pairwise rule checks; self-match is order-independent and always runs. `GraphCheckup` gains `notes?` (the boundary sentence — rendered by formatCheckup as `[note]`, carried through the deferred-body-contract filter) and `GraphProblem` gains `example?`. TIER LAW: tier-2 `match:{intent,examples}` examples are SCORING material (run time); rule-level `examples` are TEST material (build time only) — both lists on one rule is a teaching refusal, as are an empty/blank list and examples on an UNCONDITIONAL entry (it claims everything, so they would prove nothing). Zero-cost when unused: one `Array.some`, no notes key, byte-identical skills/edges/events.

## End-to-end trace (agent.run; 1 tool call then final answer; default reactMode 'dynamic')
build (once): Agent.create → AgentBuilder.build → Agent ctor → initChart(buildChart) (Agent.ts:429; RunnerBase throws on re-init :256) → buildAgentChart assembles: seed → sf-injection-engine → Context selector (`failFast: true`, selects the 3 slot subflows in parallel) → sf-cache → call-llm → route decider → branches tool-calls (pausable, `{loopTo}`) / [output-retry (7.26, conditional mount, SAME `{loopTo}`)] / final (PrepareFinal → memory writes → BreakFinal `$break`, `propagateBreak: true`).
run: createExecutor (Agent.ts:769): fresh runId + FlowChartExecutor(readTracking 'summary', commitValues 'delta') + enableNarrative + ~12 bridge recorders (causal-evidence ALWAYS inline even under deferred, Agent.ts:814) → installCheckpointTracker (listens iteration_start/end) → executor.run({input, maxIterations: N*2+10}).
seed#0 (stages/seed.ts:58): history from $getArgs OR consumePendingResumeHistory (:67-72); every emit flows typedEmit → $emit → EmitBridge (drops if no listener) → dispatcher, MID-STAGE pre-commit.
loop body: sf-injection-engine (Gather→Evaluate→Route→Delta; outputMapper `ArrayMergeMode.Replace` — load-bearing) → Context selector fans out sf-system-prompt ‖ sf-messages ‖ sf-tools (each writes its INJECTION_KEYS key; ContextRecorder resolves slot from runtimeStageId) → sf-cache gate → call-llm#N (cacheStrategy.prepareRequest; stream tokens; writes llmLatest*/token counters; reliability retry loop INSIDE this one stage if configured) → route (toolCalls.length && iteration < max ? 'tool-calls' : 'final', stages/route.ts decideBranch). With `.outputSchema(p,{retries})` the decider is `buildEnforcingDecider` instead: decide → output message-chain → judge the answer with the SAME applyOutputSchema runTyped uses → file an `outputAttempts` row → emit route_decided ONCE (may say `'output-retry'`).
output-retry#N (7.26, only when retries>0): reads the `outputSchemaFailure` carrier the decider just wrote → appends [failed assistant answer, authored correction] to history → files the `'retried'` row + emits output_schema_retry → iteration_end → iteration++ → `{loopTo}` (same target as tool-calls). A retry IS an iteration; the answering turn is NOT otherwise in history, which is why BOTH messages go.
tool-calls#N (pausable): per call — permission gate → arg validation → credential resolve → tool.execute; PauseRequest → commit partial + RETURN payload = footprintjs pause; result appended role:'tool'; iteration++; `{loopTo: sf-injection-engine}` re-enters the loop (buildAgentChart.ts:473).
final: prepareFinal sets finalContent + emits turn_end → breakFinal `$break()` returns finalContent → outputMapper bubbles it up → executor resolves → finalizeResult (Agent.ts:884): detectPause → reliabilityFail scan → policyHalt scan → return string. Errors: recoverable + history captured → wrapped in RunCheckpointError (Agent.ts:649-661).

## Backtracking
Six mechanisms layered on footprintjs (whose transaction/checkpoint machinery lives upstream): **M1** pause/resume — `pauseHere/askHuman` throw PauseRequest inside tool.execute; toolCalls commits history + pausedTool* to scope BEFORE returning the pause payload; scope is the ONLY carrier across the checkpoint. **M2** `resumeOnError` — history-only `AgentRunCheckpoint` (a DIFFERENT type from FlowchartCheckpoint) built from iteration_end events; resume REPLAYS from restored history via the `pendingResumeHistory` side channel. **M3** inline reliability retry — up to 50 attempts inside ONE stage; retry state closure-local, never scope. **M4** chart-level reliability gate — built but UNMOUNTED (buildReliabilityGateChart; editing it changes nothing at runtime). **M5** ReAct loopTo re-entry with `ArrayMergeMode.Replace` guards. **M6** counterfactual replay (context-bisect ablation probes; causal claims only from majority-flip over ≥2 seeded reruns). **M7 (triage surface)** variable-first tools over fp's slice layer: toolpack `backtrack(variable, element?)` (element mode = per-iteration history attribution, exact under the delta default) + `sliceToBacktrackTrace` (structural slice on the atui board — always correlational, every card an upper bound). Deep dive: [.claude/rules/backtracking.md](.claude/rules/backtracking.md).

## Invariants (assumed, not stated)
- ONE in-flight run per Agent instance (currentRunContext/lastExecutor/pendingResumeHistory are instance fields; concurrent runs corrupt event meta).
- Chart built once, reference-stable; closures over per-run state must be accessor lambdas (seed.ts:42-49) — direct field capture goes stale on run #2.
- Subscribe BEFORE run() — listener-presence gating drops (not queues) events at ContextRecorder/EmitBridge/RunnerBase.emit.
- `arrayMerge: Replace` on EVERY loop-crossed subflow mount (buildAgentChart.ts:284,345,358,382,431,455) — footprintjs default concatenates; omission = injections grow 8→16→24 per iteration.
- Context selector must stay `failFast: true` (buildAgentChart.ts:329) — default allSettled would swallow a throwing required slot and call the LLM half-built.
- Event payloads must be DETACHED plain data (typedEmit dev-guard) — a live TypedScope proxy breaks deferred-delivery capture and checkpoint serialization.
- Causal-evidence recorder stays inline even under `observerDelivery:'deferred'` (Agent.ts:808-814) — the memory write stage reads its accumulator mid-run.
- Tool names + memory ids unique at construction; LLM dispatches by name — a rename is a behavioral change.

## Landmines
1. Stale comment at Agent.ts:1053-1054 says the chart is rebuilt per run — it is NOT (eager initChart at :429); providerToolCache IS shared across runs; safety comes only from the Discover stage overwriting `current` each iteration.
2. `'classic'` reactMode "caching" is the ABSENCE of re-selection (Context stops picking static slots after turn 1) — "fixing" the selector converts classic into dynamic; classic + skills is broken by design (mid-run activation never reaches cached slots).
3. Branch stage ids are BARE (`'final'`, `'tool-calls'`), not the SUBFLOW_IDS prefixed forms — matchers written against SUBFLOW_IDS alone miss real runs (stageRole/milestoneFor deliberately match both).

## Pointers
- [.claude/rules/backtracking.md](.claude/rules/backtracking.md) — 6 mechanisms with step tables + pseudocode
- [examples/](examples/) — canonical imports (the authority on which subpath exports what) · [src/conventions.ts](src/conventions.ts) — the builder↔recorder protocol
- Build/test: `npm run build`, `npm test`
