agentfootprint 9.52.0 → 9.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +6 -2
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/bin/agentfootprint-check-semantics.mjs +14 -0
  5. package/dist/core/agent/coverage/read.js +13 -0
  6. package/dist/core/agent/coverage/read.js.map +1 -1
  7. package/dist/core/agent/stages/toolCalls.js +95 -4
  8. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  9. package/dist/core/tools.js +25 -1
  10. package/dist/core/tools.js.map +1 -1
  11. package/dist/debug.js +14 -3
  12. package/dist/debug.js.map +1 -1
  13. package/dist/esm/core/agent/coverage/read.js +13 -0
  14. package/dist/esm/core/agent/coverage/read.js.map +1 -1
  15. package/dist/esm/core/agent/stages/toolCalls.js +95 -4
  16. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  17. package/dist/esm/core/tools.d.ts +55 -1
  18. package/dist/esm/core/tools.js +23 -0
  19. package/dist/esm/core/tools.js.map +1 -1
  20. package/dist/esm/debug.d.ts +1 -0
  21. package/dist/esm/debug.js +7 -0
  22. package/dist/esm/debug.js.map +1 -1
  23. package/dist/esm/events/payloads.d.ts +20 -0
  24. package/dist/esm/events/registry.d.ts +3 -1
  25. package/dist/esm/events/registry.js +2 -0
  26. package/dist/esm/events/registry.js.map +1 -1
  27. package/dist/esm/index.d.ts +2 -1
  28. package/dist/esm/index.js +11 -1
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/lib/semantics/check.d.ts +70 -0
  31. package/dist/esm/lib/semantics/check.js +154 -0
  32. package/dist/esm/lib/semantics/check.js.map +1 -0
  33. package/dist/esm/lib/semantics/cli.d.ts +40 -0
  34. package/dist/esm/lib/semantics/cli.js +148 -0
  35. package/dist/esm/lib/semantics/cli.js.map +1 -0
  36. package/dist/esm/lib/semantics/envelope.d.ts +139 -0
  37. package/dist/esm/lib/semantics/envelope.js +592 -0
  38. package/dist/esm/lib/semantics/envelope.js.map +1 -0
  39. package/dist/esm/lib/semantics/format.d.ts +11 -0
  40. package/dist/esm/lib/semantics/format.js +33 -0
  41. package/dist/esm/lib/semantics/format.js.map +1 -0
  42. package/dist/esm/lib/semantics/index.d.ts +14 -0
  43. package/dist/esm/lib/semantics/index.js +15 -0
  44. package/dist/esm/lib/semantics/index.js.map +1 -0
  45. package/dist/esm/lib/semantics/types.d.ts +230 -0
  46. package/dist/esm/lib/semantics/types.js +71 -0
  47. package/dist/esm/lib/semantics/types.js.map +1 -0
  48. package/dist/esm/recorders/observability/StatusRecorder.d.ts +7 -1
  49. package/dist/esm/recorders/observability/StatusRecorder.js +30 -2
  50. package/dist/esm/recorders/observability/StatusRecorder.js.map +1 -1
  51. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +10 -0
  52. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  53. package/dist/esm/recorders/observability/status/statusTemplates.d.ts +87 -8
  54. package/dist/esm/recorders/observability/status/statusTemplates.js +177 -24
  55. package/dist/esm/recorders/observability/status/statusTemplates.js.map +1 -1
  56. package/dist/events/registry.js +2 -0
  57. package/dist/events/registry.js.map +1 -1
  58. package/dist/index.js +65 -44
  59. package/dist/index.js.map +1 -1
  60. package/dist/lib/semantics/check.js +158 -0
  61. package/dist/lib/semantics/check.js.map +1 -0
  62. package/dist/lib/semantics/cli.js +176 -0
  63. package/dist/lib/semantics/cli.js.map +1 -0
  64. package/dist/lib/semantics/envelope.js +603 -0
  65. package/dist/lib/semantics/envelope.js.map +1 -0
  66. package/dist/lib/semantics/format.js +37 -0
  67. package/dist/lib/semantics/format.js.map +1 -0
  68. package/dist/lib/semantics/index.js +34 -0
  69. package/dist/lib/semantics/index.js.map +1 -0
  70. package/dist/lib/semantics/types.js +74 -0
  71. package/dist/lib/semantics/types.js.map +1 -0
  72. package/dist/recorders/observability/StatusRecorder.js +30 -2
  73. package/dist/recorders/observability/StatusRecorder.js.map +1 -1
  74. package/dist/recorders/observability/commentary/commentaryTemplates.js +10 -0
  75. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  76. package/dist/recorders/observability/status/statusTemplates.js +179 -25
  77. package/dist/recorders/observability/status/statusTemplates.js.map +1 -1
  78. package/dist/types/core/agent/coverage/read.d.ts.map +1 -1
  79. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  80. package/dist/types/core/tools.d.ts +55 -1
  81. package/dist/types/core/tools.d.ts.map +1 -1
  82. package/dist/types/debug.d.ts +1 -0
  83. package/dist/types/debug.d.ts.map +1 -1
  84. package/dist/types/events/payloads.d.ts +20 -0
  85. package/dist/types/events/payloads.d.ts.map +1 -1
  86. package/dist/types/events/registry.d.ts +3 -1
  87. package/dist/types/events/registry.d.ts.map +1 -1
  88. package/dist/types/index.d.ts +2 -1
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/types/lib/semantics/check.d.ts +71 -0
  91. package/dist/types/lib/semantics/check.d.ts.map +1 -0
  92. package/dist/types/lib/semantics/cli.d.ts +41 -0
  93. package/dist/types/lib/semantics/cli.d.ts.map +1 -0
  94. package/dist/types/lib/semantics/envelope.d.ts +140 -0
  95. package/dist/types/lib/semantics/envelope.d.ts.map +1 -0
  96. package/dist/types/lib/semantics/format.d.ts +12 -0
  97. package/dist/types/lib/semantics/format.d.ts.map +1 -0
  98. package/dist/types/lib/semantics/index.d.ts +15 -0
  99. package/dist/types/lib/semantics/index.d.ts.map +1 -0
  100. package/dist/types/lib/semantics/types.d.ts +231 -0
  101. package/dist/types/lib/semantics/types.d.ts.map +1 -0
  102. package/dist/types/recorders/observability/StatusRecorder.d.ts +7 -1
  103. package/dist/types/recorders/observability/StatusRecorder.d.ts.map +1 -1
  104. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  105. package/dist/types/recorders/observability/status/statusTemplates.d.ts +87 -8
  106. package/dist/types/recorders/observability/status/statusTemplates.d.ts.map +1 -1
  107. package/package.json +2 -1
