<!-- 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 |
|---|---|---|---|
| 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 |

**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 — FIFTEEN 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()`). 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 |
|---|---|
| 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 |
| 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 correction names the same refs+spender via `buildEvidenceCorrection`'s third arg — threaded whenever a `wants` tool exists, NOT gated on `nudge`, clause INSIDE the authored frame so `isLibraryAuthoredTurn`'s prefix match and values-last both hold.
- **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/** → 109 typed events across 24 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`