package/AGENTS.md CHANGED
@@ -524,7 +524,7 @@ files `agentfootprint.tools.coverage_declared`; with
524
524
  to the final answer, which is how a limit survives a model that would rather not
525
525
  mention it.
526
526
 
527
- ### Observability — 101 typed events across 22 domains
527
+ ### Observability — 102 typed events across 22 domains
528
528
 
529
529
  ```typescript
530
530
  agent.on('agentfootprint.context.injected', (e) =>
package/CLAUDE.md CHANGED
@@ -17,7 +17,10 @@ not in this table, search `src/index.ts` for the nearest noun before writing cod
17
17
 
18
18
  | If you are about to build… | It is | Where | Since |
19
19
  |---|---|---|---|
20
+ | 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 |
21
+ | a build gate that refuses a triage/inventory tool that forgot its caveats, by tool name and field name | `checkSemantics` + `defineTool({ resultClass })` + bin `agentfootprint-check-semantics` | `src/lib/semantics/check.ts` | 9.53.0 |
20
22
  | 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 |
23
+ | 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; otherwise an honest generic line, never a payload dump) | `selectStatus` + `progressMessageOf` + `tool.progress` templates | `src/recorders/observability/status/statusTemplates.ts` | 9.54.0 |
21
24
  | 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 |
22
25
  | 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` + `guard-unsatisfiable` | `src/lib/injection-engine/skillGuard.ts` | 9.51.0 |
23
26
  | 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 |
@@ -89,7 +92,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
89
92
  - $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.
90
93
 
91
94
  ## Extension points
92
- - **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. 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()`.
95
+ - **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()`.
93
96
  - **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.
94
97
  - **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.
95
98
  - **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.
@@ -127,7 +130,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
127
130
  - **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`.
128
131
  - **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.
129
132
  - **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).
130
- - **events/** → 101 typed events across 22 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).
133
+ - **events/** → 102 typed events across 22 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).
131
134
  - **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.
132
135
  - **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.
133
136
  - **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).
@@ -144,6 +147,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
144
147
  - **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`.
145
148
  - **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.
146
149
  - **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.
150
+ - **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).
147
151
  - **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).
148
152
  - **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.
149
153
 
@@ -252,7 +252,7 @@ const agent = Agent.create({ provider, model })
252
252
  agent.on('agentfootprint.context.evaluated', (e) => console.log(e.payload.activeIds));
253
253
  ```
254
254
 
255
- **101 typed events across 22 domains.** Two subscription shapes and no third:
255
+ **102 typed events across 22 domains.** Two subscription shapes and no third:
256
256
  `'*'` (every event) and `'agentfootprint.<domain>.*'` (one domain). **`'agentfootprint.*'`
257
257
  is not a pattern** — TypeScript rejects it, and at runtime it would match nothing.
258
258
 
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agentfootprint-check-semantics — CI gate for semantic tool results
4
+ * (9.53.0). Humble shell: all behavior lives in src/lib/semantics/cli.ts
5
+ * (unit-tested there); this wrapper only resolves the built module and maps
6
+ * the returned code onto process.exitCode.
7
+ *
8
+ * npx agentfootprint-check-semantics semantics-catalog.json
9
+ * npx agentfootprint-check-semantics semantics-catalog.json --strict
10
+ *
11
+ * Guide: docs-next/content/docs/build/semantic-results.mdx
12
+ */
13
+ const { runCheckSemanticsCli } = await import('../dist/esm/lib/semantics/cli.js');
14
+ process.exitCode = await runCheckSemanticsCli(process.argv.slice(2));
@@ -17,6 +17,7 @@
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.readCoverageResult = void 0;
20
+ const envelope_js_1 = require("../../../lib/semantics/envelope.js");
20
21
  const absent_js_1 = require("./absent.js");
21
22
  const ledger_js_1 = require("./ledger.js");
22
23
  const ABSENT_STATUS = 'absent';
@@ -38,6 +39,18 @@ function readCoverageResult(value) {
38
39
  ],
39
40
  };
40
41
  }
42
+ // A semantic envelope's `coverage` field (9.53.0) is ABSORBED here — the
43
+ // one recognizer funnel — so the boundary a semantic tool declared flows
44
+ // through the exact channel `coverage()` uses (the `tools.coverage_declared`
45
+ // event, tracked state, the final-answer limits block) with zero extra
46
+ // wiring at any dispatch door. A semantic envelope without `coverage`
47
+ // declares no boundary, exactly like a bare result.
48
+ const sem = (0, envelope_js_1.readSemantics)(value);
49
+ if (sem !== undefined) {
50
+ if (sem.coverage === undefined)
51
+ return undefined;
52
+ return { declared: [{ kind: 'ledger', coverage: (0, envelope_js_1.coverageOfSemantics)(sem) }] };
53
+ }
41
54
  const covered = (0, ledger_js_1.readCoverageLedger)(value);
42
55
  if (covered === undefined)
43
56
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../../src/core/agent/coverage/read.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAGH,2CAA6D;AAC7D,2CAAmE;AA6BnE,MAAM,aAAa,GAAqB,QAAQ,CAAC;AAEjD;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAC,KAAc;IAC/C,MAAM,OAAO,GAAG,IAAA,uBAAW,EAAC,KAAK,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;YACL,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAA,6BAAiB,EAAC,OAAO,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,UAAU,EAAE;aACzF;SACF,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,IAAA,8BAAkB,EAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,QAAQ,GAAoB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAA,4BAAgB,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAG,IAAA,uBAAW,EAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC7C,QAAQ,CAAC,IAAI,CAAC;QACZ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,IAAA,6BAAiB,EAAC,KAAK,CAAC;QAClC,SAAS,EAAE,KAAK,CAAC,UAAU;KAC5B,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAC7C,CAAC;AArBD,gDAqBC"}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../../../src/core/agent/coverage/read.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAGH,oEAAwF;AACxF,2CAA6D;AAC7D,2CAAmE;AA6BnE,MAAM,aAAa,GAAqB,QAAQ,CAAC;AAEjD;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAC,KAAc;IAC/C,MAAM,OAAO,GAAG,IAAA,uBAAW,EAAC,KAAK,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;YACL,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAA,6BAAiB,EAAC,OAAO,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,UAAU,EAAE;aACzF;SACF,CAAC;IACJ,CAAC;IACD,yEAAyE;IACzE,yEAAyE;IACzE,6EAA6E;IAC7E,uEAAuE;IACvE,sEAAsE;IACtE,oDAAoD;IACpD,MAAM,GAAG,GAAG,IAAA,2BAAa,EAAC,KAAK,CAAC,CAAC;IACjC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACjD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAA,iCAAmB,EAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;IAChF,CAAC;IACD,MAAM,OAAO,GAAG,IAAA,8BAAkB,EAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,QAAQ,GAAoB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAA,4BAAgB,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAG,IAAA,uBAAW,EAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC7C,QAAQ,CAAC,IAAI,CAAC;QACZ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,IAAA,6BAAiB,EAAC,KAAK,CAAC;QAClC,SAAS,EAAE,KAAK,CAAC,UAAU;KAC5B,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAC7C,CAAC;AAhCD,gDAgCC"}
@@ -69,6 +69,7 @@ const validators_js_1 = require("../validators.js");
69
69
  const toolResultCap_js_1 = require("../toolResultCap.js");
70
70
  const resultCeiling_js_1 = require("../resultCeiling.js");
71
71
  const skillSteps_js_1 = require("../../../lib/injection-engine/skillSteps.js");
72
+ const envelope_js_1 = require("../../../lib/semantics/envelope.js");
72
73
  const index_js_1 = require("../coverage/index.js");
73
74
  const toolEffects_js_1 = require("../toolEffects.js");
74
75
  /** Declaration order preserved, ids de-duplicated — the shape the gate's re-prompt
@@ -381,6 +382,66 @@ function buildToolCallsHandler(deps) {
381
382
  scope.coverageDeclared = [...(scope.coverageDeclared ?? []), ...rows];
382
383
  return reading.status;
383
384
  };
385
+ /**
386
+ * The semantic envelope (9.53.0) — recognized at the SAME execute
387
+ * boundaries as coverage and the ceiling (one implementation, every door),
388
+ * AFTER `declareCoverage` (which absorbs the envelope's `coverage` field
389
+ * through the one coverage funnel) and BEFORE `refuseOverCeiling` (so the
390
+ * FULL envelope — grain, provenance, render, coverage — is on the record
391
+ * even when the content is refused as oversized: a caveat that died with
392
+ * an oversized result would be the silence the envelope exists to break).
393
+ *
394
+ * Returns the MODEL's view — the compact rendering-free projection
395
+ * (`semanticsForModel`: data + grain + provenance + composed `not_covered`
396
+ * + non-null clarify + the static note; never the marker, `render`, or the
397
+ * three-list coverage detail) — or `undefined` for every non-envelope
398
+ * value, which keeps today's path byte for byte. The full envelope rides
399
+ * the typed `tools.semantics_declared` event, so recordings and UIs get
400
+ * everything the model was spared.
401
+ *
402
+ * A value CARRYING the marker that this library cannot honor stays DATA
403
+ * (never half-applied — the recognition strictness law) and is dev-warned
404
+ * here naming its first fault; `check:semantics` names them all at build
405
+ * time.
406
+ */
407
+ const declareSemantics = (scope, call, value) => {
408
+ const sem = (0, envelope_js_1.readSemantics)(value);
409
+ if (sem === undefined) {
410
+ const faults = (0, envelope_js_1.explainSemantics)(value);
411
+ if (faults !== undefined && faults.length > 0 && (0, footprintjs_1.isDevMode)()) {
412
+ // eslint-disable-next-line no-console
413
+ console.warn(`agentfootprint semantics: tool '${call.toolName}' returned a value carrying ` +
414
+ `af_semantics that was NOT recognized (treated as plain data): ` +
415
+ `${faults[0].message} (field: ${faults[0].field})` +
416
+ (faults.length > 1 ? ` — and ${faults.length - 1} more; run check:semantics.` : ''));
417
+ }
418
+ return undefined;
419
+ }
420
+ // Detached plain data for the record — the event must not hold a live
421
+ // reference into a value the tool still owns. An envelope that cannot
422
+ // survive structuredClone cannot ride the record (or a checkpoint), so
423
+ // it is declined here — data path, dev-warned — rather than half-filed.
424
+ let detached;
425
+ try {
426
+ detached = structuredClone(sem);
427
+ }
428
+ catch {
429
+ if ((0, footprintjs_1.isDevMode)()) {
430
+ // eslint-disable-next-line no-console
431
+ console.warn(`agentfootprint semantics: tool '${call.toolName}' returned a semantic envelope ` +
432
+ `that does not survive structuredClone (a function or live handle in a fact row?) ` +
433
+ `— treated as plain data. Envelope values must be plain data.`);
434
+ }
435
+ return undefined;
436
+ }
437
+ (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.tools.semantics_declared', {
438
+ toolName: call.toolName,
439
+ toolCallId: call.toolCallId,
440
+ iteration: call.iteration,
441
+ semantics: detached,
442
+ });
443
+ return (0, envelope_js_1.semanticsForModel)(sem);
444
+ };
384
445
  // ── Step-procedure boundary (9.18.0) — ONE implementation, five sites ──
385
446
  // The batch loop and the four resume paths all finalize results; each
386
447
  // calls these where its result becomes final, so a pointer can never miss
@@ -1284,7 +1345,9 @@ function buildToolCallsHandler(deps) {
1284
1345
  }
1285
1346
  }
1286
1347
  try {
1287
- const result = await tool.execute(args, {
1348
+ // `let`, not `const`: the semantic projection below replaces the value
1349
+ // on the non-envelope path exactly as the batch loop does.
1350
+ let result = await tool.execute(args, {
1288
1351
  toolCallId,
1289
1352
  iteration,
1290
1353
  ...(env.signal && { signal: env.signal }),
@@ -1310,6 +1373,12 @@ function buildToolCallsHandler(deps) {
1310
1373
  // and wins over the envelope's only when the envelope declared none.
1311
1374
  const coverageStatus = declareCoverage(scope, { toolName, toolCallId, iteration }, envelope.content);
1312
1375
  const declaredStatus = envelope.status ?? coverageStatus;
1376
+ // The semantic envelope (9.53.0), on the unwrapped content — the
1377
+ // effects+semantics composition point: full envelope to the record,
1378
+ // projection to the model. BEFORE the ceiling, which must measure
1379
+ // what the model will actually read.
1380
+ const semanticView = declareSemantics(scope, { toolName, toolCallId, iteration }, envelope.content);
1381
+ const content = semanticView ?? envelope.content;
1313
1382
  // The tool's own ceiling (9.20.0) measures the CONTENT — the channel
1314
1383
  // that can overflow a context window. The DECLARED effects are small
1315
1384
  // validated data and are still judged by the caller: a tool that
@@ -1317,7 +1386,7 @@ function buildToolCallsHandler(deps) {
1317
1386
  // transition. The delivered status becomes 'invalid' (the declared
1318
1387
  // status described a result the model never received — the
1319
1388
  // `result_refused` event keeps what was declared).
1320
- const refusal = refuseOverCeiling(scope, { toolName, toolCallId, iteration }, tool, envelope.content, declaredStatus);
1389
+ const refusal = refuseOverCeiling(scope, { toolName, toolCallId, iteration }, tool, content, declaredStatus);
1321
1390
  if (refusal !== undefined) {
1322
1391
  return {
1323
1392
  result: refusal,
@@ -1327,12 +1396,23 @@ function buildToolCallsHandler(deps) {
1327
1396
  };
1328
1397
  }
1329
1398
  return {
1330
- result: envelope.content,
1399
+ result: content,
1331
1400
  executed: true,
1332
- envelope: declaredStatus === envelope.status ? envelope : { ...envelope, status: declaredStatus },
1401
+ envelope: declaredStatus === envelope.status && semanticView === undefined
1402
+ ? envelope
1403
+ : {
1404
+ ...envelope,
1405
+ content,
1406
+ ...(declaredStatus !== undefined && { status: declaredStatus }),
1407
+ },
1333
1408
  };
1334
1409
  }
1335
1410
  const coverageStatus = declareCoverage(scope, { toolName, toolCallId, iteration }, result);
1411
+ // The semantic envelope (9.53.0) on this path's own execute boundary —
1412
+ // a resumed call's envelope is judged exactly as an inline one's.
1413
+ const semanticView = declareSemantics(scope, { toolName, toolCallId, iteration }, result);
1414
+ if (semanticView !== undefined)
1415
+ result = semanticView;
1336
1416
  const refusal = refuseOverCeiling(scope, { toolName, toolCallId, iteration }, tool, result, coverageStatus);
1337
1417
  if (refusal !== undefined) {
1338
1418
  // Synthesized empty-effects envelope: the resume paths read the
@@ -2046,6 +2126,17 @@ function buildToolCallsHandler(deps) {
2046
2126
  const coverageStatus = declareCoverage(scope, { toolName: tc.name, toolCallId: tc.id, iteration }, result);
2047
2127
  if (toolStatus === undefined)
2048
2128
  toolStatus = coverageStatus;
2129
+ // The semantic envelope (9.53.0), on the unwrapped content and
2130
+ // AFTER coverage (whose funnel absorbs the envelope's own
2131
+ // `coverage` field), BEFORE the ceiling (which must measure
2132
+ // what the model will actually read, and must not silently
2133
+ // delete grain/provenance — they are already on the record by
2134
+ // the time it fires). Everything downstream — governance, the
2135
+ // cap, history, `tool_end` — sees the compact projection; the
2136
+ // full envelope rides `tools.semantics_declared`.
2137
+ const semanticView = declareSemantics(scope, { toolName: tc.name, toolCallId: tc.id, iteration }, result);
2138
+ if (semanticView !== undefined)
2139
+ result = semanticView;
2049
2140
  // The tool's own refusing ceiling (9.20.0) — at the execute
2050
2141
  // boundary, BEFORE the gates and the after-tool chain, so
2051
2142
  // governance and the agent-level cap compose over what the