# @adjudicate/core

## 1.9.0

### Minor Changes

- e650c37: WS7 — `IntentActor.role`, an OPTIONAL, OPAQUE adopter role carrier (staff-role authorization prerequisite for role-aware pack guards, e.g. OWNER/MANAGER/ATTENDANT × intent-kind matrices).

  `@adjudicate/core` (minor — new surface):
  - `IntentActor` gains `readonly role?: string`. The string is adopter vocabulary — the kernel assigns it NO meaning, enforces NO enum, and consults it in NO built-in guard; adopter packs may read it via `envelope.actor.role`. Orthogonal to BOTH the provenance `principal` axis AND the authority graph's identity binding.
  - Canonical-drop-safe (mirroring `attestation` / `resourceRefs`): an envelope WITHOUT `role` hashes byte-identically to pre-change envelopes — `@adjudicate/canonical` drops `undefined` keys before hashing, so NO existing `intentHash`, golden vector, or replay fixture changes (all pass unchanged). A PRESENT `role` IS bound into `intentHash` via `actor`, so a post-decision role swap is tamper-evident.
  - `isIntentEnvelope` rejects a present-but-malformed `role` (empty string / non-string); absent stays valid. `docs/specs/intent-envelope-v2.schema.json` adds the optional `actor.role` property (`string`, `minLength: 1`) under the actor's `additionalProperties: false`.

  `@adjudicate/runtime` (minor — new park-blob field):
  - `ParkDeferredIntentArgs["envelope"]` / `ParkedEnvelope["envelope"]` gain optional `actorRole`, and `verifyParkedEnvelopeHash` re-derives the hash with `role` threaded through the reconstructed actor (passed unconditionally, exactly like `resourceRefs`) — so a parked envelope CARRYING a role resumes with an IDENTICAL `intentHash` instead of false-tampering (`park_blob_tampered`), while a no-role blob re-derives byte-identically (no regression).

  `@adjudicate/adapter-core` (patch — internal threading, no API change):
  - The DEFER park projection in `translateDecision` forwards `actorRole: envelope.actor.role` so a role-carrying DEFER round-trips park → resume with its hash intact.

## 1.8.0

### Minor Changes

- efabb92: Plan 1 / Theorem E (E-1) — `RenderedReply`, the runtime-non-forgeable carrier for customer-facing egress text.

  This minor bump lands the new surface in `@adjudicate/core` **1.8.0** (main is at 1.7.0; this minor changeset computes 1.7.0 → 1.8.0). Downstream consumers that import the minter set must require `@adjudicate/core` >= 1.8.0.

  Additive new surface in `@adjudicate/core` (`src/rendered-reply.ts`, re-exported from the barrel):
  - `RenderedReply` — opaque branded object type (`{ readonly text: string }` + a module-private `unique symbol` brand that is NOT exported, so external code cannot name the key).
  - Closed minter set: `mintRenderedReply` (claims→prose), the operational factories `mintCronReply` / `mintReceiptReply` / `mintOtpReply` / `mintBroadcastReply` / `mintFallbackReply`, and the transitional, `@deprecated` `wrapLegacyResponderText` (W4→W5 seam).
  - `unwrapRendered(reply)` — the egress gate; asserts the value is in a module-private `WeakSet` of genuinely-minted replies and throws on a forged/structural literal.

  Why an object wrapper (not a branded string): tsc erases a string brand, so a branded string is forgeable at runtime with no membership test. A heap object trackable in a `WeakSet` gives `unwrapRendered` a real provenance check at the boundary (Theorem E demands runtime, not just compile-time, non-forgeability).

  Defense-in-depth: (a) the brand symbol is never exported; (b) the runtime WeakSet membership assert in `unwrapRendered`; (c) a shared-eslint-config `no-restricted-syntax` ban on `x as RenderedReply` (and the nested `as any as RenderedReply`), with the minter module exempt.

  This `@adjudicate/core` **1.8.0** also carries the Plan 1 Phase 4 (W6) soundness conjuncts that harden the §5 claim predicate. Every one is ADDITIVE + fail-safe + DEMOTE-ONLY — it can only move a verdict toward LESS (VALIDATED → UNKNOWN/REFUSED), never promote, so every existing claim type keeps **compiling** unchanged. NOTE the falsifier gate changes runtime behavior: an un-upgraded type now defaults to **UNKNOWN-only** until W5 enumerates its falsifiers (a safe degradation, not "validates exactly as before") — the C6, value-agnostic, and provenance paths remain no-ops for types that do not opt in.
  - **C6 claim value-binding** (`src/claims/soundness.ts`; Theorem S precondition (a-value)) — `MinimalClaim` gains an OPTIONAL `value` + `valueBinding { key, path? }`. When declared, `claimAllowed` binds the claim's RENDERED value to its licensing evidence entry's value via the canonical `sameValue` (the SAME comparator P2/H3 use): an in-grammar mismatch → REFUSED (the round-2 (a-value) model-authored-surplus catch); an unprovable binding (bound key absent, or a value outside the closed scalar grammar) → UNKNOWN (abstain). With no `valueBinding`, §5 stays value-agnostic (no-op). `runClaimsKernel` threads `candidate.value` into the soundness input. New exported type: `ValueBinding`.
  - **Falsifier-completeness gate** (`soundness.ts` + `evidence-requirement.ts`; inv.17; §R) — `MinimalClaim` extends an OPTIONAL `FalsifierDeclaration { falsifierComplete?, falsifiers? }`. A claim VALIDATEs only if its type has ENUMERATED how it could be falsified (`falsifierComplete: true` ∧ a non-empty `falsifiers[]`); otherwise the all-pass path is CAPPED to UNKNOWN (honest ignorance). `assertFalsifierDeclaration` throws on the inconsistent lying case (`falsifierComplete: true` with empty/missing falsifiers — §R hard error), reusing `parseEvidenceRequirement` to validate each falsifier. New exports: `FalsifierDeclaration`, `isFalsifierComplete`, `assertFalsifierDeclaration`.
  - **Cross-key conflict gate + RUNTIME WIRING** (`evidence-ledger.ts` + `soundness.ts`) — the falsifier RUNTIME arm, distinct from same-key last-write-wins: a DIFFERENT declared falsifier key being present this turn poisons the backed key to `conflict` → UNKNOWN. New ledger primitives: `EvidenceLedger.resolveAgainstFalsifiers`, the `CrossKeyConflict` type, `detectCrossKeyConflicts`. **CE#3 closure (Plan 1 Phase 3):** `claimAllowed` now CALLS `resolveAgainstFalsifiers` on the eligible all-pass path — a claim whose type is falsifier-complete and otherwise VALIDATED is DEMOTED to UNKNOWN when any declared falsifier value actually FIRES this turn (e.g. STORE_OPEN_NOW with a present ScheduleOverride; PAYMENT_STATUS=paid with a present refund/chargeback). The gate previously shipped tested-but-unwired; this completes the runtime arm. Demote-only (the inv.17 extensibility property still holds: only the all-pass path is affected, an absent/errored/conflicted falsifier never fires, and a falsifier can never promote). No integrity-ranked auto-resolution (inv.16).
  - **Structural provenance** (`evidence-ledger.ts`) — null-provenance DEFAULT-DENY at the write boundary (`normalizeEvidenceEntry`: absent/invalid taint|originProvenance → UNTRUSTED_DATA, sourceMode → cache; never coerced up), so a mislabeled row can never validate. Plus the injected `ProvenanceDeriver` seam (2nd ledger ctor arg) + write-time-only `EvidenceEntryInput.sourceOfRecord` (stripped before storage) — the kernel half; adapter-side derivation is W5.
  - **Additive predicates** — render-time freshness re-check via the source-version token (`EvidenceLedger.snapshotToken` + `isSnapshotFresh`); the attested-clock seam (`AttestedClock` + `readAttestedNow`); the STAGE-FAIL-CLOSED invariant in `runClaimsKernel` (a deterministic stage that can't complete → ESCALATE with an EMPTY renderable, never a partial render; `STAGE_FAIL_CLOSED_TERMINAL`).
  - **Registry-diff lint** (`src/claims/registry-diff.ts`; inv.17) — `classifyConsistencyTableDiff` / `classifyEvidenceRequirementDiff` / `classifyFalsifierDiff` + `assertNoRelaxation`: the mechanical ADDITIVE-vs-RELAXATION guard that makes soundness-monotonicity machine-checked. Generative inv.17 property tests prove each additive operator is demote-only and never mutates a surviving value.
  - **ClaimDefinition compiler — v1 slice** (`src/claims/claim-definition.ts`; inv.18) — the generic, registry-agnostic `ClaimDefinition` shape + a pure, total, FAIL-CLOSED completeness/consistency VALIDATOR that turns the three today-unenforced render/falsifier/template alignment CONVENTIONS into ONE definition-load-time mechanism. It REJECTS an incomplete/inconsistent definition (set); it does NOT yet GENERATE kernel artifacts (the DSL/codegen + the `Proof<Claim>` brand are the larger v2, out of scope). New exports: `ClaimDefinition`, `TemplateSlot`, `RenderTemplate`, `ValueProjection`, `ValidationContext`, `ValidationResult`, `ValidationFailureCode`, `validateClaimDefinition`, `validateClaimDefinitions`, `checkSlotProjectionAlignment` (the reusable slot↔projection static check). Eight invariants, all definition-load-time (no clock/RNG/IO), so the validator is testable in-repo against synthetic fixtures with no downstream link: INV-1 slot→projection→requiredEvidence alignment (convention (a)); INV-2 falsifier-completeness, folding in the existing `assertFalsifierDeclaration` §R throw (convention (b)); INV-3 template→registered-definition, which makes a dangling template state IMPOSSIBLE at load (convention (c)); INV-4 Triad decomposition-closure membership (both directions); INV-5 provenance default-deny; INV-6 C0 non-vacuity; INV-7 the C6 binding-key gate lifted to load-time; INV-8 structural validity (closed unions + cacheable-requires-ttl). TOTAL: the underlying `parseEvidenceRequirement` / `assertFalsifierDeclaration` throws are caught and converted to `{ ok: false, code, reason }`, never propagated. Shipped with a PROPERTY harness (a complete generated def always validates; dropping any facet always rejects; never throws on arbitrary input) + a MUTATION harness (remove/corrupt each facet → reject with the matching code).
  - **ClaimDefinition COMPILER — v2 slice** (`src/claims/claim-compiler.ts`; inv.18 v2) — the thesis inversion: the `ClaimDefinition` SOURCE is now the PRIMARY artifact and the runtime is its IMAGE. Where v1 (`claim-definition.ts`) shipped a fail-closed VALIDATOR, this ships the COMPILER that GENERATES the runtime artifacts FROM a source. New exports: `defineClaim` (the const-generic DSL builder), `compileClaimDefinition`, `lit`/`prop` (source slot constructors), `isFalsifierSetMonotone` (the designed-in versioning guard), and the types `ClaimDefinitionSource`, `RequiredKeyOf`, `NonEmpty`, `SourceSlot`, `RenderSource`, `DecompositionSource`, `FalsifierStance`, `CompiledArtifacts`, `CompiledRegistrySpec`, `CompiledClosure`, `CompiledFixtures`, `MutationFixture`. The compiler is a SMALL DECLARATIVE interpreter: a set of pure total FOLDS over the schema, each emitting one artifact (registry spec / value-projector data / render template / validator-wiring `ClaimDefinition` / decomposition closure / property + mutation fixtures / doc card), NEVER switching on `def.type` — adding a claim type is a new `defineClaim({...})` source with ZERO interpreter edits, so its size is constant in the number of types. The v1 invariants become DERIVED: the `defineClaim` builder makes an un-§5-gated `valueBinding.key` (INV-7), an empty `requiredEvidence` (INV-6), and an inconsistent `falsifier` stance (INV-2) COMPILE errors (illegal-states-unrepresentable via the F-bounded `RequiredKeyOf<Self>` key-union + non-empty tuples + a discriminated falsifier union); INV-1 (slot→projection→key alignment) is the SAME derived fact as INV-7 because the projector data is COMPUTED from `valueBinding` × the render slots, never authored twice. The load-time `validateClaimDefinitions` (v1) stays as defense-in-depth for wire/JSON-sourced defs + cross-type set resolution. Versioning is designed-in: every source carries `version`, every artifact is stamped `type@version`, and `isFalsifierSetMonotone` lets a CI step STRUCTURALLY check "a newer version is only ever safer" (append-only falsifier set). HONEST scope correction folded in: only `valueBinding.key` is constrained to the required-key union; falsifier keys are BY DESIGN cross-keys (the worked STORE_OPEN_NOW falsifier `schedule:schedule_override` is not in requiredEvidence), so the design's "falsifiers[].key in the union" claim is unsound and was NOT implemented.
  - **CanonicalClaim — the kernel-minted renderer-input brand** (`src/claims/canonical-claim.ts`; inv.17) — the runtime-non-forgeable carrier that is the renderer's REQUIRED input, mirroring `RenderedReply` EXACTLY (frozen heap object + module-private `unique symbol` brand that is NEVER exported + a module-private `WeakSet` provenance registry). It guards the OTHER end of the egress loop: CanonicalClaim brands the renderer's ENTRY, RenderedReply its EXIT (`model value → C6 ledger-bound → CanonicalClaim (kernel mint) → render() → RenderedReply → unwrapRendered at the sink`). The SOLE mint site is `runClaimsKernel` — minting is structurally reachable ONLY after a claim fully VALIDATED (the §5 predicate, incl. C6 value-binding) AND survived P2 into the `renderable` set; the mint flows the renderable claim's LEDGER-derived value (for any render-proposition type, INV-1 forces a `valueBinding`, so C6 ran and the value provably equals its licensing ledger entry). `ClaimsKernelResult` gains an ADDITIVE `renderableCanonical: readonly CanonicalClaim[]` (1:1 with `renderable`, empty on every non-RENDER terminal incl. STAGE-FAIL-CLOSED). New PUBLIC exports: the opaque `CanonicalClaim` TYPE + `unwrapCanonical` accessor (asserts WeakSet membership; throws on a forged literal). The brand symbol, the WeakSet, AND the `mintCanonicalClaim` constructor are intentionally NOT on the barrel — no public constructor (inv.17). Defense-in-depth: (a) the brand symbol is never exported; (b) the runtime WeakSet assert in `unwrapCanonical`; (c) a shared-eslint-config `no-restricted-syntax` ban on `x as CanonicalClaim` (and nested `as any as CanonicalClaim`), with the mint module (`packages/core/src/claims/canonical-claim.ts`, canonical path) exempted. Honest scope: `VALIDATED` alone does NOT imply C6 ran (a no-`valueBinding` type skips C6 and still validates); the ledger-bound guarantee flows through INV-1 (every render proposition ⟹ a valueBinding ⟹ C6) — see the module header.

  Still W5 (NOT in this change): ibatexas adoption — declaring falsifiers + value-bindings per registry type, the render-from-claims wiring, the decomposer, and the real source-of-record descriptors that move the §11 TCB Read-adapters row TRUSTED → VERIFIED.

  E-1 only for the egress brand — closed minters, enforcement, and retyped signatures land progressively; call-site value-binding (E-2) is a later wave. No existing export changes.

## 1.7.0

### Minor Changes

- 33fcb81: R1 claims-runtime kernel correctness:
  - 3-value `OriginProvenance` axis (`FIRST_PARTY | TRUSTED_THIRD_PARTY | UNTRUSTED_DATA`), distinct from the 2-value `LedgerTaint` — de-vacuums the `first_party_only` provenance gate so first-party money reads (e.g. `PAYMENT_STATUS`) are actually protected. Fail-closed: nothing auto-promotes to `FIRST_PARTY`.
  - C4 soundness broadened to `action_outcome` reads + negative-age lower bound on freshness.
  - P2 consistency: same-type discrimination; conservative `sameValue` rejects non-plain objects (distinct `Date`/`Map`/`Set` now surface an H3 same-key conflict → `UNKNOWN` instead of being silently treated as equal).

  Strengthens evidence-soundness gates only (monotonic, fail-closed); no gate is relaxed.

## 1.6.0

### Minor Changes

- 06eea00: Add the SDD claims runtime (Q1–Q5), re-exported from `@adjudicate/core`: the 3-valued `ClaimVerdict` + 4 `TurnTerminal`s, the per-type `EvidenceRequirement` schema, the `EvidenceLedger`, the soundness validator (`claimAllowed`), the consistency gate (`checkConsistency` / `ConsistencyClaim` / `SuppressionRecord`), and the three-kernel `Read`/`Action`/`Claims` interfaces (`runReadKernel`, `runClaimsKernel`) with the asymmetric Read+Action→Ledger→Claims→Renderer topology (`ASYMMETRIC_TOPOLOGY`). Purely additive — no existing export changed or removed.

## 1.5.0

### Minor Changes

- 6a73485: feat(core,audit): 052 — aggregate/limit snapshot INJECTION into the kernel decision + RECORDING into the audit record (replayable, §D-5), and the durable, coalesced aggregate-counting SUBSTRATE the multi-horizon limit guards (051) and the transactional reservation store (053) consume read-only. Per index §B/§D the aggregate/limit snapshot is an IMMUTABLE INJECTED SNAPSHOT, never a decision layer: it rides into the one kernel decision via injected `state`/deps (the impure shell computes it from the counting substrate; the kernel never refetches/mutates/timestamps it) and is recorded into the audit record so re-running the PURE kernel over the recorded snapshot reproduces the decision BIT-IDENTICALLY (invariant #5). 052 OWNS the substrate as its single owner; 051/053 CONSUME it read-only.
  - **T1 (`core/envelope.ts`):** add `AggregateSnapshot` (`{ windows: Record<string, number>; at }` — the per-(resource, horizon) committed-aggregate view + the shell-sampled sample time) and the RECORDED `RecordedAggregateSnapshot` (`{ snapshot, snapshotHash }`), co-located with `RecordedAuthoritySnapshot`. INJECTED STATE, NOT an envelope field: NOT in `intentHashInput` and NOT in `EXPECTED_ENVELOPE_KEYS` (the `intentHashInput`/`buildEnvelope`/`deriveIntentHash` bodies are BYTE-IDENTICAL — additive-only file change — so every envelope hash, golden vector, and replay corpus is unchanged; invariant #4/#5).
  - **T2 (`core/decision.ts`, `core/audit.ts`, `core/kernel/adjudicate-and-audit.ts`):** `recordAggregateSnapshot(snapshot)` content-addresses the injected snapshot (`hashAggregateSnapshot` over `@adjudicate/canonical`'s `sha256SnapshotCanonical`, RFC 8785 / JCS — NO forked canonicalizer); `aggregateSnapshotFromRecorded(recorded)` returns the SAME immutable snapshot on REPLAY after a FAIL-CLOSED integrity re-derive (throws when `snapshotHash` no longer matches its `snapshot` — tampered/drifted; invariant #6). New `AuditRecord.aggregateSnapshot` + `BuildAuditInput.aggregateSnapshot`, conditionally spread into the `auditHash` pre-image (like 033's `authoritySnapshot` and 091's `policyVersion`/`kernelVersion`) so the recorded snapshot is tamper-evident; records that injected none stay byte-identical (hash-stable). The wrapper threads a new read-only `AdjudicateAndAuditDeps.aggregateSnapshot` onto BOTH `buildAuditRecord` call sites (main/REWRITE-executed AND kill-switch early-return); it COEXISTS with the 011 REWRITE re-adjudication, 013 kill-switch, 091 version-binding, and 033 authority-snapshot recording as another conditional-spread recorded field — all wall-clock reads still route through `deps.clock ?? defaultClock`.
  - **T3 (`core/kernel/guard-stats.ts`):** document `GuardFireStats` as the SINGLE-OWNER counting substrate. It already coalesces same-`(guardName|guardPhase|decisionKind|day|packId)` buckets and writes the per-call DELTA (`count:1`) to the store, NOT the merged running total (writing merged produces triangular `N(N+1)/2` over-counts), and `queryAsync` reads the store DIRECTLY (no memory union → no double-count). 051's velocity guards and 053's reservation CONSUME this read-only via `queryAsync`; they MUST NOT re-implement the counter or write a non-additive path.
  - **T4 (`audit-postgres/src/guard-stats-store.ts`):** the durable additive contract — `UPSERT_GUARD_STAT_SQL` stays `ON CONFLICT (guard_name, guard_phase, decision_kind, day, pack_id) DO UPDATE SET count = audit_guard_stats.count + EXCLUDED.count` (atomic single-statement accumulate, NOT read-modify-write). FIX: the no-pack case now writes the empty-string sentinel `''`, NOT `null` — a NULL `pack_id` would (a) violate the implicit NOT NULL of a PK column (Postgres 23502) and (b), being treated as DISTINCT in PK/unique arbiters, defeat the `ON CONFLICT` so the upsert duplicates rows instead of coalescing (the over-count failure).
  - **T5 (`audit-postgres/migrations/006-add-guard-fire-stats.sql`):** EDIT the EXISTING migration's PK arbiter (no duplicate file): `pack_id` is now `TEXT NOT NULL DEFAULT ''` so the 5-column `PRIMARY KEY (guard_name, guard_phase, decision_kind, day, pack_id)` is the real, deterministic conflict target the additive `ON CONFLICT` depends on — making counting atomic/coalescing with no silent 42P10/23502.
  - **T6 (`audit/src/ledger.ts`):** document that the recorded aggregate snapshot persists on the durable, replayable governance record (`AuditRecord.aggregateSnapshot`, bound into `auditHash`), carried VERBATIM by this package's `replay.ts`/`replay-integrity.ts` (which take `AuditRecord[]` as-is); the hot-path Execution Ledger remains dedup-only.
  - **T7 (`runtime/src/defer-park.ts`):** align the over-commit-race reasoning — the EPHEMERAL Redis park counter's `INCR→EXPIRE→check→DECR` TOCTOU race (closed by the `evalIncrCheck` Lua seam) is a DIFFERENT atomicity mechanism from the DURABLE additive Postgres upsert; 053's reservation store MUST extend the durable additive template, NOT the ephemeral park sequence.

  The pure `adjudicate()` decision path, the closed 6-outcome `Decision` algebra, and `intentHashInput` are UNCHANGED (purity/determinism/replay preserved; counting + persisting stay in the impure shell, §D #5). 052 ships INJECTION + RECORDING + replayability + the counting substrate only; the velocity/limit guards that read it are 051 and the reservation store is 053. Monotonicity (§C) is preserved: an aggregate/limit signal may only RAISE friction, never authorize EXECUTE.

- 9056c6e: feat(core,audit,audit-postgres,admin-sdk): 093 — inter-record hash chain + external signed checkpoint + chain-continuity replay/read surface, and close the verify-on-read false-tamper cluster on the audit read path. Adds a true cryptographic inter-record link (`prevAuditHash`) so DELETION and REORDERING of audit records become detectable, anchors a chain segment with an externally-signed checkpoint that makes TAIL TRUNCATION detectable, and surfaces chain-continuity status through the replay harness, the supersession-chain report, the cold-store read path, and the admin/console query. All chain machinery lives in the impure shell AFTER the pure decision (§D: "the kernel decides; the shell signs and persists"); `prevAuditHash` is EXCLUDED from the `auditHash` pre-image and from the decision, so the pure `adjudicate()` is byte-unchanged and byte-identical replay (constitutional invariant 5) holds with chain fields present.
  - **T1 (`core/audit.ts`):** add the optional `prevAuditHash?: string` field to `AuditRecord` (the per-stream cryptographic TIP — the `auditHash` of the immediately-preceding record) and an optional `prevAuditHash` on `BuildAuditInput`. `buildAuditRecord` computes `auditHash` FIRST, then threads `prevAuditHash` onto the returned record — so it is EXCLUDED from the `sha256Canonical(baseRecord)` pre-image exactly like `signature`/`metadata`. A genesis record (no predecessor) is `undefined` and hashes byte-identically to a pre-093 record. `verifyAuditRecord` now strips `{auditHash, signature, metadata, prevAuditHash}` before re-deriving, so attaching/threading/changing the chain link never false-tampers an otherwise-intact record. NEVER read by `adjudicate()`, NEVER enters `intentHash` (invariant #4).
  - **T2 (`audit/replay-integrity.ts`):** add the `AUDIT_CHAIN_BROKEN` `IntegrityFailure` kind and a per-stream (session) cursor in `replayWithIntegrity` that compares each record's `prevAuditHash` to the immediately-preceding record's `auditHash` in the same stream. A deleted/reordered INTERIOR record is flagged DISTINCTLY from `AUDIT_HASH_TAMPERED` (each record's own bytes can be intact; the LINK between them is broken — the attack the logical `predecessorIntentHash` could not detect). A genesis record (no link) and an out-of-window predecessor are not flagged (no false positives).
  - **T4 (`audit/replay-integrity.ts`):** add the external signed checkpoint over the chain tip — `AuditCheckpoint` (`{sequence, tipAuditHash, count, signature}`), `auditCheckpointPreimage`, `emitAuditCheckpoint(records, signer, sequence)`, `verifyAuditCheckpoint(records, checkpoint, opts?)`, and the `AuditCheckpointVerification` result. It reuses the 092 `AuditSigner` + `{keyId, alg, value}` signature shape over a versioned canonical pre-image (`AUDIT_CHECKPOINT_PREIMAGE_VERSION`). A deleted TAIL no longer reproduces the signed `(tip, count)` → `count_mismatch`/`tip_mismatch`; the signature (hash-bind verified pure-JS, asymmetric via an injected verifier) stops a forged checkpoint from matching the truncated set; the bound `sequence` stops checkpoint replay at another position.
  - **T3 (`audit/supersession-chain.ts`):** surface the per-stream cryptographic tip on each `SupersessionChainNode` (`auditHash` + `prevAuditHash`) and add a `chainBreaks` diagnostic to `SupersessionChainReport`: a record whose `prevAuditHash` does not equal its RESOLVED supersession predecessor's `auditHash` — the cryptographic break surfaced DISTINCTLY from the (still-resolving) logical `predecessorIntentHash` link.
  - **T5/T6 (`audit-postgres/migrations/012-add-prev-audit-hash.sql`, `src/postgres-sink.ts`):** new ADDITIVE, idempotent migration `012-add-prev-audit-hash.sql` (NOT 011 — `011-create-turn-trace.sql` exists) adding `prev_audit_hash TEXT` PLUS `authority_snapshot_jsonb`/`aggregate_snapshot_jsonb` JSONB columns — NO CHECK widening, NO index/arbiter change (migration 009's UNIQUE arbiter and the 010 `record_version` CHECK are untouched, so the 42P10/23514 activation blockers cannot recur). `INSERT_AUDIT_SQL` / `auditInsertParams` / `IntentAuditRow` / `recordToRow` grow from 25 to 28 columns binding the three new columns in declared order.
  - **T7/T8 (`audit-postgres/src/replay.ts`, `src/audit-store.ts`):** `rowToRecord` rehydrates `prevAuditHash` from the new column (excluded from the pre-image — round-trips) AND, closing the **092-F1 read-path false-tamper cluster**, rehydrates `authoritySnapshot` (033) + `aggregateSnapshot` (052) with the SAME presence-exact omission `buildAuditRecord` used — both ARE in the `auditHash` pre-image, so before 093 a snapshot-bearing record round-tripped through Postgres re-derived a DIFFERENT hash and 092 verify-on-read FALSELY flagged it tampered (fail-SAFE, never fail-open). The cold-store reader's `SELECT_COLUMNS` carries the three new columns.
  - **T9 (`admin-sdk/src/handlers/audit-query.ts`, `src/schemas/query.ts`):** the audit-query handler computes per-stream, ORDER-INDEPENDENT (sorts each stream chronologically before walking, since the cold store lists newest-first) chain-continuity over the returned records and surfaces it as the new optional `AuditQueryResult.chainIntegrity` (`{checked, breaks[]}`) — additive, reading only fields already on each record (no hashing), never altering `records`/`verifications`. Computed standalone (admin-sdk is the BASE package and must not depend on `@adjudicate/audit`).
  - **admin-sdk read-path-owner schema fix (the verify-on-read false-tamper cluster at the WIRE boundary):** Zod `.object()` STRIPS unknown keys, and the tRPC `audit.query`/`audit.byHash` procedures gate output through `AuditRecordSchema`/`AuditQueryResultSchema`. The wire `SupersessionSchema` omitted `binding` (071-F1) and `AuditRecordSchema` omitted `aggregateSnapshot` (052) — BOTH in the `auditHash` pre-image — so a binding/aggregate-bearing record was STRIPPED at the wire and any downstream re-verify FALSELY tampered. Added `binding` to `SupersessionSchema`, `aggregateSnapshot` (+ a new `RecordedAggregateSnapshotSchema`/`AggregateSnapshotSchema` in `schemas/envelope.ts` with drift guards) and `prevAuditHash` to `AuditRecordSchema`. A wire round-trip now preserves every pre-image field and `verifyAuditRecord` stays `verified:true`.
  - **T10 (`apps/console`):** `chainIntegrity` flows through the mounted `adminRouter` + the additive `AuditQueryResultSchema.chainIntegrity` output gate unchanged; a console-side test pins that it reaches the tRPC response via the exact `withVerifyOnRead(store) → createAuditQueryHandler` pipeline the route mounts.

  Invariants preserved: the pure `adjudicate()` path, `intentHashInput`/`EXPECTED_ENVELOPE_KEYS`, and the closed 6-outcome `Decision` algebra are UNCHANGED (no confidence/metadata on Decision; chain fields ride the persist/replay side as injected/recorded state, never the hashed envelope pre-image; invariants #2/#3/#4/#5). Chain-continuity verification and checkpoint validation only ADD friction (§C), never authorize. New tests: `prevAuditHash` pre-image exclusion + genesis verify (`core/tests/audit-record-v5.test.ts`); byte-identical replay with the chain field (`core/tests/kernel/invariants/replay-determinism.property.test.ts`, 1000-run property); chain-break (delete/reorder) + interleaved-stream + out-of-window cases and full checkpoint deleted-tail/forged/sequence-binding suite (`audit/tests/replay-integrity.test.ts`); per-stream cryptographic tip + broken-link-distinct-from-logical-link (`audit/tests/supersession-chain.test.ts`); 28-column INSERT/params binding + chained/snapshot round-trip false-tamper closure + migration-012 additive-only guards (`audit-postgres/tests/postgres-sink.test.ts`); live-PG migration-012-applies + round-trip-verified (`audit-postgres/tests/integration.test.ts`, validated 19/19 against the docker `ibatexas` stack); chain-integrity surfacing + wire-schema-carries-pre-image-fields (`admin-sdk/tests/audit-query-handler.test.ts`, `schemas-roundtrip.test.ts`); and `chainIntegrity` reaching the tRPC response (`apps/console/src/lib/audit-verification.test.ts`).

- b77f6b0: feat(core,audit,audit-postgres,admin-sdk): 092 — pluggable `AuditSigner` + verify-on-read. Wire a real cryptographic `signature` over each audit record's `auditHash` (replacing the never-populated keyless stub) and verify records on the cold-store READ path so tampered/forged rows are FLAGGED rather than rendered as authoritative. Signing and verification live entirely in the impure shell AFTER the pure decision (§D: "the kernel decides; the shell signs and persists") — the pure `adjudicate()` is byte-unchanged and never signs. The `signature` stays EXCLUDED from the `auditHash` pre-image, so post-hoc signing never invalidates tamper-evidence; verify-on-read only ADDS friction (§C), never authorizes.
  - **T1 (`core/audit.ts`):** add the `AuditSignature` type, the pluggable `AuditSigner` interface (`{ keyId; sign(auditHash) }`), the browser-safe pure-JS hash-bind signer (`hashBindAuditSigner` / `bindAuditSignature` / `auditSignaturePreimage`, `alg: "sha256-hashbind"`, mirroring `bindCapability`), and the `AUDIT_HASHBIND_ALG` / `AUDIT_SIGNATURE_PREIMAGE_VERSION` constants. `buildAuditRecord` gains an optional `signer` on `BuildAuditInput`: it computes `auditHash` FIRST, then attaches `signer.sign(auditHash)` — a THROWING signer propagates (FAIL-CLOSED, §D inv. 6). `verifyAuditRecord` gains a new `{ verified:false, reason:"invalid_signature", keyId, alg }` outcome layered ON TOP of the four-way union: the hash-bind leg is verified pure-JS in core; an optional `VerifyAuditRecordOptions.verifySignature` hook lets a node caller verify asymmetric (ed25519) signatures. Core stays browser-bundleable: no `node:crypto`, no `Buffer`. An ABSENT signature stays a valid, tamper-evident-only record (the OSS contract).
  - **T2 (`core/kernel/adjudicate-and-audit.ts`):** thread `signer` through `AdjudicateAndAuditDeps` and populate `record.signature` at BOTH `buildAuditRecord` call sites — the kill-switch early-return REFUSE AND the main (incl. 011 REWRITE-executed) site. A signer error FAILS CLOSED: it propagates out of `buildAuditRecord` BEFORE `sink.emit`, so no unsigned record is ever emitted when a signer was configured (friction, never bypass). Coexists with 011/013/091/052/033 conditional-spread fields; all wall-clock reads still route through `deps.clock ?? defaultClock`.
  - **T3 (`audit/src/replay-integrity.ts`):** map the new signature verdict — `IntegrityFailure.kind` gains `AUDIT_SIGNATURE_INVALID` (distinct from `AUDIT_HASH_TAMPERED`) so an operator can tell "the bytes were modified" from "the bytes are intact but the signature is not authentic"; the existing tamper/intent-mismatch axes are unchanged.
  - **T4 (`audit-postgres/src/audit-store.ts`):** VERIFY-ON-READ on the cold-store read path. `query` runs `verifyAuditRecord` over every returned row and populates the new `AuditQueryResult.verifications` array (index-aligned with `records`; pure / no-I/O so cost is bounded per row). `getByIntentHash` verifies the single row and attaches the verdict via a non-enumerable Symbol slot (`readVerificationSlot`) so the `AuditStore` contract and serialized shape are unchanged. A forged/tampered row is FLAGGED, never dropped (forensics keep the bytes) and never silently authoritative. Reuses the existing `signature`/`audit_hash` rehydration in `replay.ts` (no migration — the columns already exist).
  - **T5 (`admin-sdk`):** add `AuditRecordVerificationSchema` (mirrors the core verdict union) and an OPTIONAL `verifications` array on `AuditQueryResultSchema`; `createAuditQueryHandler` passes the store's verdicts through UNCHANGED (the InvalidCursorError → BAD_REQUEST mapping is preserved). A store that does not verify on read simply omits the field.
  - **T6 (`apps/console`):** new `withVerifyOnRead` store decorator (idempotent — it fills in verdicts only when the inner store omitted them) wraps the route's audit store so the admin Explorer's `audit.query` response carries per-record tamper/signature status in BOTH Postgres and in-memory modes.

  The closed 6-outcome `Decision` algebra, the guard order, and `intentHashInput` are UNCHANGED. Monotonicity (§C) holds: verify-on-read surfaces tamper/forgery — it never weakens a decision or authorizes EXECUTE.

- 5a261ef: feat(core): 032 — authority-graph data model + store + PURE ownership resolver. Add the `AuthorityGraph` snapshot model (`principal —relationship→ resource —permits→ {actions, limits}`, index §G) co-located with the actor/envelope contracts in `envelope.ts`: new `AuthorityRelationship` (`owns`/`joint`/`advisor`/`custodian`), `AuthorityPermits`, `AuthorityEdge`, `AuthorityGraph` types. The graph is an IMMUTABLE INJECTED SNAPSHOT (index §B/§D) — it is NOT an envelope field and does NOT enter the `intentHashInput` pre-image (`intentHashInput`/`EXPECTED_ENVELOPE_KEYS` are byte-identical to their post-031 value; every existing envelope hash is unchanged). In `decision.ts` add `createAuthorityGraphStore` (a read-only, frozen-snapshot lookup with a pure `edgesFor(principal, resource)`) and the pure `resolveOwnership(store, envelope) => OwnershipFact` resolver: it binds the envelope's declared owner/resource (`resourceRefs`, 031) to the snapshot and returns a FACT (`{ principal, resource, bound, relationships, permits, edges }`) — NEVER a `Decision` (index §B), so it can never authorize EXECUTE or lower friction (index §C). `hashAuthorityGraph` content-addresses the snapshot via `@adjudicate/canonical` for replay (invariant #5). PURE & synchronous (no clock/RNG/IO, kernel-purity §D). The closed 6-outcome `Decision` algebra is UNTOUCHED — no 7th outcome, no `confidence`/`metadata` field (invariant #2). ADDITIVE: no pack policy, no authority guard (034), no AC-007 (035).

  feat(canonical): 032 — add `canonicalSnapshot` / `sha256SnapshotCanonical`, intent-revealing aliases over `canonicalJson` / `sha256Canonical` for injected-snapshot serialization (authority-graph + future aggregate/limit snapshots). They DELEGATE byte-for-byte — same NFC normalization, same `RangeError` on non-finite, same undefined-elision — so a recorded snapshot replays bit-identically (invariant #5) and never drifts from `intentHash` semantics; NO forked canonicalizer (index §B caveat). Re-exported from `@adjudicate/core` via `hash.ts`. Golden-vector tests pin authority-graph snapshot canonicalization (key-order insensitivity, edge-array order significance, NFC, fail-on-non-finite, tamper-evidence).

  feat(primitives): 032 — add `ownershipBindingPredicate`, the seam that adapts an `OwnershipFact` to the EXISTING `requireTenantBinding(isActorBoundToTenant)` predicate shape `(actor, state) => boolean` (identity on `OwnershipFact.bound`) so plan 034 can wire a constitutional authority guard onto a pack's `authGuards` WITHOUT reshaping the fact. Seam ONLY — 032 wires NO guard; PIX/access bundles still ship `authGuards:[]`. Pure; browser-safe; no authorization (a `false` lets `requireTenantBinding` REFUSE — raise friction — never EXECUTE).

  feat(conformance): 032 — extend `ConformanceOptions` with an optional `authorityGraph` snapshot so a later authority/ownership check (035 AC-007) can be fed the graph deterministically without another options change. Surface only: `DEFAULT_CHECKS` (AC-001..AC-006, AC-008) is unchanged, AC-007 stays out of scope (035), and `runConformance` does NOT throw on the option's presence/absence (a report with the snapshot supplied is identical to one without).

- 014e8fe: feat(core): 033 — authority-snapshot INJECTION into the kernel decision + RECORDING into the audit record (replayable, §D-5). Per index §B/§D the authority-graph snapshot is an IMMUTABLE INJECTED SNAPSHOT, never a decision layer: it rides into the one kernel decision via injected `state` (because `Guard<K,P,S>` is `(envelope, state)` — the kernel never passes identity) and is recorded into the audit record so re-running the pure kernel over the recorded snapshot reproduces the decision BIT-IDENTICALLY (invariant #5).
  - **T1 (`envelope.ts`):** add the RECORDED `RecordedAuthoritySnapshot` type `{ graph, snapshotHash }` (the injected `AuthorityGraph` + its `hashAuthorityGraph` content-address), co-located with `AuthorityGraph`/`IntentActor`. It is INJECTED STATE, NOT an envelope field: it is NOT in `intentHashInput` and NOT in `EXPECTED_ENVELOPE_KEYS` (both byte-identical to their post-031 value — invariant #4 untouched, every envelope hash unchanged).
  - **T2 (`install.ts`):** thread the snapshot through `installPack` — the documented injection seam (no existing guard injection, no signature check). New optional `InstallPackOptions.authoritySnapshot?: AuthorityGraph`; when supplied, `installPack` content-addresses it (`recordAuthoritySnapshot`) and exposes the RECORDED snapshot on `InstalledPack.authoritySnapshot`. NO authority guard is wired (that is 034); the pack's `authGuards` are untouched.
  - **T3 (`pack-conformance.ts`):** record the injected snapshot by REUSING the `withBasisAudit`/`wrapBundle` idempotent, non-blocking discipline — `recordAuthoritySnapshotOnPack` stamps the recorded snapshot onto a NEW pack object under a `Symbol.for` tag (non-enumerable, never a hashed byte), `readRecordedAuthoritySnapshot` reads it back. Idempotent on an equal snapshot; mutates no guard/policy/Decision. The audit record itself carries it: `AuditRecord.authoritySnapshot` + `BuildAuditInput.authoritySnapshot`, conditionally spread into the `auditHash` pre-image (like 091's `policyVersion`/`kernelVersion`) so the recorded snapshot is tamper-evident and records that injected none stay byte-identical (hash-stable).
  - **T4 (`canonical`):** the recorded snapshot rides 032's `canonicalSnapshot`/`sha256SnapshotCanonical` (RFC 8785 / JCS, NFC, fail-on-non-finite) — NO forked canonicalizer — so it replays bit-identically. Golden-vector tests pin the recorded `{ graph, snapshotHash }` surface.
  - **T5 (`decision.ts`):** `recordAuthoritySnapshot(graph)` builds the recorded snapshot; `authorityGraphStoreFromRecorded(recorded)` re-derives the read-only store from the RECORDED snapshot on REPLAY (so the pure resolver re-runs over byte-identical edges → byte-identical `OwnershipFact` → byte-identical Decision) and FAILS CLOSED (throws) when the recorded `snapshotHash` no longer matches its `graph` (tampered/drifted recorded snapshot — invariant #6). The closed 6-outcome `Decision` algebra is UNTOUCHED (no 7th outcome, no field — invariant #2).

  feat(admin-sdk): 033 — surface the recorded authority snapshot on the audit-envelope schema. Add `AuthorityRelationshipSchema`/`AuthorityPermitsSchema`/`AuthorityEdgeSchema`/`AuthorityGraphSchema`/`RecordedAuthoritySnapshotSchema` (mirroring the core types, with bidirectional build-time drift guards) and the OPTIONAL `authoritySnapshot` field on `AuditRecordSchema`, so recorded decisions expose the injected snapshot for replay/inspection. The `_recordCoreToSchema` drift guard enforces that the schema tracks `AuditRecord`.

  fix(audit-postgres): 033 — `recordedAuthoritySnapshotFromRow` degrade-safe legacy read. The record-level recorded snapshot is 033-new; OLDER audit rows lack it. The tolerant reader returns the recorded snapshot when a structurally-valid one is present and `undefined` otherwise (unreadable JSON, absent, or malformed) — NEVER throws — so legacy rows reconstruct an `AuditRecord` with NO `authoritySnapshot` key (byte-identical, hash-stable, no false-positive tamper on verify). Mirrors the drop-safe `resourceRefs` posture in `legacyV1ToV2`.

  033 ships the INJECTION + RECORDING + replayability only. It does NOT wire the authority guard (034) and does NOT add AC-007 (035). REUSES 032's `AuthorityGraph`/`createAuthorityGraphStore`/`resolveOwnership`/`hashAuthorityGraph`/`canonicalSnapshot` — nothing re-implemented.

- f34c493: feat(adapter): 022 — single-use capability burn store + expiry + nonce reconciliation. Introduce the AUTHORIZATION-BEARING at-most-once redemption primitive the cap-gated executor (024) will consume: a kernel-minted `Capability` (021) is redeemed EXACTLY ONCE before its executor honors it. Unlike the lossy display-only approval registry and the `ConfirmationStore` (a transport for a user-held token), this store OWNS the single-use guarantee via an ATOMIC claim-and-burn. ADDITIVE: no consumer is wired into the kernel decision here (024 wires the executor); removing the store cannot loosen any guard, so the failure mode is friction, never bypass (§C / §D #6).
  - **T1 (`adapter-core/persistence.ts`):** `BurnStore<R = Capability>` interface (`mint(nonce, record, ttlSeconds) → boolean` first-writer-wins; `burn(nonce) → R | null` atomic claim-and-burn) + `createInMemoryBurnStore` reference. The in-memory burn is atomic within the single-threaded event loop (read+delete synchronous between `await` points → two concurrent burns yield the record AT MOST ONCE), with an opportunistic `sweepExpired` on `mint` bounding the map (mirrors `createInMemoryConfirmationStore`). Expiry fails closed: a grant past TTL returns `null`.
  - **T2 (`adapter-core/persistence-redis.ts`):** `createRedisBurnStore` closing the production confirmation store's documented NON-ATOMIC GET-then-DEL double-spend race. `mint` claims via `SET NX EX` (first-writer-wins, mirroring `Ledger.recordExecution` / `defer-resume`'s `acquired !== "OK"` suppression); `burn` runs the ATOMIC Lua `EVAL` get-and-delete (`BURN_CLAIM_AND_BURN_LUA` = `GET; if v then DEL; return v`) so two concurrent burns of one nonce cannot both observe the pending grant — at-most-once holds across replicas WITHOUT relying on the kernel-ledger `REPLAY_SUPPRESSED` backstop. `eval` is REQUIRED (no safe non-atomic fallback for a single-use authorization store). Fail-closed: a burn miss, an EX-expired (gone) key, a malformed blob, or a store/IO error yields NO redemption, never a fail-open grant.
  - **T3 (`core/envelope.ts`):** `reconcileNonceHash(envelope, presentedHash)` — re-derives the nonce-bound `intentHash` via `deriveIntentHash` over the UNTOUCHED `intentHashInput` recipe (`{version,kind,payload,nonce,actor,taint,origin[,resourceRefs]}` — invariant #4 byte-identical, `createdAt` still excluded) and compares constant-time via `timingSafeHexEqual` (fail-closed `false`, never throws). The Redis-side `reconcileBurnedCapability(capability, envelope)` composes it with a `timingSafeHexEqual` cap-vs-stored-hash check so a burned grant is honored only when the presented envelope STILL re-derives to the bound hash — a mutated nonce (or any tampered hashed field) is rejected. Uses `@adjudicate/canonical` `sha256Canonical` (via core's `hash.ts` re-export), NOT the conformance fork.
  - **T4 (`adapter-core/index.ts`):** export the burn store + reconciliation seam (`createInMemoryBurnStore`, `createRedisBurnStore`, `reconcileBurnedCapability`, `BURN_CLAIM_AND_BURN_LUA`, `BurnStore`, `BurnRedisClient`, `CreateRedisBurnStoreOptions`) and re-export the single canonical hash path (`sha256Canonical`, `reconcileNonceHash`, `timingSafeHexEqual`) so the burn seam pins ONE encoder.

  The pure `adjudicate()` decision path, the closed 6-outcome `Decision` algebra, and `intentHashInput` are UNCHANGED (purity/determinism/replay preserved; burn + persist stay in the impure shell, §D). REUSES 021's `Capability`/`bindCapability`, the `defer-resume` SET-NX claim pattern, and the shared `timingSafeHexEqual`/`sha256Canonical` — nothing re-implemented.

- a9be0ad: feat(adapter-core,core,pack-payments-pix,pack-incident-response,pack-access-governance): 024 — cap-gated executor contract + adapter wiring. Make the executor honor a kernel-shell-minted, single-use, resource-bound capability instead of a raw envelope, so the §B "on EXECUTE → mint signed CAPABILITY → capability-gated EXECUTION FABRIC" edge is enforced in code, not by pack-author convention. `intentHashInput`, the pure `adjudicate()` path, and the closed 6-outcome `Decision` algebra are UNCHANGED: the kernel decides, the impure shell mints/signs AFTER the decision (§D), and constitutional invariant #1 holds bytewise — only EXECUTE (or the REWRITE-rewritten envelope) reaches `invokeIntent`, now additionally gated by a burned-on-use capability.
  - **`@adjudicate/adapter-core` (`src/types.ts`, `src/decisions.ts`, `src/loop.ts`, `src/index.ts`):** add the `CapabilityGate` contract (T1) — a DEPENDENCY-INJECTED gate carrying `mint` (the node-side ed25519 `signCapability`, supplied by the adopter), `verify` (the node-side `verifyCapabilitySignature` bound with the issuer's public keys), 022's atomic `burnStore`, and `kernelId`. Adapter-core never imports `@adjudicate/approval-engine` (that would be a dependency cycle) and stays `node:crypto`-free / browser-bundleable, mirroring the config-seal verifier seam. New `AdjudicatedAgentOptions.capabilityGate` (T2) — **DEFAULT OFF**; omitted ⇒ byte-identical to the pre-024 raw-envelope seam (rollback dial, §7). When set, the loop's shell MINTS + SIGNS a capability bound to the EFFECTIVE (EXECUTE or REWRITE-rewritten) envelope's `intentHash` AFTER the pure decision and `mint`s it into 022's store keyed by the effective nonce (best-effort; a throwing signer/store leaves no grant ⇒ the seam fail-closes). `runExecute` (T5) then redeems it EXACTLY ONCE before `invokeIntent`: BURN from 022's atomic store (single-use; a second use re-burns to `null` and is suppressed — never a parallel one), ed25519-VERIFY via the injected `verify`, and constant-time-BIND the capability's `intentHash` to the effective envelope's own hash (anti-IDOR / anti-replay). Any failure (burn miss/expiry, store/IO error, bad signature, hash mismatch) ABORTS the EXECUTE — `invokeIntent` is never reached (invariant #1, fail-closed per #6; §C: gating only adds friction). Composes ABOVE the existing 023 `verifyResourceBinding` fence. `CapabilityGate` re-exported from the package surface (T6).
  - **⚠️ Kernel authority is ed25519, NOT the forgeable hash-bind (021-F1 footgun).** The gate honors a capability as kernel-minted ONLY when the injected `verify` returns true; adopters MUST wire approval-engine's ASYMMETRIC `verifyCapabilitySignature`, NEVER core's pure-JS `verifyCapability` (which checks only hash-bind self-consistency — integrity, not authenticity, and is forgeable by anyone who can recompute the canonical hash). The contract type documents this; the approval-engine integration test PROVES it end-to-end (a `bindCapability` hash-bind-only grant with `alg:"sha256-hashbind"` is REJECTED by the gate; an `ed25519`-signed one is accepted).
  - **`@adjudicate/core` (`src/llm/planner-conformance.ts`, `src/llm/index.ts`):** (T4) widen the optional `pack` parameter of `safePlan` and `assertPlanSubsetOfPack` from a full `PackV0` to the minimal `PackIntentsSurface` (`{ intents }`). A full `PackV0` is structurally assignable (existing callers — `install.ts`, tests — unaffected), but the narrower surface lets a Pack break the planner↔pack construction cycle and adopt the pack-bound 3-arg form from its own `capabilities.ts` (the full `PackV0` value references the planner). `PackIntentsSurface` exported from `@adjudicate/core/llm`. The pure capability schema (021: `Capability`/`UnsignedCapability`/`verifyCapability`) was already exported from `@adjudicate/core` and is unchanged.
  - **`@adjudicate/pack-payments-pix` / `pack-incident-response` / `pack-access-governance` (`src/capabilities.ts`):** (T4) adopt the pack-bound 3-arg `safePlan(planner, classification, pack)` form — declare the pack's intent tuple (`PIX_INTENTS`/`INCIDENT_INTENTS`/`ACCESS_INTENTS`, `satisfies readonly <Kind>[]` to pin against the declared `IntentKind` union, and asserted equal to the `PackV0.intents` field in conformance tests so they cannot drift) and pass `{ intents }` so `assertPlanSubsetOfPack` runs on EVERY `plan()` (not only at install). A planner advertising an intent kind absent from the pack's declared intents now throws `PlanConformanceError` loud — the subset invariant the shipped 2-arg form never engaged.

  Tests: cap-gate single-use redemption / second-use suppression / burn-miss fail-closed / ed25519-verify rejection (non-vacuous toggle) / intentHash-bind anti-IDOR / store-error fail-closed / REWRITE-path gating / OFF-by-default no-op (`adapter-core/tests/cap-gate.test.ts`); the name-collision-not-READ cap-gate-bypass closure (`adapter-core/tests/bridge.test.ts`); the REAL ed25519 kernel-authority leg through the actual executor seam incl. the 021-F1 hash-bind-rejection and cross-intent-replay (`approval-engine/tests/cap-gate-ed25519.test.ts`); the 3-arg subset-leak conformance via the new `PackIntentsSurface` (`core/tests/llm/plan-allowed-intents.test.ts`); the shipped-pack subset invariant engaged non-vacuously (`pack-payments-pix/tests/conformance.test.ts`); and constitutional invariant #1 / kernel-unchanged under the cap gate (`core/tests/kernel/invariants/untrusted-never-executes.property.test.ts`). No schema/persistence migration is hashed (the gate is pure registry/runtime wiring, not ConfigSeal-pinned), so revert is clean (drop the worktree branch / unset the option).

- e8698b1: feat(core,adapter-core,audit,admin-sdk,pack-payments-pix,pack-incident-response,pack-access-governance): 025 — capabilities-as-budgets (bounded standing pre-auth). Add a human-granted, BOUNDED, STANDING pre-authorization that lets a CLASS of intents satisfy the "ask first" threshold up to a declared limit WITHOUT a per-intent confirmation receipt. The pure kernel only ever SUBSTITUTES EXECUTE for the threshold-style outcome (exactly as the confirmation-receipt override does today) and never weakens any state/taint/auth/business guard; the impure shell burns the budget down per EXECUTE. `intentHashInput`, the pure `adjudicate()` path, and the closed 6-outcome `Decision` algebra are UNCHANGED (§D #2: no new Decision kind, no `confidence`/free metadata). The budget substitution is monotonicity-preserving (§C) and fully replayable (§D #5): omitting the additive `budgetGrant` deps slot keeps records byte-identical to pre-025.
  - **`@adjudicate/core` (`src/audit.ts`, `src/basis-codes.ts`, `src/kernel/adjudicate-and-audit.ts`, `src/explain.ts`):** add the `BudgetGrant` data contract (`{ budgetId, intentKind, limit, windowSeconds }`) and the `budget_satisfied` `SupersessionReason` (T1/T3). Add the `budget` basis category with `BASIS_CODES.budget.SATISFIED` (T1). New optional additive `AdjudicateAndAuditDeps.budgetGrant` slot (T1); when supplied AND `grant.intentKind === envelope.kind` AND the kernel returned `REQUEST_CONFIRMATION`, the kernel substitutes `EXECUTE` with an appended `budget:satisfied` basis and auto-derives a `budget_satisfied` `Supersession` linking back to the original REQUEST_CONFIRMATION row (`token` carries the `budgetId`) — EXACTLY mirroring the `confirmationReceipt` override (T2). REFUSE/REWRITE/ESCALATE/DEFER/EXECUTE pass through UNCHANGED. The branch is the second site (after the confirmation receipt) explicitly allowlisted under the `@adjudicate/monotonic-ceiling` lint as a deterministic §C carve-out (a human-granted bounded pre-auth is a recorded deterministic input, not a risk model lowering a ceiling). The kernel does NOT verify or count the grant — the shell asserts it ONLY after a successful atomic decrement. Explain narrations added for `budget:satisfied` and `supersedes:budget_satisfied`.
  - **`@adjudicate/adapter-core` (`src/persistence.ts`, `src/decisions.ts`, `src/loop.ts`, `src/types.ts`, `src/index.ts`):** add the authoritative single-use-COUNTED `BudgetStore` + `createBudgetStore` (T4) backed by the ATOMIC `ParkRedis.evalIncrCheck` Lua primitive (increment-and-check against `limit`) — a deliberately DISTINCT store from 022's claim-and-burn `BurnStore` (a budget METERS N substitutions; a capability BURNS a single token; a per-token burn cannot express an N-use budget). Concurrent burn-downs over a `limit`-N budget yield AT MOST N grants across replicas (the headline atomicity guarantee), WITHOUT mirroring the non-atomic GET+DEL caveat of `persistence-redis.ts`. A client without `evalIncrCheck` throws at construction (no silent non-atomic fallback — fail-closed §D #6). Add an in-memory `evalIncrCheck` to `createInMemoryDeferStore` (atomic within the single-threaded event loop; window refills on TTL expiry). Add the `runBudgetBurnDown` shell helper (T5) that calls `evalIncrCheck` directly (decrement-then-assert-grant; fail-closed on over-limit / missing-primitive / store error). Wire it into the loop's send path (T5): on a REQUEST_CONFIRMATION for a budget-capable kind, resolve a grant (`AdjudicatedAgentOptions.budget.resolveGrant` — host authority), atomically burn down, and on a successful in-budget decrement RE-adjudicate with `budgetGrant` asserted — yielding a budget-satisfied EXECUTE that supersedes the REQUEST_CONFIRMATION row. **DEFAULT OFF** (option omitted) ⇒ byte-identical to the pre-025 REQUEST_CONFIRMATION path (rollback dial, §7). Authority stays in the single-use-counted counter, never the lossy approval projection.
  - **`@adjudicate/audit` (`src/supersession-chain.ts`):** extend the exhaustive `Record<SupersessionReason, number>` reason-count map + key list with `budget_satisfied` (T6). A budget-satisfied EXECUTE rides the existing EXECUTE-claim ledger plumbing — it claims a key first-writer-wins exactly like any EXECUTE; a second attempt for the same intentHash is REPLAY_SUPPRESSED, so the budget burn is observable in the ledger without weakening first-writer-wins.
  - **`@adjudicate/admin-sdk` (`src/schemas/basis.ts`, `src/schemas/audit.ts`):** add `budget` to `BasisCategorySchema` (keeps the build-time core↔wire drift guard satisfied) and `budget_satisfied` to `SupersessionReasonSchema`, so a budget-satisfied record round-trips through the admin wire schemas (consequence of the new core category/reason).
  - **`@adjudicate/pack-payments-pix` / `pack-incident-response` / `pack-access-governance` (`src/capabilities.ts`, `src/index.ts`):** declare the budget-CAPABLE intent class (T7): `PIX_BUDGET_CAPABLE_INTENTS` (`pix.charge.create`/`pix.charge.refund` — the LLM-proposable money-movers; the TRUSTED-only `pix.charge.confirm` webhook is NOT budget-capable), `INCIDENT_BUDGET_CAPABLE_INTENTS` (`incident.remediation.execute`), `ACCESS_BUDGET_CAPABLE_INTENTS` (`access.request`/`access.revoke`). Each is `satisfies readonly <Kind>[]`, a non-empty subset of the pack's declared intents (asserted in conformance tests), and excludes system-only/escalate kinds. Re-exported from each pack surface so a host wires `budget.resolveGrant` against an operator-authorized subset.

  Tests: kernel substitution + non-flip over all six outcomes + supersession + ledger-claim + confirmation-receipt-wins precedence (`core/tests/kernel/budget-grant.test.ts`); determinism fence (additive-omitted-slot byte-identical auditHash + replayable same-grant byte-identical record) + closed-algebra + property over random kinds (`core/tests/kernel/invariants/budget-substitution.property.test.ts`); atomic at-most-`limit` under CONCURRENT burn-down + window-refill + missing-primitive/store-error fail-closed + loop wiring (in-budget EXECUTE invokes executor, over-limit/no-grant/OFF leaves REQUEST_CONFIRMATION standing) (`adapter-core/tests/budget.test.ts`); in-memory `evalIncrCheck` primitive (`adapter-core/tests/persistence.test.ts`); budget burn recorded in the ledger without weakening first-writer-wins + replay-intact (`audit/tests/ledger.test.ts`); budget-capable declaration per pack (`pack-*/tests/conformance.test.ts`). The substitution is behind an additive deps slot (no slot ⇒ byte-identical legacy behavior); revert = stop asserting grants from the shell and drop the branch.

- 6121a7a: feat(core): 021 — capability schema + canonical pre-image + constant-time hash-bind verify. Add a new `Capability` record (`{ intentHash, kernelId, signature }`) whose `signature` slot (`{ keyId; alg; value }`) is shaped IDENTICALLY to `AuditRecord.signature` so audit and capability share one signature shape; plus `UnsignedCapability`/`CapabilitySignature` types and the `CAPABILITY_PREIMAGE_VERSION` (`"adjudicate-capability-v1"`) tag. `capabilityPreimage` builds a versioned canonical pre-image STRING (tag line + `sha256Canonical({intentHash, kernelId})` via `@adjudicate/canonical` — the NFC, invariant-#4-compatible encoder, NOT the conformance fork), binding the authorizing `intentHash` so a capability is non-detachable and non-replayable across intents (§D #4). `verifyCapability` is the PURE-JS, browser-safe, constant-time hash-bind check (re-derives the pre-image, compares with `timingSafeHexEqual` — never early-exit, never throws; fail-safe `false` on any malformed input). `bindCapability` mints the hash-bound (non-asymmetric) variant. ADDITIVE: no consumer is wired into the kernel — the cap-gated executor is plan 024. The pure `adjudicate()` decision path and `intentHashInput` are UNCHANGED (purity/determinism preserved; six outcomes intact).

  feat(approval-engine): 021 — node-resident ed25519 `signCapability` / `verifyCapabilitySignature` (impure shell, §D shell-signs boundary). Signs/verifies the SAME canonical `capabilityPreimage` string with `node:crypto` (alg `"ed25519"`, base64 detached signature in the shared signature slot), mirroring `createEd25519AttestationVerifier`'s node-only boundary. Fails CLOSED (never throws) on unknown key id / malformed key / non-ed25519 alg / bad or cross-intent / cross-kernel replay. Stays out of `@adjudicate/core` so core remains browser-bundleable.

  fix(canonical): add the `capability-preimage-body` cross-impl golden vector (`sha256Canonical({intentHash, kernelId})`) plus a 021 capability-pre-image lock test pinning the full versioned pre-image string and its hash; existing v3 envelope + resource-refs vectors are untouched.

  T5: `KernelIdentity.attest` stays a THROWING v0.2 seam — 021 does NOT unstub it. Capability minting/signing happens in the impure shell; the kernel never signs and merely records a `KernelIdentity.id` into the capability's `kernelId`. Documented in `identity.ts`; pinned by a test asserting `attest()` still rejects.

- c0b1b44: feat(core): 042 — contaminating session-flag model on the origin axis (consumes 041). Make untrusted ORIGIN contaminating at the session level so an LLM-proposed intent that follows retrieved/external content in context no longer re-enters the loop byte-identical to a user-induced proposal. Adds to `taint.ts`: `isContaminatingOrigin(origin)` (the pure predicate over the closed `Origin` union — `Retrieved`/`ExternalAPI` are contaminating; `Human`/`System`/`LLM` are not), the `SessionContamination` flag type (`{ taint; origin }`), `applySessionContamination(declaredTaint, flag)` (the monotonic lattice-meet fold — minted taint = `mergeTaint(declared, contamination.taint)`, never raises trust), and `contaminateSession(prior, datum)` (monotonic accumulation that only ever tightens; preserves the FIRST contaminating origin as the audit anchor). The pure kernel taint gate (`kernel/adjudicate.ts`) now reads `envelope.origin` READ-ONLY (already in the intentHash pre-image from 041) to ATTRIBUTE a sub-minimum `canPropose` refusal: a contaminating origin populates the previously-UNUSED `taint:propagation_violation` basis (instead of the bare `taint:level_insufficient`) so audit can distinguish a contamination-lowered refusal from a declared-untrusted one. This is NOT a 7th outcome (still REFUSE), NOT a new guard phase, NOT a friction change, and adds NO IO — kernel purity (§D), guard order #3 (taint short-circuits before auth), the closed 6-outcome algebra #2, the intentHash recipe #4, and monotonicity #7 are all preserved. The pre-existing 041 invariant `origin-not-gated.property.test.ts` is EVOLVED in lock-step (origin still never changes the Decision KIND; `propagation_violation` now appears ONLY on a taint REFUSE AND ONLY for a contaminating origin).

  feat(primitives): 042 — adopter-facing `createSessionContaminationPolicy({ enabled })` factory (DEFAULT OFF) + `SessionContaminationPolicy`/`SessionContaminationPolicyOptions` types, mirroring `createSystemTaintPolicy` so "is contamination enabled for this Pack?" is a one-line single-sourced audit. Default OFF keeps existing deployments byte-identical to pre-042.

  feat(adapter-core): 042 — fold the per-session contamination flag into the minted taint at the SINGLE envelope-minting seam (`loop.ts`), replacing the former unconditional `taint:"UNTRUSTED"` literal with the lattice meet of the declared taint and the session contamination taint, applied BEFORE `buildEnvelopeFromToolUse` hashes (so the contaminated taint/origin are inside the intentHash pre-image #4 — an LLM cannot post-hoc flip them). An authorized READ that SERVES a datum (the laundering leg) contaminates the session (treated as `Retrieved`); the next minted LLM intent then inherits the lowered taint and the contaminating origin stamp. `buildEnvelopeFromToolUse` (`bridge.ts`) threads an optional `contamination` arg via `applySessionContamination` (monotonic; idempotent under the loop's pre-meet); `routeReadThroughKernel` (`decisions.ts`) returns a `served` flag; `AdjudicatedAgentOptions.contamination` (`types.ts`) is the adopter opt-in (DEFAULT OFF — option omitted is byte-identical to pre-042). Clearing is structural: a fresh `runLoop` (including the authenticated `resume()` path) starts uncontaminated — never an LLM-controlled action.

  feat(red-team): 042 — land the `provenance_injection` (contamination / data-provenance) vector that 041 only opened the union seam for. New `vectors/provenance-injection.ts` generator: for each system-only intent kind, an UNTRUSTED envelope stamped with a CONTAMINATING origin (`Retrieved`/`ExternalAPI`), sourced from `planner.visibleReadTools` (the 041 declared-but-unconsumed seam — the READ→inject→intent path), expecting REFUSE. `ScenarioIntent` gains an optional, canonical-drop-safe `origin` (the runner threads it only when present, so existing vectors hash and decide byte-identically). Wired into `generateAllVectors`. Non-vacuity: against a pack whose state guards do not pre-empt the taint gate the kernel REFUSEs every contaminated proposal with `taint:propagation_violation`.

  feat(cli): 042 — wire `generateProvenanceInjectionEnvelopes` into the `adjudicate red-team` command's per-vector dispatch (the `provenance_injection` key already in `ALL_VECTORS` now produces real scenarios instead of zero).

- 86abd1a: feat(core): 051 — deterministic cumulative/velocity (rate-limit) guard family + fail-closed rate-limit rollback seam. Per index §C/§D the multi-horizon limit guard is a PURE business-layer predicate: it reads the IMMUTABLE aggregate/limit snapshot that 052 INJECTS into the one kernel decision (read-only `state`/deps) and, on breach, can only RAISE friction (REFUSE/ESCALATE/DEFER) — it never lowers a ceiling, never authorizes EXECUTE. 052 OWNS the aggregate-counting substrate (the `GuardFireStats` delta-write + the additive Postgres upsert + migration-006 PK arbiter); 051 CONSUMES it READ-ONLY and adds the velocity/cumulative GUARD family that reads the coalesced counts, plus hardens the load-bearing rate-limit rollback so a non-EXECUTE decision never poisons a legitimate user's counter.
  - **T1 (`core/kernel/rate-limit.ts`):** new `createCumulativeVelocityGuard(...)` — a synchronous, PURE multi-horizon guard. It reads the injected `AggregateSnapshot` (the 052 `windows` map keyed by an opaque `(resource, horizon)` string) via `resolveSnapshot`, projects this decision's contribution (`resolveIncrement`, default 1, clamped to ≥0 so a malformed resolver can never fabricate headroom), and FIRES when any configured horizon's `committed + increment > max` (the cap value itself is ALLOWED — strict greater-than, identical to `checkRateLimit`'s `count > max`). Deterministic precedence: horizons are evaluated in DECLARED array order (not snapshot key order), so the first-breaching window is replay-stable. Default `onExceeded` ⇒ REFUSE `cumulative_limit_exceeded`, basis `business/RULE_VIOLATED` (monotonicity §C). NO clock/RNG/IO/env — re-running it over the recorded snapshot reproduces a byte-identical decision (invariant #5). New exported types `VelocityHorizon`, `VelocityBreach`, `CumulativeVelocityGuardOptions`. The pre-existing `checkRateLimit`/`createRateLimitGuard` single-window semantics (`exceeded = count > args.max`, idempotent `rollback` closure, decrement-failure → `recordSinkFailure({ sink: "rate-limit" })`, OPTIONAL `decrement` no-op) are pinned unchanged.
  - **T2 (`core/kernel/adjudicate-and-audit.ts`):** harden the rollback `finally` seam — `deps.rateLimitRollback` runs for EVERY non-EXECUTE decision EVEN WHEN `sink.emit` throws (the throw path rethrows in `catch` after the `finally` fires; the success path returns normally). The guard reads `decision.kind !== 'EXECUTE' && deps.rateLimitRollback && !rewriteExecuted`, preserving the 011/T2 carve-out (a validated REWRITE that re-adjudicated to EXECUTE ran its bytes, so it does NOT roll back; a REWRITE that failed re-adjudication collapsed to REFUSE and rolls back like any non-EXECUTE). COEXISTS with the 013 kill-switch early-return rollback (its own try/finally), the 091 version-binding, the 033/052 snapshot recording, and the 011 REWRITE/ledger-release error path. §C/#6: a store/IO error on the write path aborts EXECUTE (the error propagates; the caller never receives a clean result hiding a failed audit write) and never fails OPEN.
  - **T3–T5 (read-only consumers, 052/053-owned substrate UNCHANGED):** 051 consumes 052's `core/kernel/guard-stats.ts` delta-write (`count:1`, anti-double-count — assert-6-not-9 regression kept) and `queryAsync` (store-direct, no memory union), the `audit-postgres/src/guard-stats-store.ts` additive `ON CONFLICT DO UPDATE SET count = count + EXCLUDED.count` + migration-006 PK arbiter, and re-affirms the `runtime/src/defer-park.ts` `INCR→EXPIRE→check→DECR` TOCTOU note + `evalIncrCheck` Lua seam as the canonical over-commit reference plan 053 inherits. None of those files are edited by 051.

  `intentHashInput`/`EXPECTED_ENVELOPE_KEYS`, the closed 6-outcome `Decision` algebra, and the pure `adjudicate()` decision path are UNCHANGED (purity/determinism/replay preserved; the aggregate snapshot rides injected state, never a hashed envelope field — invariant #4). New tests: the cumulative/velocity guard's boundary enforcement (under/at/over the cap), multi-horizon declared-order precedence, increment clamping, monotonicity (never EXECUTE), and replay-over-recorded-snapshot in `rate-limit.test.ts`; the guard wired end-to-end through `adjudicateAndAudit` (over-limit→REFUSE+rollback, under-limit→EXECUTE+no-rollback, exact boundary, and FAIL-CLOSED rollback-on-sink-throw for both over- and under-limit decisions) in `adjudicate-and-audit.test.ts`; and the exported guard surface locked in `api-surface.test.ts`. The live-PG additive-upsert integration gate (`pnpm -F @adjudicate/audit-postgres integration`) is the T4 durable exercise; it requires `PG_TEST_URL`/`DATABASE_URL` and is environment-gated.

- d2c3625: feat(core,conformance,adapter-core,primitives,cli): 082 — enforce the SIGNED pack at LOAD time (`installPack`). The adopter's in-process load path now REFUSES to install a Pack whose signature/trust or config seal does not verify, so a swapped/unsigned/tampered Pack cannot become the live adjudication authority (§D-1: only a verified Pack reaches the executor; §D-6: a write-path verification failure ABORTS the install; §C: failure → friction, never bypass). Fail-closed by default; behind the new `verifyOnLoad` option so an absent option is byte-identical to pre-082 (only `assertPackConformance` runs).
  - **T1 (`core/src/install.ts`):** add `VerifyOnLoadOptions` to `InstallPackOptions` and a FAIL-CLOSED provenance gate inside `installPack` that runs AFTER conformance but BEFORE any sink wiring / default install / snapshot recording, so a Pack that does not verify installs NOTHING destructive. The verifiers (`verifyPackTrust` / `verifyConfigSeal`) are INJECTED through `verifyOnLoad` — `@adjudicate/core` takes NO dependency on `@adjudicate/conformance` (which already depends on core; a `core → conformance` import would be a cycle, and the kernel dep allowlist stays clean: `@adjudicate/canonical, @noble/hashes, zod`). Defaults are STRICT at the load boundary: trust policy `require_signature` (NOT the library `best_effort`) and seal policy `require_signature` (NOT `require_digest`), so an UNSIGNED Pack (no signature / no publicKeyPem) refuses the install. A non-verifying report throws the new `PackLoadVerificationError` (axis: `trust` | `config_seal`). New exports: `VerifyOnLoadOptions`, `LoadTrustReport`, `LoadSealReport`, `PackFingerprintLike`, `PackLoadVerificationError` (all additive; recorded in the V1 freeze matrix). The pure `adjudicate()` path and `intentHashInput` are UNTOUCHED — this is impure install-shell wiring (§D).
  - **T2 (`conformance/src/index.ts`):** confirm + document that `verifyPackTrust` (`pack-trust.ts`) and `verifyConfigSeal` (`config-seal.ts`) are the single public verifiers the core load path injects; the pre-existing verifiers are unchanged.
  - **T3 (`adapter-core/src/types.ts`):** document the STRICT KNOB PAIRING on `AgentLoopOptions.configSeal` — operators must set `policy:"require_signature"` + `publicKeyPem` + `engageKillSwitchOnMismatch:true` together for fail-closed runtime posture (the same enforcement the load path runs by default). The runtime enforcement path (`loop.ts` `checkConfigSeal`) already honors this; documented, not silently relied upon (082 §7 risk: lax adapter default).
  - **T4 (`primitives/src/guards.ts`):** inline residual-blind-spot note at the `createRewriteGuard` code-artifact site — a clean seal proves SIGNATURE + sealed-surface provenance, NOT behavioral correctness of every closure (a state-derived `cap` pins the function source, not its runtime value), so load-time enforcement does not over-claim; closing the cap-pinning gap is 081's upstream scope.
  - **T5 (`cli/src/commands/pack-verify.ts`, `cli/src/bin.ts`):** align the `pack verify` command docs with the load-path posture — CI/adopters should run `--policy require_signature --public-key --signature` (+ `--expect-seal`) so the CLI gate and the runtime `installPack` load gate agree. The runtime `--policy` default stays `best_effort` for backwards-compatible local dev.
  - **T6 (tests):** `core/tests/install.test.ts` exercises the fail-closed gate with REAL ed25519 sign/verify (refuses unsigned / wrong-key / drifted-seal / unsigned-seal; installs a validly signed pack + matching signed seal; verifies no sinks install on failure; absent option ⇒ unchanged). `conformance/tests/pack-trust.test.ts` + `config-seal.test.ts` add the explicit `require_signature` load-path defaults (ed25519 + rsa-pss over the fingerprint; re-extract/re-hash of the LIVE pack), each with accept + fail-closed cases.

  Invariants preserved: kernel purity/determinism/replay (verification reads injected snapshots + the live pack surface only; no IO/clock/RNG; `intentHashInput` byte-identical), the closed 6-outcome `Decision` algebra (no new outcome), fail-closed (#6), and monotonicity (§C — an unverified Pack only ADDS friction by refusing to install).

- 0d83e43: feat(core,primitives,red-team,cli): 043 — origin-aware policy branch at the kernel taint gate + READ→inject→intent red-team vector (consumes 041/042). Close the laundering gap 042 cannot reach: an UNTRUSTED-min MUTATING intent whose trust-rank floor (`canPropose`, `1 >= 1`) ALWAYS passes regardless of where the bytes came from, so a `READ`→inject→intent path re-enters byte-identical to a user-induced intent and cleanly EXECUTEs. 043 adds the real per-intent propagation gate that actually FLIPS such a decision when the proposal traces to a contaminating origin, plus a 4th red-team vector that proves it catches what current packs honestly fail. Default-OFF / dark-ship: with no policy opt-in, behaviour is byte-identical to pre-043.
  - **`@adjudicate/core` (T2 `src/taint.ts`):** add the OPTIONAL `TaintPolicy.requiresUncontaminatedOrigin?(kind): boolean` method and the new `canProposeWithOrigin(taint, kind, origin, policy)` gate. The gate is MONOTONIC by construction: it first applies the unchanged trust-rank floor (`canPropose`) and NEVER authorizes a proposal the floor rejects; it only ADDS friction — when the floor passed but the policy marks the kind origin-required AND the origin is contaminating (`isContaminatingOrigin` — `Retrieved`/`ExternalAPI`), it returns `false`. A policy without the method (or returning `false`) falls straight through to the `canPropose` result, so every existing `{ minimumFor }` policy is unaffected. Pure: a function of `(taint, kind, origin, policy)` only — no clock/RNG/IO. Re-exported via the package barrel (`export * from "./taint.js"`).
  - **`@adjudicate/core` (T1 `src/kernel/adjudicate.ts`):** the single taint-gate call site now calls `canProposeWithOrigin` (alongside the rank-floor `canPropose`, used only to ATTRIBUTE the refusal). When the origin branch fires (rank floor would have PASSED but the proposal laundered its provenance), the refusal is attributed to the latent `taint:propagation_violation` basis with a distinct message and a `detail.branch === "origin_required"` marker; a `requiresUncontaminatedOrigin` that throws fails CLOSED as a taint-phase `kernel:guard_panic`. The 042 rank-floor attribution path (sub-minimum + contaminating origin → `propagation_violation` without the branch marker) is preserved exactly. Still a REFUSE (no 7th outcome), still one envelope-level call, still ahead of auth (guard order #3); `intentHash` reads only the already-bound `origin` + payload provenance — NO new field enters the hash pre-image (#4).
  - **`@adjudicate/core` (`src/basis-codes.ts`):** document the dual emission of the (already-present) `taint.PROPAGATION_VIOLATION` code — 042 ATTRIBUTION vs 043 ORIGIN-BRANCH (`detail.branch === "origin_required"`). No vocabulary/category change.
  - **`@adjudicate/primitives` (T3 `src/taint.ts`):** extend `createSystemTaintPolicy` with the OPT-IN `originRequiredKinds?: ReadonlyArray<string>` option. When at least one kind is declared, the returned policy ALSO carries `requiresUncontaminatedOrigin`; when absent/empty the method is OMITTED entirely so the policy shape is byte-identical to pre-043. `minimumFor` is unchanged for every kind (the trust-rank floor is untouched) — listing a kind can ONLY add friction.
  - **`@adjudicate/red-team` (T4 `src/scenario.ts` + `src/runner.ts`):** add the 4th `AttackVector` member `read_inject_intent` (additive/MINOR) and the matching `emptyByVector()` key so the runner's exhaustive Record stays total over the closed union.
  - **`@adjudicate/red-team` (T5 `src/vectors/taint-escalation.ts` + `src/vectors.ts` + `src/index.ts`):** add `generateReadInjectIntentEnvelopes` and register it in `generateAllVectors`. It targets ONLY UNTRUSTED-min kinds the pack MARKS origin-required (skips elevated-min kinds — those belong to the trust floor / the 042 provenance vector — and emits NOTHING for a pack that declares none, so it is never vacuous), stamps a contaminating origin + a READ-tool laundering source drawn from the previously-unconsumed `planner.visibleReadTools` seam (synthetic fallback otherwise), and expects REFUSE.
  - **`@adjudicate/cli` (T6 `src/commands/red-team.ts`):** surface the `read_inject_intent` vector through the existing `red-team` command (added to `ALL_VECTORS` + the per-vector dispatch) without changing its contract.

  Tests: kernel-gate origin-branch unit tests + a new invariant suite (`tests/kernel/invariants/origin-policy-branch.property.test.ts`) pinning MONOTONICITY (an origin-aware policy never EXECUTEs where its origin-blind twin REFUSEd), DARK-by-default byte-identity (a no-branch / disabled policy is identical to a plain `{ minimumFor }` policy), the `origin_required` branch-marker appearing only on a rank-PASS REFUSE for a contaminating origin, and replay byte-identity (#4); `canProposeWithOrigin` unit/property tests; policy-factory tests for `originRequiredKinds`; red-team generator NON-VACUITY (the kernel REFUSEs the laundered proposal via the 043 branch, basis `taint:propagation_violation`, NOT `level_insufficient`) + CONTROL tests (clean origin EXECUTEs; a pack without the branch lets it ESCAPE); CLI smoke tests (vector wired for PIX as a no-op; fires and is defended for an origin-required fixture pack). The pre-existing 041/042 `origin-not-gated.property.test.ts` invariant is UNCHANGED and still passes — its plain-policy fixtures never trip the opt-in 043 branch.

  Kernel purity (§D), the closed 6-outcome `Decision` algebra (#2), the guard order (#3, taint short-circuits before auth), `intentHashInput` (#4, no new field), and monotonicity (§C / #7) are all preserved. Rollback: contained to the listed packages on `feat/merged-043-origin-policy-redteam`; disable the policy option (or revert the branch) to restore the single rank-floor `canPropose` call and the 3-member `AttackVector` union with no residual schema/hash change.

- 44c46d2: feat(core,adapter-core,approval-engine,adjutant): 071 — bind the confirmation receipt to (intentHash, capability, approver, channel). Today the kernel's confirmation-receipt override fires on a BARE `intentHash` equality match gated only by `decision.kind === "REQUEST_CONFIRMATION"`; the receipt binds nothing about the capability authorized, the approver who confirmed, or the channel the confirmation arrived on. This widens the receipt so the post-confirmation EXECUTE is provably bound to the same (intentHash, capability, approver, channel) tuple the confirmation was resolved against, while keeping the override ADDITIVE and the kernel PURE. `intentHash` stays the LOAD-BEARING identity gate (§D-4 untouched — `intentHashInput` is NOT modified); the new fields are an ADDITIONAL fail-closed gate that defaults to friction on any mismatch (§D-6). The single-use/replay defense remains the adapter's `confirmationStore.take` + timing-safe hash compare; this plan strengthens WHAT the override trusts, not WHERE it is enforced. Omitting all binding fields yields a byte-identical `supersedes`/`auditHash` (§D-5 determinism fence). Closed 6-outcome algebra preserved (§D-2): the override still only maps `REQUEST_CONFIRMATION → EXECUTE`.
  - **`@adjudicate/core` (`src/kernel/adjudicate-and-audit.ts`, `src/kernel/index.ts`, `src/audit.ts`):** add the additive optional `confirmationReceipt.binding` field (`ConfirmationBinding` = `{ capability?, approver?, channel? }`, each a `ConfirmationBindingField` = `{ confirmed, requested? }`) (T1). The override predicate keeps `intentHash === envelope.intentHash` as the primary gate and additionally requires, via the new pure `confirmationBindingMatches`, that EVERY present field whose issued-against `requested` value is supplied equals its resolved `confirmed` value — a mismatch on ANY present field falls through to the original `REQUEST_CONFIRMATION` verdict (fail-closed). A field with no `requested` is recorded forensically but not gated; an absent `binding` is vacuously satisfied (unchanged back-compat path). The auto-derived `confirmation_resolved` `Supersession` now carries the BOUND (confirmed) tuple via the new pure `confirmationBindingRecord`, with sub-keys (and the whole `binding` key) omitted when unsupplied — so a confirmation with no binding produces a byte-identical supersession and auditHash (T2). Extend `Supersession` with the optional `binding?: { capability?, approver?, channel? }` carrier (IS in the auditHash pre-image, like the existing `token`). Export `ConfirmationBinding`, `ConfirmationBindingField`, `confirmationBindingMatches`, `confirmationBindingRecord` from `/kernel` (and the root barrel).
  - **`@adjudicate/adapter-core` (`src/types.ts`, `src/loop.ts`):** add the optional `ConfirmArgs.binding` input (T3) and forward it onto the kernel `confirmationReceipt.binding` in `confirm()` AFTER the existing single-use `take()` + timing-safe hash compare (T4). The loop still owns `intentHash`/`at`/`token` from the already-verified pending envelope; a caller (e.g. the approval-engine) supplies only the binding it holds. Conditionally spread so omitting `binding` is byte-identical to pre-071 `confirm()`.
  - **`@adjudicate/approval-engine` (`src/engine.ts`):** thread the resolving (approver, channel) into the forwarded receipt in `resolve()` (T5). Hoist the verified-approver computation ABOVE the `agent.confirm()` call so the bound approver is the CRYPTOGRAPHICALLY-VERIFIED `attestation.approverId` (when an attestation verifier is configured) — never the forgeable `input.by` claim. The channel is `existing.channel` (stamped at `request()` time), supplied as BOTH the issued-against `requested` and the resolved `confirmed` value, so a forwarded resolve cannot retroactively claim a different channel (a channel mismatch fails the kernel override closed). The binding is forwarded ONLY on an ACCEPTED resolve (a decline never overrides). The capability is not modeled in this single-approver path; the approver carries no `requested` value (the proposer/requestedBy surface that enables approver≠proposer separation-of-duty is plan 072 — 071 only RECORDS the bound approver).
  - **`@adjudicate/adjutant` (`src/orchestrator.ts`):** carry the resolving approver (`args.by.id`) and the single ops-plane channel (the new `ADJUTANT_CHANNEL` constant, stamped into the `ApprovalRequest` at proposal time) into the kernel `confirmationReceipt.binding` in `resolve()` (T6) instead of dropping them at `markResolved`. Each sub-field conditionally spread; an approve with no `args.by` is byte-identical to pre-071.

  Tests: kernel binding override (fires on full match; falls through fail-closed on a mismatched approver / channel / capability; a field with no `requested` is recorded-not-gated; `intentHash` stays load-bearing; byte-identical auditHash + supersedes when `binding` omitted/undefined) (`core/tests/kernel/confirmation-receipt.test.ts`); end-to-end loop forwarding (matching binding overrides + records confirmed tuple on `supersedes.binding`; mismatch fails closed to REQUEST_CONFIRMATION; omitted binding byte-identical) (`adapter-core/tests/confirm-binding.test.ts`); engine forwards (approver, channel) into `agent.confirm` on accept, none on decline, channel-only without an approver (`approval-engine/tests/engine.test.ts`); reference wiring threads the VERIFIED approver (not the forgeable `by`) + channel end-to-end (`approval-engine/tests/engine-reference-wiring.test.ts`); orchestrator binds (approver, channel) onto the EXECUTE supersession, channel-only without `by` (`adjutant/tests/orchestrator.test.ts`). Governance/quorum/attestation paths re-run with no regression (`approval-engine/tests/engine-governance.test.ts`). The change is additive behind the optional `binding` field (omit it ⇒ byte-identical pre-071 behavior); revert = drop the receipt fields and their wiring (T1–T6), leaving the four-field override keyed on `intentHash`.

- e81b801: feat(core): 023 — resource-binding verifier (`verifyResourceBinding`, `ResourceBindingPolicy`, `ResourceBindingResult`, `DEFAULT_RESOURCE_BINDING_POLICY`) in `envelope.ts`. Re-derives the envelope's `intentHash` via the UNTOUCHED `intentHashInput` recipe (`deriveIntentHash`) and constant-time-compares it against the carried hash with `timingSafeHexEqual` — the executor must honor ONLY the kernel-bound (signed) payload. A `payload` / `resourceRefs` (031) swapped AFTER the kernel decision re-derives a DIFFERENT hash and fail-closes (anti-IDOR / anti-resource-swap; invariants #1, #4, #6). The `intentHashInput`/`buildEnvelope`/`deriveIntentHash` bodies are BYTE-IDENTICAL (additive-only file change), so every existing envelope hash, golden vector, and replay corpus is unchanged (invariant #5). No `node:crypto`, no `Buffer` — core stays browser-bundleable (pure-JS canonical fence). The passive `AuditRecord.signature` slot stays PASSIVE — 023 is a hash fence only; the AuditSigner is plan 092. The bound envelope inputs are already recorded on the AuditRecord for replay.

  feat(adapter-core): 023 — enforce the resource binding at the executor seam (`runExecute`, `decisions.ts`) before `invokeIntent`, threaded via a new `resourceBindingPolicy` option (default `"strict"`). The check SUBSUMES the 011/T4 forged-REWRITE re-verify AND EXTENDS the same fence to the EXECUTE payload, so a post-decision resource-swap can never reach the executor (invariant #1). Coexists with 012 (reads serve via `invokeRead`, never reach this gate) and 013 (the kernel crossing that produced the Decision already emitted the required AuditRecord) — none weakened. `"warn"` still fail-closes a mismatch (friction never decreases, §C); `"off"` is the documented rollback dial restoring the exact pre-023 seam. Re-exports `verifyResourceBinding` from the barrel so the seam pins ONE recipe. The `AdopterExecutor.invokeIntent` contract now documents that it receives only the kernel-bound payload.

  feat(runtime): 023 — re-export `verifyResourceBinding` / `ResourceBindingPolicy` and a T4 cross-drift note pinning that the resource-binding pre-image equals the parked-envelope verifier's pre-image (`verifyParkedEnvelopeHash`) — the SAME canonical recipe + comparator, so the executor-seam binding and the resume-time park check cannot disagree (no drift; invariants #4/#5).

  feat(adjutant): 023 — `assertResourceBound` fence at the orchestrator's direct `invokeIntent` seam (it has no `runExecute`): re-derive + constant-time-compare the envelope's `intentHash` before the side effect in both `handle` (EXECUTE) and `resolve` (confirmation EXECUTE), so a swapped/forged proposal envelope fail-closes before the executor (anti-IDOR).

  feat(pack-\*): 023 — document the bound-payload contract on the three shipped packs' `capabilities.ts` (pix / incident-response / access-governance): an LLM-proposable intent reaches the adopter's executor ONLY through a binding-enforced seam, so the executor honors only the exact kernel-adjudicated `payload` / `resourceRefs`.

- 539337f: feat(core): 081 — pin per-guard CODE artifacts into the policy descriptor. Add `attachGuardCodeArtifact` / `readGuardCodeArtifact` / `GuardCodeArtifact` (a symbol-keyed slot carrying closure-captured numeric caps + predicate body) and surface a per-guard `codeDigest` (sha256-over-canonical via `@adjudicate/canonical`) on `GuardDescriptor` in `describePolicyBundle`. Additive + back-compatible: guards without an artifact carry no `codeDigest`. No new kernel dependency; the kernel decision is unchanged (purity/determinism preserved).

  feat(conformance): the ConfigSeal sealable surface now binds guard CODE, not just declared metadata. `SealableSurface` gains an order-stable `guardCodeDigests` list (new `GuardCodeDigest` type) threaded through `extractSealableSurface`; `computeConfigDigest` / `verifyConfigSeal` / `verifyConfigSealFrozen` signatures are unchanged. Closes Critique #27 / the 034→081 body-integrity dependency: editing a `createRewriteGuard` closure-captured cap (e.g. `AUTO_REMEDIATION_BLAST_CAP` 5 → 5000) now drives a digest mismatch instead of verifying clean (fail-closed, §D-inv-6).

  fix(primitives): `createRewriteGuard` exposes its closure-captured cap (and clamp body) to the descriptor via `attachGuardCodeArtifact`, so a behavior-changing cap edit is no longer invisible to the seal.

  feat(red-team): add `runConfigSealCapEditRegression` (+ `CapEditRegressionResult`) — a `config_integrity` regression that asserts a tampered guard cap is DETECTED by the sealed surface digest.

  feat(cli): `pack verify --expect-seal <hex>` verifies the extended ConfigSeal surface (guard code bodies pinned), in addition to the declarative-subset fingerprint.

  chore(adapter-core, admin-sdk): doc + wire-schema updates for the extended descriptor surface (the `configSeal` loop gate now binds guard code; `GuardDescriptorSchema` tolerates the optional `codeDigest`).

- 3f4bbbc: feat(core): 031 — v3 IntentEnvelope resource-refs (drop-safe hash binding). Add the OPTIONAL `resourceRefs` slot (new `ResourceRefs = Readonly<Record<string,string>>` type) to `IntentEnvelope` / `BuildEnvelopeInput`, threaded through `buildEnvelope`, and bound into the module-private `intentHashInput` pre-image so a present owner ref is tamper-evident (§D #4). CANONICAL-DROP-SAFE — exactly like `actor.attestation`: an envelope without resource-refs (or with the field explicitly `undefined`) omits the key from the canonical pre-image and hashes IDENTICALLY to its post-041 value (the replay-longevity corpus hash `dc624bd0…` is unchanged). `EXPECTED_ENVELOPE_KEYS`/`isIntentEnvelope` admit the new key without requiring it (nine required keys + one optional). No guard consults it in 031 — the authority predicate is plan 034; the kernel decision and determinism are unchanged.

  fix(canonical): add the v3 `envelope-with-resource-refs` cross-impl golden vector plus drop-safety tests; existing no-resource-refs vectors are untouched (the `envelope-hash-recipe` baseline `cd017dd3…` still pins the no-refs sibling).

  feat(admin-sdk): `IntentEnvelopeSchema` gains the optional `resourceRefs` field (new `ResourceRefsSchema = z.record(z.string(), z.string())`) with build-time core↔schema drift guards. Additive — old (no-refs) and new (with-refs) envelopes both round-trip.

  chore(audit-postgres): `legacyV1ToV2` threads stored `resourceRefs` through replay reconstruction; drop-safe for every v1/v2 row (omitted → byte-identical recomputed hash).

  feat(red-team): `ScenarioIntent` gains optional `resourceRefs`, threaded through the runner's `buildEnvelope`; `generateTaintEscalationEnvelopes` emits one v3-with-resource-refs probe per eligible kind asserting a declared owner does NOT weaken the taint short-circuit (still REFUSE).

  Docs: `intent-envelope-v2.schema.json`, `canonical-json-hash.md`, and `canonical-hash-vectors.json` updated to declare/pin the v3 field and its drop-safety.

### Patch Changes

- c0d1b93: feat(ci,ops): 083 — maker/checker/signer change-control + CI segregation-of-duties on the publish path, and promote the kernel-purity dep allowlist from a local-only script to a REQUIRED CI gate. This is a CI/OPS control only — zero kernel/package source changes; the pure `adjudicate()` path, `intentHashInput`, the closed 6-outcome `Decision` algebra, and `installPack`'s load surface are all UNTOUCHED. Every added gate can only INCREASE friction on the publish/merge path, never authorize a release the existing gates would refuse (§C monotonicity); a missing checker approval or a failing `rc:check` ABORTS publish, never publishes by default (§D constitutional invariant #6 fail-closed).
  - **T1 (`.github/CODEOWNERS` [new], `.github/workflows/release.yml`):** add CODEOWNERS four-eyes over the publish surface — a DISTINCT checker (the `@adjudicate/release-checkers` team, disjoint from the maker) must approve any edit to `.changeset/**`, `packages/*/package.json`, and the release/CI machinery; GitHub enforces this via "Require review from Code Owners" on `main`. Gate the publish (SIGNER) step behind a protected GitHub `release-signing` Environment so `NPM_TOKEN` is environment-scoped and the publish only runs after the environment's required-reviewer gate releases it. The changesets two-phase flow (`.changeset/config.json` `"commit": false`, `"baseBranch": "main"`) is the maker→checker→signer spine; provenance + SBOM attestation remain the artifact-signing evidence. NPM_TOKEN / CODEOWNERS are CI secrets (out-of-kernel; the gates never run inside `adjudicate()`).
  - **T2 (`.github/workflows/ci.yml`):** promote the `@adjudicate/core` kernel-dep allowlist (`scripts/rc-checks.ts` — `{@adjudicate/canonical, @noble/hashes, zod}`) to a REQUIRED CI gate by wiring `pnpm rc:check` into a SEPARATE `kernel-purity` job (083 §7: independently revertible). An unlisted `@adjudicate/core` dependency now fails every PR + push-to-main (§D kernel-purity boundary), instead of being enforced only if someone ran it locally.
  - **T3 (`scripts/check-freeze-matrix.ts`, `.github/workflows/release-candidate.yml`):** make the §24 immutable-version-pin demand ENFORCING on the segregated publish stage. New `--version-pin` mode structurally validates the §24 pin table (every `@adjudicate/*` pin row is well-formed `| name | version | stance |`, semver-valid, non-duplicated, table non-empty) and the RC pipeline runs it with NO `continue-on-error`. Deliberately STRUCTURAL, not value-equality against live `package.json`: the §24 pin values have drifted ahead across the merged-architecture history (tracked as the batched freeze-matrix sweep), so a value gate would redden the pipeline for accumulated cross-plan state outside this diff. The advisory symbol-completeness `--strict` step is unchanged. The §5 default (non-`--version-pin`) `pnpm check:freeze-matrix` still exits 0.
  - **T6 (tests, `packages/core/tests/install.test.ts`):** add a non-vacuous suite proving change-control is ORTHOGONAL to the kernel load path — `installPack` produces a byte-identical result with or without the full CI / publish-path environment (`CI`, `GITHUB_ACTIONS`, `NPM_TOKEN`, `GITHUB_TOKEN`), and `InstallPackOptions` carries NO change-control / CODEOWNERS / NPM_TOKEN option key (smuggled ambient keys are ignored). The seal/trust round-trip suites (`config-seal`, `config-seal-frozen`, `pack-trust`) re-run unchanged to confirm change-control is orthogonal to the seal.

- cb8d608: fix(core): u7 — type-check the load-bearing `@ts-expect-error` change-control directives in CI (H11). Test-fidelity only — NO runtime/`src` change; the pure `adjudicate()` path, closed 6-outcome `Decision` algebra, `state→taint→auth→business` guard order, and `intentHash`/`auditHash` recipe are UNTOUCHED.

  **H11 (MED) — the 083/084 orthogonality `@ts-expect-error` directives were inert AND stale.** `core/tsconfig.json` has `include: ["src"]` and the package `lint`/`build` only ever compiled `src`, so the load-bearing directives in `core/tests/install.test.ts` (the assertions that `InstallPackOptions` carries NO change-control / canary / rollout key — 083 §publish-segregation, 084 §staged-rollout) were never type-checked by CI. Worse, as written each smuggled key sat in a single object literal that was `as InstallPackOptions`-cast — and an `as` cast defeats excess-property checking, so every one of those `@ts-expect-error` directives was already STALE (would report `TS2578: Unused '@ts-expect-error' directive` the instant it was compiled). The directives suppressed nothing.

  FIX (test-fidelity):
  - **`core/tsconfig.test.json` (new):** extends `tsconfig.json`, `noEmit: true`, scoped to `src` + the change-control test file (`tests/install.test.ts`) so those directives are actually compiled. Scope is deliberately narrow — NOT the whole `tests/` tree, which contains many intentional negative-typing patterns (deliberately-invalid envelope versions, internal casts) that run fine under vitest but do not strictly type-check and are out of scope here.
  - **`core/package.json` `lint`:** now runs `tsc --noEmit && tsc -p tsconfig.test.json --noEmit && eslint "src/**/*.ts"` — the test-typecheck is a hard CI gate.
  - **`core/tests/install.test.ts`:** the 083 and 084 load-bearing assertions were restructured from one multi-key `as`-cast literal into per-key, separately-typed `InstallPackOptions` literals (one excess key each, so excess-property checking fires per key and each `@ts-expect-error` is genuinely load-bearing). The runtime defensive-ignore call (`installPack` ignores ambient props) is preserved.
  - **`@adjudicate/eslint-config`:** added `@typescript-eslint/ban-ts-comment` (`ts-expect-error: allow-with-description`, `ts-ignore`/`ts-nocheck` banned) so every directive must self-document its WHY; verified non-breaking across the whole monorepo lint (no `@ts-*` directive exists in any `src/`).

  Non-vacuity proven: pointing one load-bearing directive at a REAL `InstallPackOptions` key makes the new `tsc -p tsconfig.test.json --noEmit` fail with `TS2578` (exit 2); restoring returns it to green (exit 0). `@adjudicate/admin-sdk` was evaluated but its sole change-control directive (`trpc-router.test.ts`) shares a file with unrelated pre-existing tRPC-internal negative-typing casts, so a test-typecheck there is not straightforward and was left out of scope; it still benefits from the shared `ban-ts-comment` rule.

- 6e18f2c: docs(security,architecture): 121 — fix the docs-that-lie so the prose contracts match the as-built kernel (REWRITE, R2/policyVersion, E3/DEFER resume, the dangling §9.5 anchors, the stale ADR index).

  Six documentation passages asserted behaviors the code does not (or no longer) implements, plus two dangling cross-references and one stale ADR-index range. This is a documentation-correctness pass over existing files — NO kernel, executor, or audit code is touched, and every constitutional invariant (§C monotonicity, §D kernel-purity, the closed 6-outcome Decision algebra, the `state→taint→auth→business→default` guard order, the `intentHash` recipe) is preserved by construction. The §5 gates run the unchanged test suites to confirm the rewritten references no longer contradict a green tree.
  - **REWRITE (T1, `AI_CONTEXT.md`).** The flow-diagram line read "REWRITE → re-adjudicate the sanitized envelope". As-built today (plan 011 landed): the kernel re-runs the FULL guard order on the rewritten envelope (a single bounded second pass, intentHash re-derived fail-closed) and only flows the rewritten bytes to the executor on a second-pass EXECUTE; otherwise the second-pass decision stands and the rewrite never executes. Line rewritten to that two-stage truth (grounded in `packages/core/src/kernel/adjudicate-and-audit.ts` step 2b and `packages/adapter-core/src/decisions.ts`). Pinned by `adapter-core/tests/decisions.test.ts` (REWRITE → executor runs the rewritten bytes) — left UNCHANGED.
  - **R2 / pack drift at replay (T2, `docs/security/threat-model.md`).** R2, the cross-cutting replay-determinism note, and the mitigation matrix asserted `policyVersion` as an unconditional replay join key. As-built today (plan 091 landed): `buildAuditRecord` and BOTH `adjudicateAndAudit` call sites (kill-switch + main) thread `policyVersion` / `kernelVersion` onto the record ONLY when the host supplies `deps.policyVersion` / `deps.kernelVersion` (`packages/core/src/audit.ts` emits each field only when defined). Rewritten to state the binding is host-conditional, not unconditional; matrix status changed from "Mitigated" to "Mitigated when host supplies `policyVersion`".
  - **E3 / resume taint floor (T3, `docs/security/threat-model.md`).** E3 claimed a blanket "resume cannot upgrade effective taint without going through `canPropose()`". FALSE as a blanket: the DEFER `resume()` path builds a FRESH envelope with `actor.principal:"system"` / `taint:"TRUSTED"` (`packages/adapter-core/src/loop.ts`), an INTENTIONAL elevation, while the CONFIRMATION `confirm()` path and the approval-engine `resolve()` path (which routes into `confirm()`, `packages/approval-engine/src/engine.ts`) DO preserve the original taint. Rewritten to scope the guarantee to the taint-preserving paths and document the DEFER elevation explicitly (with the runtime `defer-resume.ts` constructing no envelope, and SoD controls tracked under ADR-143). Pinned by `adapter-core/tests/resume.test.ts` (resume yields `principal==='system'`, `taint==='TRUSTED'`, differing `intentHash`) — left UNCHANGED.
  - **Dangling `§9.5` anchors (T4/T5, `docs/security/threat-model.md` + `docs/security/security-review-checklist.md`).** Both cited a non-existent `docs/concepts.md §9.5`; the guard-ordering closed-enum invariant actually lives under `## 9` at the stable heading "Invariant to preserve through any refactor" (the `GuardPhase` closed enum). Both references re-pointed to bare §9 + the stable heading text (per §7 risk mitigation, not a numbered subsection). `grep -rn "§9.5" docs/` now returns zero.
  - **Stale ADR-index range (T6, `docs/architecture/decisions.md`).** The index line claimed the directory runs `ADR-101..ADR-136`; it actually runs `ADR-101..ADR-143` (highest `ADR-143-approval-engine-governance.md`). Range corrected; the §4 representative-ADR table (through ADR-116) is NOT a lie and was left untouched.

- 580fc68: fix(core): u1 — FAIL-CLOSED the impure audit-shell tail so a synchronous signer throw never orphans the ledger / skips the rate-limit rollback, and refuse a config seal supplied without a verifier.

  Three fail-closed defects in `@adjudicate/core` (impure shell only — the pure `adjudicate()` path, the closed 6-outcome `Decision` algebra, the `state→taint→auth→business` guard order, and the `intentHash`/`auditHash` recipe are UNTOUCHED; this is a §C-monotonic, friction-only hardening that removes a fail-OPEN tail, never weakens a decision):
  - **H16 (HIGH) + H15 (MED) — `core/src/kernel/adjudicate-and-audit.ts`:** the durable `record = applyMeta(buildAuditRecord({...signer...}))` was constructed BEFORE the audit-emit `try` on BOTH the main path and the kill-switch path. A SYNCHRONOUS signer throw (`audit.ts` `signer.sign(auditHash)`) therefore escaped the call BEFORE entering the `try`, bypassing BOTH the catch-block ledger release and the finally-block rate-limit rollback. Consequences: an EXECUTE dedup key already claimed by `recordExecution` was ORPHANED for the full TTL — every legitimate retry of the same envelope then hit `REPLAY_SUPPRESSED` (a self-inflicted denial); and for a non-EXECUTE decision the rate-limit counter stayed poisoned (a maintenance/kill-switch window charging budget against users it refused). FIX: the `record` construction is relocated INSIDE the audit-emit `try` on both paths (`let record: AuditRecord` is hoisted so the success-path return still sees it), so a signer throw now lands in the existing cleanup tail — the catch releases the claimed key (still gated on `ledgerAcquired`, so the race-loser/`exists` path is unaffected) and the finally runs the rollback. The record CONTENT is byte-identical (only WHERE it is built moved), so `intentHash`/`auditHash`/determinism are unaffected and `test:invariants` stays green. The signer-FAIL-CLOSED doc comment that wrongly claimed the rollback "still runs" pre-fix is corrected.
  - **H8 (LOW) — `core/src/install.ts`:** the config-seal enforcement branch required BOTH `v.seal !== undefined` AND `v.verifyConfigSeal !== undefined`, with no else — so supplying a `seal` while OMITTING the verifier fell through to a SUCCESSFUL install with the seal silently UNENFORCED (fail-open). FIX: a fail-closed guard now runs BEFORE the enforcement branch — `seal` present without a verifier throws `PackLoadVerificationError(packId, 'config_seal', ['seal supplied but no verifier injected — refusing to install fail-closed'])`. The seal-ABSENT "trust only" path is documented by-design and is unchanged.

  New regression tests (each proven NON-VACUOUS — FAILS on the pre-fix source): a throwing signer with an acquired EXECUTE ledger key ⇒ `ledger.release` called exactly once, and a throwing signer on a non-EXECUTE decision ⇒ `rateLimitRollback` fired (both in `core/tests/kernel/adjudicate-and-audit.test.ts`); a `seal` supplied with `verifyConfigSeal` omitted ⇒ `installPack` throws `PackLoadVerificationError` axis `config_seal` (in `core/tests/install.test.ts`).

- 7832b4c: docs(architecture,security): 122 — ADR scaffold + index, Status backfill, ADR-144, SECURITY.md reconciliation.

  Documentation-only Layer-12 plan that finishes the doc-truth pass plan 121 began. NO kernel, executor, or audit code path is touched; every constitutional invariant (closed 6-outcome Decision algebra, `state→taint→auth→business→default` guard order, §C monotonicity, fail-closed default, kernel purity, the ADR-104 `intentHash` recipe) is preserved by construction. The §5 gates run the unchanged suites that PIN the documented behavior (`decisions.test.ts`, `resume.test.ts`, `guard-order.test.ts`) so the prose cannot silently outlive the code it describes.
  - **ADR scaffold (T6, `docs/architecture/adr/README.md`).** The directory previously had no template / README / index (grep `template|readme|0000|index` returned zero). Added a README carrying the purpose, numbering rules, the canonical `ADR-143` header template (`# ADR-NNN — <title>` + `Status`/`Date`/`Scope`/`Supersedes`/`Related` bullets + `## Context`/`## Decision`/`## Why this shape`), the constitutional-invariant guardrails an ADR may not contradict, and the authoritative full index (ADR-101..ADR-144, all Accepted).
  - **Status-line backfill (T6).** Normalized the 9 ADRs whose `Status` deviated from the de-facto `ADR-143` bullet shape — ADR-105..ADR-112 (were `**Status**: Accepted (date)`) and ADR-116 (was a `## Status` heading) — to the canonical `- **Status:** … / - **Date:** … / - **Related:** …` block, preserving each ADR's existing status value, date, supersedes, and related links verbatim (the M1/M2/M3 execution notes folded into the Date bullet; ADR-116 carried no explicit date so it states the v1.0-RC milestone honestly).
  - **ADR-144 (T6, new, `docs/architecture/adr/ADR-144-doc-truth-reconciliation.md`).** New Accepted ADR recording the documentation-as-truth reconciliation discipline that plans 121/122 established: docs follow code, anchored to `file:line` citations, gated by the suites that pin the documented behavior; the six concrete drifts that were corrected (REWRITE re-adjudication, R2/`policyVersion` host-conditional binding, E3/DEFER-resume taint elevation, the dangling §9.5 anchors, the stale ADR range, the missing scaffold) are catalogued with their code anchors. Prose-only; preserves all invariants.
  - **ADR index range (T5, `docs/architecture/decisions.md`).** The §4 authoritative-range line, corrected by 121 to ADR-101..ADR-143, is advanced to ADR-101..ADR-144 (new highest `ADR-144-doc-truth-reconciliation.md`); a pointer to `adr/README.md` and rows for ADR-143/ADR-144 added to the representative table. The "ADR-101..ADR-136" stale range remains absent.
  - **SECURITY.md reconciliation (T6).** The coarse "In scope" list is reconciled with the as-built threat model: added the monotonicity/fail-closed ceiling, the taint-short-circuit guard order, the `auditHash` chain + host-conditional `policyVersion`/`kernelVersion` binding (matching threat-model R2), and the authority-guard IDOR caveat (real closure needs a host-injected authenticated principal), with pointers to `docs/security/threat-model.md`, `decisions.md §5`, and the ADR index. No overstated guarantee.

- e9cc367: feat(approval-engine,core,adjutant): 073 — out-of-band confirmation channels + reference channel + accessibility, hardened and proven kernel-pure. Deliver out-of-band approval delivery over real transports (Slack, Teams, email, webhook) plus a zero-dependency `console-log` reference channel, with adopter-rendered approve/decline deep links and the out-of-band `escalation` projection scheduler input — all as pure I/O strictly OUTSIDE the kernel (§D). Channels surface `REQUEST_CONFIRMATION` and carry display facts only; they never authorize, weaken, or get read by the kernel. The `ApprovalChannel` seam (built in 071/072) is unchanged in surface; 073 HARDENS the accessibility-rendering + fallback-routing paths and PROVES the kernel-purity boundary by test. No production API was added or changed — the deliverable is additive test/conformance coverage plus the documented assertions that the kernel override remains the sole authority over the receipt-gated EXECUTE.
  - **`@adjudicate/approval-engine` (T1 `src/channel.ts`):** the four production transports (webhook/slack/teams/email) and the `console-log` reference channel are pure I/O with INJECTABLE transports (`fetchImpl`/`send`); with none injected the channel is declarative and returns `{}` (byte-identical to pre-change → trivial rollback, §7). `summarize()` renders the prompt plus the adopter-built `approveUrl`/`declineUrl` deep links into the Slack/Teams/email message body, and the webhook channel forwards the links via the POSTed context. Tests cover: slack/teams/email/webhook transports POST/send the expected payloads and return a `channelRef`; non-ok responses throw; declarative `{}` when no transport injected; deep-link rendering through `summarize()` (slack/teams/email) AND the webhook body; no link line when neither URL is present; and the `console-log` reference channel recording each delivery + its safe-no-op `notifyResolved` (`tests/channels.test.ts`).
  - **`@adjudicate/approval-engine` (T2 `src/engine.ts`):** confirm + lock in the deterministic multi-modal fallback — `request()` tries `targetIds` in declared order, the FIRST successful channel wins (`// first successful channel wins`), a failed channel falls through to the next, and if EVERY channel fails the engine still records the display projection before surfacing `CHANNEL_FAILED`. `notifyResolved` fires on the resolving channel at resolve time with the outcome + approver. New tests: first-successful-wins, fallback-to-next-on-failure, stop-at-first-success (later channel never tried), projection-recorded-when-all-fail, `route()` selection, deep-link threading into the channel context, and `notifyResolved` invoked on both approve and decline (`tests/engine.test.ts`).
  - **`@adjudicate/approval-engine` (T3 `src/registry.ts` + `src/registry-redis.ts`):** confirm the out-of-band `escalation: { afterMs, to: "human" | "supervisor" }` projection and the `channel`/`channelRef` display fields round-trip through `put`/`get`/`list` in BOTH the in-memory and Redis registries (the Redis projection serializes the whole `ApprovalRequest`, so `escalation`/`channelRef` round-trip automatically). `isEscalationDue` consumes the projection for an adopter-driven re-notify/escalate scheduler — NEVER read by the kernel. New tests: escalation + channel/channelRef round-trips (`tests/registry.test.ts`, `tests/registry-redis.test.ts`).
  - **`@adjudicate/approval-engine` (T4 `src/index.ts`):** the channel/registry/escalation surface (`createWebhookChannel`/`createSlackChannel`/`createTeamsChannel`/`createEmailChannel`/`createConsoleLogChannel`, `ApprovalChannel`/`ApprovalChannelContext`, the `ApprovalRequest.escalation` projection, `isEscalationDue`) is re-exported through the package barrel — verified by the new tests importing them from `../src/index.js`.
  - **`@adjudicate/approval-engine` (`tests/engine-reference-wiring.test.ts`):** end-to-end reference wiring channel → registry → resolve: deliver to the `console-log` channel, record the projection (carrying `escalation`), poll `isEscalationDue` (not-due-then-due), resolve → `agent.confirm()` runs once → projection flips to `approved` → channel re-notified; once resolved, escalation no longer fires.
  - **`@adjudicate/core` (T5 `tests/kernel/confirmation-receipt.test.ts`):** ASSERT (by test, not by absence of the word "channel") that the kernel override NEVER treats a channel as a standalone authority input. The override's load-bearing identity gate stays `intentHash`; 071's optional `channel` binding participates ONLY in the equality/audit gate. New tests prove: a perfectly-matching `channel` binding does NOT authorize when the `intentHash` is wrong (channel is not authority); changing ONLY the bound channel does not change WHETHER the override fires (same intentHash → same EXECUTE, channel recorded forensically/distinctly); and a receipt whose only distinguishing fact is the channel (non-matching intentHash) cannot mint EXECUTE (fail-closed friction, §D-6 / constitutional invariant #1). `adjudicate-and-audit.ts` is UNCHANGED — the binding remains the pure `confirmationBindingMatches` equality gate added by 071.
  - **T6 (`adapter-core/src/loop.ts`):** ASSERTED-NO-EDIT — the upstream single-use/tamper defense in `confirm()` (`confirmationStore.take()` get-and-delete + timing-safe re-derived-hash compare, enforced BEFORE the kernel) is structurally independent of the channel layer (which lives ABOVE adapter-core in approval-engine) and is byte-unchanged; the adapter-core suite (incl. `tests/confirm-binding.test.ts`) stays green.
  - **`@adjudicate/adjutant` (T7 `tests/resolve.test.ts`):** confirm the ops-plane `resolve` records the approver identity ONLY via `markResolved` (the registry display projection's `resolvedBy`), never as a kernel authority input — the kernel override authorizes on `intentHash`, and the 071 `{approver, channel}` binding is an equality/audit gate only; the proposer is never on the receipt. `orchestrator.ts` is UNCHANGED. New test: the approving operator's identity is stamped onto the projection via `markResolved`.

  The pure `adjudicate()` decision path, the closed 6-outcome `Decision` algebra (no 7th outcome, no `confidence`/`metadata`, invariant #2), the guard order, and `intentHashInput` (invariant #4) are UNCHANGED. Monotonicity (§C) holds: channels and the escalation scheduler only ADD friction (deliver, re-notify, escalate `human`/`supervisor`); they never downgrade a decision and never reach the kernel's EXECUTE path. Constitutional invariant #1 holds: only `EXECUTE` — minted by the kernel after `confirm()`'s upstream `take` + hash check — reaches the executor. Rollback: every channel behavior is additive and gated behind injectable transports (declarative no-op when none injected); revert the branch and behavior is byte-identical to pre-change.

- 79f47fe: feat(audit-postgres): 053 — durable, transactional reservation store with a single-statement over-commit guard, so a multi-horizon cumulative/velocity cap can be decremented (claimed) under concurrency WITHOUT over-commit. Per index §B/§D the reservation read/write is store IO that lives ONLY in the impure shell AFTER the pure kernel decision — it never enters `adjudicate()`. The reservation EXTENDS the durable additive guard-stats upsert template (NOT the ephemeral park `INCR→EXPIRE→check→DECR` counter, which has a documented TOCTOU over-commit race); over-cap fails CLOSED (§C monotonicity: a decrement may only RAISE friction, never silently over-commit) and a store/IO error on the write path aborts EXECUTE (§D-#6, it propagates rather than failing open). The pure kernel is UNTOUCHED; the rollback + EXECUTE-race-dedup seams are REUSED, not forked.
  - **`@adjudicate/audit-postgres` (`src/guard-stats-store.ts`) — the reservation store (T1/T2):** add `RESERVE_GUARD_STAT_SQL` and `createPostgresReservationStore`. The SQL extends the additive `ON CONFLICT (guard_name, guard_phase, decision_kind, day, pack_id) DO UPDATE SET count = audit_guard_stats.count + EXCLUDED.count` template (same migration-006 PK arbiter — NO new migration) with TWO cap gates so over-commit fails closed in ONE statement: a fresh-key `SELECT $delta WHERE $delta <= $cap` source gate AND a conflict-path `WHERE table.count + EXCLUDED.count <= $cap` predicate on the `DO UPDATE`. An over-cap claim affects ZERO rows (`rowCount === 0` ⇒ REFUSE); a positive count ⇒ the units were reserved atomically. There is NO read-modify-write window — concurrent over-cap claims cannot both win (one updates/inserts, the other's `WHERE` matches zero rows). `reserve` also refuses a non-positive / non-finite delta LOCALLY (it would fabricate headroom, §C) and coerces the no-pack case to the 052 `''` PK sentinel (a NULL would 23502 or split the additive arbiter). `$delta`/`$cap` are cast to `bigint` so Postgres deduces a single consistent parameter type. The `ON CONFLICT` arbiter MUST be a real `UNIQUE`/`PK` exercised against a live DB (the migration-006 `42P10` lesson) — proven by the §6 integration test, not just an asserted SQL string.
  - **`@adjudicate/audit-postgres` (`src/pg-types.ts`, `src/index.ts`) — aligned row types + barrel (T2):** add `coerceBigIntCount` (string | number | bigint → safe-integer `number`, loud on precision loss) and route the shared `audit_guard_stats.count BIGINT` read-back through it from the guard-stats reader, so the reservation store and the guard-stats counter agree on the column shape. Surface `RESERVE_GUARD_STAT_SQL`, `createPostgresReservationStore`, `ReservationKey`, `ReservationOutcome`, `ReservationWriter`, and `CreatePostgresReservationStoreDeps` through the package barrel.
  - **`@adjudicate/runtime` (`src/defer-park.ts`) — durable-vs-ephemeral documentation (T6):** update the over-commit-race doc block to record that 053 DELIVERED the durable answer on the additive `ON CONFLICT` template (`RESERVE_GUARD_STAT_SQL`), contrasting it with this module's EPHEMERAL `INCR→EXPIRE→check→DECR` (Lua-`evalIncrCheck`-seamed) park counter; copying the park sequence into the durable reservation would re-introduce the over-commit race against the authoritative limit — 053 deliberately did not.
  - **`@adjudicate/core` — rollback + dedup wiring REUSED, not forked (T3/T4; tests only):** the `RateLimitResult.rollback` idempotent closure (`kernel/rate-limit.ts`), the `:616-631` non-EXECUTE rollback `finally` and the SET-NX EXECUTE-race dedup (`kernel/adjudicate-and-audit.ts`) are the existing 051/092 seams a refused reservation rides — no source change. Added `rate-limit.test.ts` assertions: a `decrement` FAILURE routes to `recordSinkFailure({ sink: "rate-limit" })` WITHOUT throwing, and that path stays idempotent. The pure kernel (`kernel/adjudicate.ts`) is byte-unchanged (replay-determinism + `test:invariants` green; ZERO `Date.now|Math.random|new Date|process.env` hits).
  - **`@adjudicate/audit` — ledger contract kept intact (T5; tests only):** `ledger.ts` / `ledger-redis.ts` (best-effort `DEL` release, 14-day default TTL) are unchanged so reservation claims do not orphan. Added `ledger.test.ts` assertions: `recordExecution` is first-writer-wins (`'acquired'` then `'exists'` for the same intentHash), `release` (when the client exposes `del`) clears an orphaned key so a retry re-acquires (namespaced key), and `release` is ABSENT when the client cannot DEL (the kernel takes its orphan-telemetry branch).

  §6 live-DB concurrency test (`audit-postgres/tests/integration.test.ts`, gated by `pnpm -F @adjudicate/audit-postgres integration`): exercises `RESERVE_GUARD_STAT_SQL` against the real migration-006 PK arbiter (no `42P10`), proving two concurrent over-cap decrements do not over-commit (one wins, one refuses), 200 concurrent single-unit claims converge on EXACTLY the cap, the fresh-key over-cap first claim inserts zero rows, and distinct packIds key independent caps. Validated against a live Postgres (docker `ibatexas` stack, migrations 001–010 applied): 18/18 integration tests pass.

  Rollback: `RateLimitStore.decrement` and `Ledger.release` are OPTIONAL and the change is additive + worktree-isolated on `feat/merged-053-reservation-store`; dropping the wiring degrades rollback to a no-op without changing the pure decision. Revert the branch to restore prior behavior.

- 1978f2b: feat(red-team,adapter-core,cli,ci): 084 — staged rollout (shadow → canary → auto-rollback) + a frozen adversarial-canary gate. Turns the `@adjudicate/red-team` suite (which previously ran only as part of `pnpm test`, wired to zero workflows) into an explicit DETERMINISTIC publish/rollout gate, tightens the canary-stage config-seal knobs to fail-closed, and wires the canary as an explicit CI + publish-precondition gate over the 6 shipped pack dist bundles. All orchestration lives in the impure shell (red-team / adapter / CLI / CI), never inside `adjudicate()` — the pure kernel, `intentHashInput` (invariant #4), the closed 6-outcome `Decision` algebra, and `installPack`'s load surface are all UNTOUCHED. Every added gate can only INCREASE friction (§C monotonicity); a failed canary ABORTS promotion (§D-6 fail-closed), never promotes by default.
  - **T1 (`red-team/src/runner.ts`):** add `runCanaryGate(pack, { stage, policy, ...gen })` — a frozen adversarial-canary gate that reuses `runRedTeam` + `computeRedTeamExitCode` (0 = promote / 2 = rollback) over a FROZEN scenario set (`frozenCanaryScenarios` = `generateAllVectors` PLUS the 035/T10 ownership/IDOR vector `generateOwnershipViolationEnvelopes`, which `generateAllVectors` omits — closing the ownership-axis canary gap so the gate protects §D #8). Under `policy:"strict"` (default) it PROMOTES the `taintEscalationCausality` non-vacuity warning into a HARD FAIL: a `escaped===0` taint pass where the taint gate was never exercised (`byTaintGate===0`, all defenses fired upstream) is a vacuous guarantee → rollback. The new `policy:"execute-escape"` is the §D-1 privilege-escalation gate: it rolls back ONLY on a reached clean `EXECUTE` (the executor) or an error, treating vacuity / non-EXECUTE friction (DEFER/etc.) as advisory — for a heterogeneous catalog whose adversarial scenarios are legitimately defended upstream of the taint gate. Both policies are fail-closed on the real escape and on errors; `execute-escape` only relaxes the advisory axes. Pure: no clock/RNG/IO; deterministic over `(pack, seed)`. Also adds `runBaselinedCanaryGate(pack, baseline, { seed })` + `deriveCanaryBaseline(result)` (and the `CanaryBaseline` type): the FULL STRICT canary measured against a committed baseline — the function CI/release wire (see T6). It rolls back on any new escape/error/IDOR-escape/vacuity beyond the baseline counts AND on any per-scenario §C friction regression (a recorded decision moving to a strictly less-restrictive `kind` via `restrictivenessRank`), with a reached `EXECUTE` / error unconditionally non-baselineable.
  - **T2 (`red-team/src/history.ts`):** add `runStagedCanaryRollout(pack, { store, candidate?, shadowAt, canaryAt, policy? })` — runs the SHADOW stage over the trusted baseline `pack` and the CANARY stage over the `candidate` (defaults to `pack`), persists BOTH stage reports through the existing in-memory history seam (`record(report, at)` — the same surface used for trend charting), and flips to ROLLBACK (exit 2) on any stage failure OR a shadow→canary DELTA regression (more escapes/errors, an IDOR hole newly opened, or taint coverage newly collapsed to vacuous). Friction-only: the rollout exit is never lower than the worst stage verdict. Caller-supplied timestamps (no clock).
  - **T3 (`cli/src/commands/red-team.ts`, `cli/src/bin.ts`):** extend the `red-team` subcommand with `--canary` (run the frozen gate, exit 2 = ROLLBACK / 0 = PROMOTE; ignores `--vectors`) and `--canary-policy <strict|execute-escape>`. Invocable locally and from CI/release.
  - **T4 (`adapter-core/src/types.ts`, `adapter-core/src/index.ts`):** add `canaryStageConfigSeal({ seal, publicKeyPem, onDrift? })` — builds the FAIL-CLOSED canary-stage seal posture (`policy:"require_signature"` + `engageKillSwitchOnMismatch:true` + `reverify:"every_turn"`) so a seal drift during canary LATCHES the kill switch instead of self-healing the next turn (§C/§D-7 monotonicity — the rollout may only add friction). Extract the previously-inline `configSeal` shape into the named exported `AgentConfigSealOptions` type the helper returns. Scoped to the canary stage only, leaving the documented one-release lax default intact for normal turns (082 deprecation window).
  - **T5 (`core/tests/install.test.ts`):** assert the kernel install path stays orchestration-free — `InstallPackOptions` carries NO canary/rollout/red-team key, a candidate installs byte-identically regardless of ambient canary state, and the 082 seal/trust verifiers remain caller-INJECTED (core never imports `@adjudicate/conformance`). `installPack` is UNCHANGED by this plan; the canary runs AROUND install via the red-team shell.
  - **T6 (`.github/workflows/ci.yml`, `.github/workflows/release.yml`, `.github/canary-baselines/*.json`):** wire the adversarial canary as an explicit gate alongside the ADR-140 composition gate and as a PUBLISH PRECONDITION (before the SIGNER/publish step). The wired gate runs the FULL STRICT canary (`red-team --baseline <committed-baseline> --seed 1`, via the new `runBaselinedCanaryGate`) over each shipped pack dist bundle, measured against a COMMITTED, version-controlled baseline (`.github/canary-baselines/<packId>.json` produced by `deriveCanaryBaseline` from a strict run). The gate PROMOTES iff the run is no-worse-than-baseline and ROLLS BACK (exit 2) on ANY new escape/error/IDOR-escape/vacuity beyond the baseline OR any §C friction REGRESSION on a baselined scenario (a recorded decision weakening to a strictly less-restrictive kind, e.g. a money-mover's IDOR `REFUSE → DEFER`). The committed baseline FREEZES the documented pre-existing 035-F1 #8 gaps (pack-identity-kyc's forged-owner DEFER cases; cli/pix/deploy's taint defended-upstream/vacuous cases) so CI/publish does not go permanently red on KNOWN holes, while gating REGRESSIONS — the property the weaker `execute-escape` policy could not deliver (it only catches a reached `EXECUTE`, so it promoted kyc's 12 open non-EXECUTE IDOR DEFERs and any non-EXECUTE friction-lowering). A change to a pack's defended posture therefore requires a REVIEWED baseline update in the same PR. `--canary-policy execute-escape` remains available on the CLI for ad-hoc local inspection but is NOT the CI/publish gate.
  - **Tests:** `red-team` adds a canary-gate suite (frozen-set ownership coverage, vacuous-taint HARD FAIL, EXECUTE-escape rollback, determinism, the strict-vs-execute-escape contrast, never-promote-fail-open), a baseline-gate suite (PROMOTE on a matching baseline, DOCUMENT a pre-existing 035-F1 DEFER gap without reddening, ROLLBACK a NEW non-EXECUTE IDOR escape beyond baseline [finding 1], ROLLBACK a §C `REFUSE → DEFER` friction regression with NO EXECUTE reached [finding 2], a reached EXECUTE can never be baselined away, `deriveCanaryBaseline` round-trip), and a staged-rollout suite (clean PROMOTE + idempotent persistence, stage-failure rollback, shadow→canary delta-regression rollback). `adapter-core` adds the canary-stage seal suite (strict knobs forced, valid seal proceeds, drift LATCHES + does NOT self-heal, contrasted against the lax default that self-heals). `conformance` extends `config-seal.test.ts` (frozen-cadence `verifyConfigSealFrozen` under require_signature gates a clean digest + is fail-closed on unsigned/drift) and `pack-trust.test.ts` (`verifyPackTrust` under require_signature as the canary-stage trust precondition).

- Updated dependencies [5a261ef]
- Updated dependencies [6121a7a]
- Updated dependencies [6e18f2c]
- Updated dependencies [7832b4c]
- Updated dependencies [3f4bbbc]
  - @adjudicate/canonical@1.2.0

## 1.4.0

### Minor Changes

- 93d5cda: Add the shared `LearningEventV1` wire contract to the kernel barrel
  (`LearningEventV1` + `LEARNING_EVENT_SCHEMA_VERSION` / `LEARNING_EVENT_CHANNEL` /
  `LEARNING_EVENT_SUBJECT`). This single-sources the `learning.event.v1` pub/sub
  payload that the ibatexas runtime publishes and the adjudicate console consumes,
  killing the hand-copied-interface drift flagged by UltraReview (#94-20 / #28-11).
  Additive only — the existing kernel `LearningEvent` type is unchanged.

## 1.3.0

### Minor Changes

- 570db36: feat(core): AuditRecord v5 adds optional `metadata` (EXCLUDED from auditHash) + `attachAuditMetadata` + an `adjudicateAndAudit({ metadataProvider })` seam (ADR-124).

  feat(observability): hallucination scoring — `createHallucinationMetadataProvider` + `bucketHallucinationScore` + `adjudicate.hallucination.score`/`.bucket` semconv attributes.

  fix(admin-sdk,audit-postgres): accept AuditRecord v5 (schema + row mapping).

- 464db38: feat(primitives): add `createDataClassificationGuard` (PII/PHI redaction & refusal). REWRITE masks matched payload fields (taint preserved); REFUSE blocks. Runtime sensitivity tier + redacted fields ride in `DecisionBasis.detail`.

  feat(core): widen `GuardDescription` with the additive `data_classification` variant; add `validation.PII_DETECTED/PII_REDACTED/PII_BLOCKED` basis codes (ADR-117).

  feat(analyze): AJD-104 also flags a `data_classification` REWRITE guard with empty `scannedFields`.

  feat(admin-sdk): add `governance.piiClassificationStats` — aggregates data-classification dispositions by (sensitivityLevel × disposition) for the console.

### Patch Changes

- fdc0344: Adversarial-audit remediation (464db38→804af8f review):
  - **audit-postgres (release-blocker):** migration `010-add-v5-metadata.sql` widens
    the `record_version` CHECK to `IN (1,2,3,4,5)` and adds the nullable
    `metadata_jsonb` column. Core stamps `record_version=5` unconditionally, so
    against a DB migrated through 009 every audit insert previously failed Postgres 23514. The sink now persists and recovers `metadata` losslessly.
  - **primitives:** `createTokenBudgetGuard` now fails **closed** on a non-finite
    over-budget meter — `+Infinity` ≥ any budget crosses (REFUSE) instead of
    passing through. NaN/negative remain non-crossing.
  - **conformance:** `generateAiBom` array comparators are now total-order (equal
    keys → 0), so the `bomDigest` is reproducible for inputs with duplicate keys.
  - **anthropic / openai:** the provider adapters now declare and forward the
    agent-loop seams `onTokenUsage`, `memoryStore`, `enrichContext`,
    `deriveMemoryWriteback`, `configSeal`, and `traceSink` — previously these were
    unreachable through the bridges (token budget, memory, and config-seal were
    effectively dead via the published adapters).
  - **pack-deployments-approval:** total-order tie-break for the model/prompt gate;
    README documents three release-gate limitations (opt-in regression score,
    carbon clamp has no data-residency allow-list, model/prompt gate fires on first
    deploy).
  - **core:** documents and pins the v5 metadata cross-version verification contract
    (a pre-v5 verifier would falsely flag a metadata-bearing record as tampered).

- ce2cdc5: feat(primitives): add `createCommandRiskGuard` + `command-classify` (classifyCommand/stripDangerousFlags) for CLI/terminal agents — REFUSE/REWRITE(flag-strip, taint preserved)/REQUEST_CONFIRMATION by command risk (ADR-123).

  feat(core): add `validation.COMMAND_BLOCKED/COMMAND_FLAG_STRIPPED/COMMAND_SANITIZED` basis codes.

- 7545b17: feat(conformance): add Configuration Integrity Seal — sealPackConfig / verifyConfigSeal pin the introspectable config surface (declarative + guard metadata + probed taint minimums + basis codes) under a signature (ADR-121). Factored shared canonicalJson into its own module.

  feat(adapter-core): config-seal loop gate — verifies once per agent instance before the first adjudication; on mismatch refuses the turn (new `refused` AgentOutcome + `config_seal_violation` trace) and can engage the kill switch.

  feat(core): add `kill.SEAL_MISMATCH` basis code.

  feat(admin-sdk): add `governance.configSealStatus` for the console seal panel.

## 1.2.0

### Minor Changes

- e9fc3ad: # v0.5 — Foundation hardening, L2 expansion, analyzer, observability, console UX, 7 new CLI commands

  5 milestones (M1 → UX cut), 876 tests passing (was 748; +128), zero regressions. Status and remaining work tracked in `PROJECT_STATUS_AND_NEXT_STEPS.md`.

  ## Kernel hardening (M1)

  **Guard exception isolation (ADR-106).** `_adjudicateImpl` now wraps every guard invocation in `try/catch`. A throwing guard becomes a `SECURITY` REFUSE with `kernel.GUARD_PANIC` basis — never propagates to the adopter. New `BASIS_CODES.kernel` category. 9 property tests.

  **Resume-hash verification.** `verifyParkedEnvelopeHash` re-derives `intentHash` via `sha256Canonical` and asserts byte-equality on resume. `verifyHash: "strict" | "warn" | "off"` option on `resumeDeferredIntent` and the Anthropic adapter (default `"warn"`). The adapter now parks full envelope fields at DEFER time. Tampered park blobs are detected and fail-closed.

  **Portuguese externalization (ADR-107).** Kernel inline pt-BR strings replaced with English defaults. New `RefusalMessages` interface + `localizeDecision(decision, messages)` helper exported from `@adjudicate/core`. New `@adjudicate/locales-pt-BR` package supplies opt-in pt-BR strings.

  ## L2 primitives expansion (M2 / ADR-108)

  Four new factories in `@adjudicate/primitives`:
  - `createRewriteGuard` — REWRITE factory with `mutatesPayloadFields` metadata
  - `createConfirmGuard` — REQUEST_CONFIRMATION via threshold + prompt
  - `createEscalateGuard` — ESCALATE via threshold + route + reason
  - `createIdempotencyGuard` — domain-level idempotency check

  All carry `GuardMetadata` per ADR-105. Existing Pack guards are unchanged.

  ## Static analyzer (M2 / ADR-109)

  New `@adjudicate/analyze` package shipping Tier 1 metadata-driven analyzers:
  - AJD-101 MissingMetadataAnalyzer
  - AJD-102 SignalConsistencyAnalyzer (caught a real bug — PIX missing `Pack.signals`)
  - AJD-103 BasisCodeConsistencyAnalyzer
  - AJD-104 RewriteScopeAnalyzer
  - AJD-105 TaintPolicyAnalyzer
  - AJD-106 DefaultPolarityAnalyzer

  text / JSON / SARIF 2.1.0 output. CLI: `adjudicate analyze --pack <m> [--format] [--strict]`.

  PIX + deployments Packs now declare `Pack.signals` per AJD-102.

  ## AuditRecord v4 (M3 / ADR-111)

  Additive fields:
  - `policyVersion` — Pack.version at adjudication time
  - `kernelVersion` — `@adjudicate/core` package version
  - `auditHash` — `sha256` over `canonical(record \ {auditHash, signature})`
  - `signature` — pluggable KMS signature seam (v0.6+)

  `verifyAuditRecord(record)` exported for tamper detection. `AUDIT_RECORD_VERSION = 4`. v3 readers tolerate v4 (additive only). New `audit-postgres` migration `008-add-v4-fields.sql` adds 4 nullable columns + 2 indexes. admin-sdk Zod schema accepts v4.

  ## Shipped packages
  - `@adjudicate/conformance` (ADR-110) — `runConformance(pack)` ships 6 invariant checks (AC-001..AC-006) adopters call from CI. Deterministic via seeded LCG.
  - `@adjudicate/observability` (ADR-112) — OTLP-shaped `MetricsSink`, `LearningSink`, `AuditSpanExporter` + stable `SEMCONV` constants. Pluggable `Exporter` interface.
  - `@adjudicate/migrate` (ADR-112) — ts-morph codemod runner + first codemod (`nameGuard` → `withMetadata`).
  - `@adjudicate/locales-pt-BR` (ADR-107) — Brazilian Portuguese refusal-message mapping.

  ## Console UX (T-080..T-086)
  - **Live tail** (2s polling fallback; WebSocket bridge post-v0.6) via `<LiveTailToggle>` in TopBar
  - **WhyNotPanel** on decision detail page — explains which other Decisions were NOT reached and why
  - **Lineage explorer** at `/decisions/[hash]/lineage` — supersession chain as depth-limited tree
  - **DriftPanel** on Dashboard — counts `guard_panic` / `rewrite_taint_regression` / `defer_signal_drift` / `basis_code_drift`
  - **SLOPanel** on Dashboard — p50/p95/p99 per intent kind with utilization vs SLO budget
  - **ReplayDialog** extended for single-field payload edit + side-by-side decision diff
  - **FailureBanners** (Postgres lag, DLQ, drift) at the top of every page

  ## CLI commands (T-091, T-108..T-113)

  Seven new commands (5 + 7 = 12 total):
  - `adjudicate reap` — Idle-DeferStore Redis scanner
  - `adjudicate visualize` — Standalone HTML force-graph of a Pack's PolicyBundle (SVG-only)
  - `adjudicate repl` — Interactive intent → decision shell
  - `adjudicate replay` — Re-adjudicate stored AuditRecords + mismatch classification
  - `adjudicate export` — Audit records to JSON / CSV (Parquet deferred to v0.6)
  - `adjudicate scenarios generate` — Seeded LCG-based scenario fixture generation
  - `adjudicate dev` — Docker Compose harness (Redis + Postgres) for local dev

  ## Pack templates (T-034..T-036)

  `adjudicate pack init <name> --template <basic|payment|approval|kyc|deployment>` — 4 new domain-specific scaffolds covering payment / approval / kyc / deployment shapes. Each ships realistic guards using L2 primitives, taint policy, scenarios, and a conformance test.

  ## ADRs (7 new — ADR-106 through ADR-112)
  - ADR-106 — Guard exception isolation
  - ADR-107 — RefusalMessages externalization
  - ADR-108 — Primitives expansion
  - ADR-109 — Analyzer architecture + diagnostic catalog
  - ADR-110 — Conformance package
  - ADR-111 — AuditRecord v4 additive fields + verifyAuditRecord
  - ADR-112 — Observability + migrate packages

  ## Documentation (~7,000 lines, 19 new files)
  - `docs/perf/v0.2-baseline.md` — p50/p99 microbenchmarks (>200× SLO headroom on all paths)
  - `docs/release/{semver,api-surface,deprecations}.md`
  - `docs/pack-ecosystem/{quality-scoring,registry-foundations,signing-design}.md`
  - `docs/architecture/hosted/{control-data-plane,rbac-and-tenant-isolation,deployment-topology}.md`
  - `docs/security/{threat-model,security-review-checklist}.md`
  - `docs/compliance/{soc2-mapping,shared-responsibility}.md`
  - `PROJECT_STATUS_AND_NEXT_STEPS.md` — status snapshot + remaining work

  ## CI workflows (deliverable; not yet exercised)
  - `.github/workflows/ci.yml` — lint + typecheck + test
  - `.github/workflows/release.yml` — CycloneDX SBOM + Sigstore signing + npm provenance (workflow_dispatch)
  - `.github/workflows/security-codescan.yml` — pnpm audit on dep changes

  ## Non-negotiable invariants preserved
  - Kernel determinism: no `Date.now()`, no `Math.random()` in adjudication paths
  - LLM has zero mutation authority: every envelope still crosses `adjudicateAndAudit`
  - Decision algebra closed at 6 variants
  - Wire format frozen: IntentEnvelope v2, canonical-JSON hash, Decision shape unchanged
  - AuditRecord v4 is additive-only over v3
  - Fail-closed default preserved (REWRITE scope check telemetry-first; enforcement opt-in)
  - ADR-105 closed-vocabulary discipline applied to `BASIS_CODES.kernel`, `AJD-*`, `AC-*`, `SEMCONV.*`

- 36e7e76: # v0.6 — adapter-core extraction + OpenAI + Tier 2 analyzer

  Second-phase architectural advancement pass. The kernel API stays frozen; the provider integration surface, the analyzer, and the Pack ecosystem primitives all gained substance.

  ## `@adjudicate/adapter-core` (new) — ADR-113

  Extracted the provider-neutral orchestration into its own package. Contains the tool-use loop, the bridge (`classifyIncomingToolUse` + `buildEnvelopeFromToolUse`), the Decision translator, persistence shims (`createInMemoryDeferStore`, `createInMemoryConfirmationStore`), and the error taxonomy (`AdapterError`, `AdapterErrorCode`).

  Provider adapters now implement a `ProviderBridge<H>` against their SDK and re-export `createAdjudicatedAgent` from adapter-core. Adding a third provider is a < 200-line PR.

  History `H` is opaque to the loop — the bridge is the only thing in the codebase that knows the SDK-specific conversation-history shape. Every invariant the v0.5 loop preserved (replay determinism, fail-closed semantics, REWRITE executes the rewritten envelope, DEFER hash-verification, REQUEST_CONFIRMATION blob tamper detection) flows through unchanged.

  ## `@adjudicate/openai` (new)

  Reference OpenAI Chat Completions integration. Thin SDK shim over adapter-core. Accepts any object satisfying `OpenAIChatLikeClient` — the official `openai` SDK satisfies it structurally, mocks satisfy it, Azure OpenAI wrappers satisfy it. No hard `openai` dependency.

  Cross-provider parity verified by `tests/integration-pix.test.ts` — the same canned PIX-Pack conversation reaches the same six Decision kinds with the same audit-record counts and no `withBasisAudit` drift events.

  ## `@adjudicate/anthropic` — breaking surface change
  - The package is now a thin shim over adapter-core. The public API (`createAdjudicatedAgent`, `createAnthropicPromptRenderer`, persistence shims, error taxonomy) is preserved by re-exports from adapter-core.
  - `AgentEvent.tool_result.payload` is now the provider-neutral `ToolResultBlock` shape (`{ toolUseId, content, isError? }`) instead of the Anthropic-specific `ToolResultBlockParam` (`{ type: "tool_result", tool_use_id, content, is_error? }`). The loop maps to the SDK shape only at the bridge boundary.
  - `AnthropicAdapterError` / `AnthropicAdapterErrorCode` are kept as deprecated aliases for `AdapterError` / `AdapterErrorCode`; both will be removed in v2.0.

  ## `@adjudicate/analyze` — Tier 2 AST analyzer

  New `AJD-201 RewriteScopeAstAnalyzer` walks the actual source AST to verify a REWRITE guard's declared `mutatesPayloadFields` matches what the rewritten envelope's payload literal touches. Catches:
  - **Undeclared mutations** (error): a field is assigned in the rewrite but not declared.
  - **Stale declarations** (warning): a declared field is never touched by any rewrite.
  - **Unsafe spreads** (note): `{ ...payload }` without explicit overrides — static scope analysis cannot reason; surface to the operator.

  Diagnostics carry `sourceLocation: { file, line, column }` so editors and GitHub Code Scanning can deep-link. Opt-in via `analyzePolicy({ sourceFiles })`.

  ## `@adjudicate/conformance` — `validatePackManifest` primitive

  Standalone validator for the `package.json` `adjudicate` field per `docs/pack-ecosystem/registry-foundations.md`. Returns either `{ ok: true, manifest }` with a typed view, or `{ ok: false, errors }` with operator-readable violations. Consumed by the CLI, the future registry indexer, and adopter install hooks.

  `crossCheckPackVsManifest` cross-checks the live Pack against its declared manifest — catches drift between what the manifest claims (`intents`, `signals`) and what the Pack actually declares.

  ## `@adjudicate/core`
  - `KERNEL_REFUSAL_CODES` now includes `guard_panic`. The conformance harness's `KERNEL_INTERNAL_REFUSAL_CODES` overlay is removed; one less place for refusal-code drift to hide.
  - `assertPackConformance` vs `runConformance` split documented prominently in the module header — the boot-time / runtime / CI split is no longer ill-documented.
  - `explainRecord` gained `mergeExplanationRegistries(...)` for Pack-authors composing locale registries.
  - `DecisionExplanation` gained `supersession` field — when an AuditRecord v3+ carries `supersedes`, the explanation renders it as a single-sentence narration. Default templates cover `confirmation_resolved`, `defer_resumed`, `rewrite_executed`, `replay`.

  ## Numbers
  - 928 tests passing (up from 876), 1 skipped, 0 failing.
  - 52 net new tests: 24 adapter-core, 12 openai, 10 analyze (Tier 2), 10 core (explain extensions), 20 conformance (manifest), minus 24 anthropic tests that moved into adapter-core.
  - 1 new ADR (ADR-113).
  - 1 new package (`@adjudicate/adapter-core`).
  - 1 new provider adapter (`@adjudicate/openai`).

### Patch Changes

- 36e7e76: v0.7 — operational hardening + ecosystem trust. All additive; no kernel breaking changes.

  **Distributed kill switch v2.** `startDistributedKillSwitchPubSub` in `@adjudicate/audit` adds Redis pub/sub propagation on top of the existing polling helper. Sub-100 ms transitions when the subscriber is connected; polling retained as fallback for disconnects, restarts, and broker outages. See ADR-114.

  **Real-time audit event substrate.** `createInMemoryAuditEventBus`, `createRedisAuditEventBus`, and `bridgeAuditSinkToBus` in `@adjudicate/audit`. Operator consoles and live-tail views fan out without touching the durable sink contract.

  **Restart-durable confirmations.** `createRedisConfirmationStore` in `@adjudicate/adapter-core/persistence-redis`. REQUEST_CONFIRMATION tokens survive process restarts and rolling deploys.

  **Pack trust primitives.** `computePackFingerprint`, `signPackFingerprint`, `verifyPackSignature`, `verifyPackTrust` in `@adjudicate/conformance`. Pure functions, ed25519 + RSA-PSS, no hosted dependencies. See ADR-115.

  **`adjudicate pack verify` CLI.** Install-time + CI-gate wrapper around `verifyPackTrust`. Modes: `none | best_effort | require_fingerprint | require_signature`.

  **`replayWithIntegrity` + `explainReplayReport`.** `@adjudicate/audit` gains a verifier that runs decision-axis check AND envelope `intentHash` + AuditRecord `auditHash` tamper detection in one pass. `explainReplayReport` produces operator-readable narration in three formats (`ci-line | summary | operator`).

  **Cross-runtime golden vectors.** `docs/specs/canonical-hash-vectors.json` is the language-neutral consumer of the canonical-JSON SHA-256 spec. `packages/core/tests/cross-runtime-hash-vectors.test.ts` reads it and asserts the Node implementation matches; non-Node runtimes can do the same.

  **Adapter loop `TraceSink`.** `@adjudicate/adapter-core` exposes a low-cardinality lifecycle hook (`iteration_start | decision_emitted | paused | completed | max_iterations_exceeded`). Defaults to no-op; opt in via `traceSink:` on `createAdjudicatedAgent`.

  **Extended SEMCONV.** Eight new low-cardinality `adjudicate.*` attributes in `@adjudicate/observability` for adapter / provider / pause / kill-switch lifecycle. All additive; no renames.

  **Chaos test suites.** `packages/audit/tests/chaos-kill-switch.test.ts` and `chaos-replay.test.ts` exercise burst-of-malformed messages, disconnect/reconnect recovery, trip/clear storm convergence, multi-replica race (no split-brain), subscribe leak detection, and 100+ corrupted replay envelopes.

  **Test totals.** 1022 passing (was 924), 1 skipped (audit-postgres needs a live DB), 0 failing.

  See `docs/architecture/V0.7-AUDIT-REPORT.md` for the full v1.0 readiness review.

## 1.1.0

### Minor Changes

- Audit-2026-05-24 F2 — three backwards-compatible additions bundled as a single minor release. Surfaced by the IbateXas adopter during the audit-2026-05-24 closeout sweep; landed together so downstream consumers can adopt all three on one `pnpm install`.
  - **NEW: `BASIS_CODES.kernel.KERNEL_INTENT_DISPATCHED = "intent_dispatched"`** — adopters can now emit an explicit "the kernel dispatched this intent" basis on audit records (distinct from `GUARD_PANIC` which signals a guard threw). The value is the bare name `"intent_dispatched"` matching every other BASIS_CODES leaf (`"guard_panic"`, `"active"`, etc.); the category prefix is added by downstream `${category}:${code}` lookup. The pre-1.1.0 IbateXas consumer was hand-coding the literal `"kernel.intent_dispatched"` (with category prefix baked in); the new vocabulary entry closes that drift AND corrects to convention-consistent naming. `DEFAULT_EXPLANATION_REGISTRY` gains templates for `kernel:intent_dispatched` AND `kernel:guard_panic` (the latter was previously absent — operator console rendered the raw key for `GUARD_PANIC`).

  - **NEW: `SupersessionReason` gains `"lgpd_scrub"`** — per-surface LGPD/GDPR anonymization records can now link back to the originating customer-anonymize envelope via `supersedes.predecessorIntentHash` with a precise reason. Audit readers reconstruct the full scrub fan-out (`OrderProjection`, `ConversationMessage`, `LoyaltyAccount`, etc.) from one root record. Surfaced by IbateXas H3 Wave A1, which had been using `"replay"` as the closest fit in the closed union — semantically lossy. The default explanation registry, the supersession-chain analytics (`REASON_KEYS` / `emptyReasonCounts`), the admin-sdk Zod wire schema, and the operator-console lineage visualization all gained the new value alongside the union extension.

  - **CHANGED: `MetricsSink.recordShadowDivergence` is now optional** — downstream consumers running always-on kernels (no shadow path) can omit the method without keeping a no-op stub. The framework's internal call sites in `setMetricsSink` and `MetricsSinkSlot.recordShadowDivergence` use `?.()` to dispatch safely under any sink shape. `noopMetricsSink`, `noopSink`, and `createConsoleMetricsSink` all continue to define the method (optional methods may be present). Existing MetricsSink implementations across the workspace continue to define the method; the relaxation is purely additive for downstream consumers.

  **Migration:** required only for consumers that exhaustively pattern-match on `SupersessionReason`.
  - Adopters using `BASIS_CODES.kernel.GUARD_PANIC` continue to work; the new `KERNEL_INTENT_DISPATCHED` is additive.
  - Adopters whose `Supersession.reason` use is **non-exhaustive** (`if (r.reason === "replay") {...}`, switch with `default:` branch, etc.) continue to type-check; the new `"lgpd_scrub"` literal is additive for these consumers.
  - Adopters whose `Supersession.reason` use is **exhaustive** (`Record<SupersessionReason, X>`, `switch + assertNever`) will get a `TypeScript error: Property 'lgpd_scrub' is missing in type` on upgrade. Add the new branch carrying the locale-appropriate handler/label. Same fix on the Zod side: `@adjudicate/admin-sdk@1.1.0` extends `SupersessionReasonSchema` — upgrade both in lockstep so consumer code and the schema stay aligned. See RELEASE-1.1.0.md "Backwards-compatibility caveat" for a codemod snippet.
  - Adopters with hand-written MetricsSink implementations that already define `recordShadowDivergence` continue to work; the method moving to optional is a strict relaxation of the contract. Adopters who want to drop the no-op stub now can — `?.()` guards in the framework handle absence cleanly. `setMetricsSink` emits a one-time `console.warn` at install time when the installed sink omits the method so the silent-drop is operator-visible.

  **Verification:** 1122 tests pass across 21 workspace packages (plus 1 testcontainer-gated skip in `@adjudicate/audit-postgres`). `@adjudicate/core` 377/377 (including basis-vocabulary-purity property test which automatically picks up the new BASIS_CODES.kernel entry); `@adjudicate/audit` 181/181 (including supersession-chain); `@adjudicate/admin-sdk` 70/70 (including the Zod schema round-trip).

## 1.0.0

### Major Changes

- 663b572: Envelope v2 — nonce-based intentHash + auth-after-taint kernel reorder + v1 replay compat. Resolves #5, #7 (partial), #13, top-priority G.

  **Breaking** — `INTENT_ENVELOPE_VERSION` bumps to `2`. v1 envelopes are REFUSEd at runtime with `schema_version_unsupported`. Live writes are v2; pre-T8 audit rows replay via `legacyV1ToV2`. Within the `0.1.0-experimental` window, this is a deliberate fail-loud cutover that retires the most-cited foot-gun in the framework.

  The pre-T8 hash recipe `(version, kind, payload, createdAt, actor, taint)` made `createdAt` load-bearing for ledger dedup. An adopter rebuilding an envelope on retry without preserving `createdAt` silently produced a different `intentHash` — duplicate webhook deliveries re-executed. The README warned about this; the type system did not. T8 promotes idempotency to a first-class field.
  - **CHANGED: `IntentEnvelope` schema v2.** New `nonce: string` field (idempotency key, hashed). `createdAt` becomes descriptive metadata only (not hashed). Hash recipe is now `(version, kind, payload, nonce, actor, taint)`.
  - **CHANGED: `BuildEnvelopeInput.nonce` is required.** Adopters supply `crypto.randomUUID()` for first attempts and the SAME value for retries. `createdAt` remains optional; it can vary freely without affecting the hash.
  - **CHANGED: kernel evaluation order is `state → taint → auth → business`** (was `state → auth → taint → business`). UNTRUSTED inputs short-circuit before any auth side effect runs. Refusal-code distribution shifts in audit history: taint refusals on UNTRUSTED inputs that would also have failed auth now surface the taint refusal instead. Net safer; replay drift on the auth-vs-taint path may surface as `BASIS_DRIFT` for one corpus.
  - **NEW: `legacyV1ToV2(row)`** in `@adjudicate/audit-postgres` — synthesizes a v2 envelope from a v1 `intent_audit` row. Uses `row.nonce` when present (v2 row), falls back to the stored envelope's `nonce`, then to `createdAt` for true v1 rows. Replay produces the same Decision under unchanged policy; the synthesized `intentHash` does NOT match the v1 row's stored hash (different recipe) but the kind/basis comparison is meaningful.
  - **NEW: migration `003-add-nonce.sql`** adds the `nonce TEXT NULL` column plus a partial index on non-null nonces. Idempotent (`IF NOT EXISTS`).
  - **CHANGED: `IntentAuditRow.nonce: string | null`** carried through `recordToRow` and `rowToRecord`.
  - **NEW: `taintRank(taint)` exported** from `@adjudicate/core` (T4 carryover) — used by `withBasisAudit` for REWRITE taint regression detection.
  - **CHANGED: `replayEnvelopeFromAudit` reads `record.envelope.nonce`** with `record.envelope.createdAt` as a fallback for pre-T8 records.
  - **CHANGED: pix-payments-pix REWRITE site** plumbs `nonce: envelope.nonce` (preserves the original idempotency key through clamping).
  - **NEW: 6 unit tests** (`v1-replay-compat.test.ts`) covering nonce sourcing precedence, createdAt preservation, intentHash divergence under different recipes.
  - **NEW: 2 property tests** (`v2-hash-stability.property.test.ts`, 5 000 + 5 000 runs) — invariance under `createdAt` perturbation; differentiation under `nonce` perturbation.
  - **CHANGED: kernel ordering tests** in `adjudicate.test.ts` updated to assert the new pass-basis sequence and the new auth-after-taint short-circuit.
  - ADR-104 documents the cutover.

  **Migration:**
  - Adopters using `buildEnvelope({...})` without `nonce`: TypeScript error. Add `nonce: crypto.randomUUID()` for first attempts; preserve the value across retries.
  - Adopters with v1 envelopes in flight at deploy time: those envelopes will be REFUSEd by the new kernel. Quiesce v1 producers, drain in-flight messages, then deploy.
  - Adopters with v1 audit rows: `legacyV1ToV2` enables replay reads through the standard `replay()` harness without touching the storage.
  - Adopters whose auth guards had side effects: those side effects no longer fire on UNTRUSTED-refused intents. Most adopters benefit; a few who relied on auth-side logging for UNTRUSTED inputs need to move that logging to the taint pre-gate.

- 663b572: Pack conformance — REFUSE-by-default, Plan⊆Pack validation, drift across all decision kinds. Resolves #1, #3, #4 (partial), #18, #20, top-priority D + F.

  The pre-T4 conformance surface only caught REFUSE refusal-code drift, accepted any `policy.default`, and did not validate that a planner's `allowedIntents` matched the Pack's declared `intents`. The strongest claim of the framework — "the LLM cannot propose intents the Pack does not handle" — relied on adopter discipline. T4 closes the gaps.

  **Breaking** — Packs that ship `policy.default = "EXECUTE"` without explicit opt-in now throw `PackConformanceError`. Within `0.1.0-experimental` this is a deliberate posture flip toward fail-safe defaults.
  - **NEW: `assertPackConformance(pack, options?)` rejects `policy.default = "EXECUTE"`** unless `options.allowDefaultExecute === true`. The framework's recommended polarity is REFUSE; an EXECUTE default is the most direct authority leak and should be a deliberate, documented choice. Read-only Packs (search, summary) can opt in.
  - **NEW: `installPack(pack, { allowDefaultExecute: true })`** threads the option through to conformance.
  - **NEW: `assertPlanSubsetOfPack(plan, pack)`** — pure helper that throws `PlanConformanceError` if `plan.allowedIntents` contains an intent absent from `pack.intents`. Catches a planner advertising a mutation the Pack never claimed to handle (typically a renaming regression).
  - **NEW: `safePlan(planner, classification, pack?)`** — third optional arg. When supplied, every `plan()` call asserts both `assertPlanReadOnly` (existing) and `assertPlanSubsetOfPack` (new). The pre-T4 two-arg form continues to work; only adopters who pass a pack get the stricter check.
  - **NEW: `PlanConformanceError.intentsLeaked`** — companion to `mutatingToolsLeaked`. Lists intents that violated the pack-subset relation.
  - **NEW: optional `Pack.signals: readonly string[]`** — DEFER signal vocabulary. When declared, every DEFER Decision the Pack emits must use a `signal` from this list; `withBasisAudit` records `defer_signal_drift` for unknown signals. Cross-pack signal collision detection is left to a future Phase-2 registry.
  - **CHANGED: `withBasisAudit` extends drift detection across all decision kinds.** Previously it only inspected REFUSE; now every basis whose `category:code` is outside `BASIS_CODES` records `basis_vocabulary_drift`, REWRITE with `rewritten.taint` of higher rank than `envelope.taint` records `rewrite_taint_regression`, and DEFER with a signal outside declared `pack.signals` records `defer_signal_drift`. Decisions are still **not** blocked — drift is observed, not enforced.
  - **NEW: `taintRank(taint)` exported** from `@adjudicate/core` so adopters can perform their own rank comparisons. Used internally by `withBasisAudit`.
  - **NEW: `KERNEL_REFUSAL_CODES` gains `"ledger_replay_suppressed"`** (T1 carryover) so `withBasisAudit` does not flag it as Pack drift.
  - **NEW: 6 unit tests** (`pack-conformance.test.ts`) for default-EXECUTE rejection, signals shape validation, basis-vocabulary drift on EXECUTE.
  - **NEW: 9 unit tests + 1 property test** (`plan-allowed-intents.test.ts`, 5 000 runs) for `assertPlanSubsetOfPack` and the safePlan optional pack arg.
  - **NEW: 2 install-pack tests** for the new `allowDefaultExecute` plumbing.

  **Migration:**
  - A Pack with `policy.default = "EXECUTE"`: pass `{ allowDefaultExecute: true }` to `assertPackConformance` / `installPack`, OR change to `default: "REFUSE"` and add an explicit EXECUTE guard.
  - An adopter using `safePlan(planner, classification)`: no migration needed; the pack-subset check is opt-in via a third arg.
  - An adopter writing a custom Pack with mixed-vocabulary basis codes: any code outside `BASIS_CODES` now emits `basis_vocabulary_drift` telemetry. The decision still flows; treat the new event as a runbook signal.

### Minor Changes

- d8c11b7: Phase 6.2 — `adjudicate simulate` command + state rehydration convention.

  ## `@adjudicate/cli` — new `simulate` subcommand

  Run a single envelope against a Pack's policy and render the resulting Decision + per-guard evaluation trace.

  ```sh
  adjudicate simulate --pack @adjudicate/pack-payments-pix --scenario refund-medium.json
  adjudicate simulate --pack ./packs/my-pack/src/index.ts --intent intent.json --state state.json --format json
  ```

  - `--pack <module>` accepts any Node import specifier: npm package name (workspace symlinks work) or relative/absolute path (converted to `file://`).
  - `--scenario <file>` reads a bundled JSON with `intent` + `state` + optional `expected`.
  - `--intent <file> --state <file>` reads them separately — useful when many fixtures share state.
  - `--format text|json` selects output. Text is a minimal line-oriented placeholder; the full ANSI-boxed renderer lands in Phase 6.3.
  - When the scenario carries `expected.kind` and the decision doesn't match, exit code is 2. Otherwise exit 0.

  Scenario schema is Zod-validated; malformed JSON or unknown enum values produce a structured `ScenarioParseError` with a bullet list of issues + the source path.

  New programmatic exports from `@adjudicate/cli`:
  - `runSimulate`, `SimulateOptions`
  - `loadScenario`, `loadIntentAndState`, `ScenarioParseError`, `Scenario`, `IntentInput`
  - `loadPackFromModule`, `findPackExport`, `isLikelyPack` (shared with `pack lint`)
  - `renderSimulation`, `SimulationOutput`, `SimulationFormat`

  Internal refactor: the Pack-discovery helpers (`findPackExport`, `isLikelyPack`) moved from `pack-lint.ts` into a new `lib/pack-loader.ts` so `simulate` and `lint` share one definition of "what counts as a Pack export."

  New dependency: `zod ^4.3.6` for scenario validation.

  ## `@adjudicate/core` — `PackV0.rehydrateState`

  PackV0 gains an optional `rehydrateState?: (raw: unknown) => State`. Tools that source state from JSON (CLI `simulate`, future Console scenario builder, future audit-replay payload restoration) call this to convert from a serializable representation (typically `JSON.parse` output) back into the runtime state shape — needed when state contains `Map`/`Set`/`Date`/etc. that don't survive `JSON.stringify` round-tripping.

  Optional and backward-compatible — Packs with state that's already plain JSON (records, arrays, primitives) omit it.

  ## `@adjudicate/pack-payments-pix` — `rehydratePixState`

  Exports a new `rehydratePixState(raw)` function (also wired as `paymentsPixPack.rehydrateState`) that converts `{ charges: { [id]: PixCharge } }` from JSON into `PixState` with the runtime `Map<string, PixCharge>`. Idempotent on already-rehydrated input.

  ## `@adjudicate/pack-identity-kyc` — `rehydrateKycState`

  Exports a new `rehydrateKycState(raw)` function (wired as `IdentityKycPack.rehydrateState`) that converts `{ sessions: { [id]: KycSession } }` into the runtime `Map<string, KycSession>` shape.

  ## Verification
  - 8 new `simulate` integration tests cover all six PIX outcomes (EXECUTE, REQUEST_CONFIRMATION, ESCALATE, DEFER, REWRITE) plus KYC DEFER and the system-only-kind taint refusal.
  - 10 new scenario-schema tests cover valid input, structured Zod errors, missing fields, extra keys, and malformed JSON.
  - All existing tests remain green: core 253/253, PIX 28/28, KYC 14/14, primitives 13/13.

- d8c11b7: Phase 6.3 — ANSI-boxed `simulate` text renderer + `nameGuard` helper.

  ## `@adjudicate/cli` — rounded-box text renderer

  The `simulate` command's default text output is now a fixed-width rounded-box layout with four sections: decision header, intent metadata, per-guard trace, and decision-specific detail (refusal / escalation / prompt / defer / rewrite).

  Sample:

  ```
  ╭─ DECISION: REQUEST_CONFIRMATION ─────────────────────────────────────────────╮
  │ Pack       pack-payments-pix                                                 │
  │ Kind       pix.charge.refund                                                 │
  │ Actor      llm/sess-1                                                        │
  │ Taint      UNTRUSTED                                                         │
  │ Nonce      n-1                                                               │
  │ Hash       460891c47222...                                                   │
  ├──────────────────────────────────────────────────────────────────────────────┤
  │ Trace                                                                        │
  │   kill                                                                pass   │
  │   schema                                                              pass   │
  │   state[0]     escalateFailedConfirm                                  pass   │
  │   ...                                                                        │
  │   business[3]  requestConfirmForMediumRefund                          MATCH  │
  ├──────────────────────────────────────────────────────────────────────────────┤
  │ Basis                                                                        │
  │   schema     / version_supported                                             │
  │   ...                                                                        │
  │   business   / rule_satisfied                                                │
  │                rule:      confirm_threshold_reached                          │
  │                threshold: 50000                                              │
  │                requested: 60000                                              │
  ├──────────────────────────────────────────────────────────────────────────────┤
  │ Prompt                                                                       │
  │   You're about to refund R$ 600.00. Confirm?                                 │
  ╰──────────────────────────────────────────────────────────────────────────────╯
  ```

  - Width: terminal-adaptive, clamped to `[70, 120]`. Override via `RenderOptions.width` programmatically.
  - Color: chalk-styled, auto-disabled under `NO_COLOR=1` or non-TTY stdout.
  - Visual-width math is ANSI-escape-aware, so styled spans align correctly.
  - Long values (refusal `userFacing`, escalate `reason`, rewrite reason) word-wrap to the inner column.
  - `expected.kind` mismatch surfaces inline as `MISMATCH (got X)` in yellow.
  - Decision-specific detail blocks: `Refusal` (kind/code/user-facing/detail), `Escalation` (to/reason), `Prompt` (REQUEST_CONFIRMATION), `Defer` (signal/timeoutMs), `Rewrite` (reason/new kind/new hash). EXECUTE has no detail block — basis alone tells the story.

  `render(output, format, options?)` signature is unchanged on call sites; the new `options.width` is optional.

  ## `@adjudicate/core` — `nameGuard(name, guard)` helper

  Exported from `@adjudicate/core/kernel`. Attaches a stable `Function.name` to factory-built guards so they appear in `AdjudicationTraceEntry.guardName`:

  ```ts
  import { nameGuard } from "@adjudicate/core/kernel";
  import { createThresholdGuard } from "@adjudicate/primitives";

  const escalateLargeRefunds = nameGuard(
    "escalateLargeRefunds",
    createThresholdGuard({ ... }),
  );
  ```

  Guards declared as named consts (`const validateAmount: Guard = ...`) already get useful names via TS's variable-name inference — `nameGuard` is only needed when the guard comes back as an anonymous closure from a factory.

  Implementation: `Object.defineProperty(guard, "name", { value, configurable: true, writable: false })`. Idempotent (re-naming is allowed via `defineProperty`); preserves the guard's type identity (pass-through return).

  ## `@adjudicate/pack-payments-pix` — apply `nameGuard` to factory-built guards

  `escalateLargeRefunds`, `requestConfirmForMediumRefund`, `deferChargeCreate` now carry their names in trace output. Inline-arrow guards (`validateChargeAmount`, `clampRefundToOriginal`, `escalateFailedConfirm`, etc.) were already named via TS inference; no change there.

  ## `@adjudicate/pack-identity-kyc` — apply `nameGuard` to factory-built guards

  `requireDocumentUpload`, `waitForVerification`, `refuseLowScore`, `executeOnHighScore` now carry their names in trace output.

  ## Verification
  - 15 new renderer tests cover box framing, width clamping, section ordering, all six decision detail blocks, trace name/index formatting, expected/mismatch indicators, and JSON-format parseability.
  - All existing tests remain green: core 253/253, primitives 13/13, PIX 28/28, KYC 14/14, CLI scenario+simulate 18/18.
  - Manual smoke-test against PIX for all six outcomes confirms readable terminal output with correct alignment, colors, and decision-specific details.

- 663b572: Coordination integrity — atomic park, rate-limit rollback, defer-resume cycle cap, ledger race fix. Resolves #35, #36, #37, #38 (partial), #41, top-priority E + I.

  The framework's coordination primitives had three gaps. The kernel's load-bearing claim ("the same intent cannot side-effect twice") sat behind first-writer-wins on the ledger key, which two parallel `adjudicate()` callers could both pass before either recorded the SET-NX. The defer-resume cycle had no global cap on resume-park-resume oscillation. Rate-limit counters incremented on every request — including REFUSEd ones — letting hostile traffic exhaust legitimate users' budgets.
  - **NEW: `RateLimitResult.rollback()`** — return a rollback handle from `checkRateLimit`. When the kernel returns a non-EXECUTE Decision, the executor invokes `rollback` to decrement the counter. Idempotent (safe to call once or skip). No-op when the store does not implement `decrement`.
  - **NEW: `RateLimitStore.decrement?(key)`** — optional method on the store contract. `createInMemoryRateLimitStore` implements it (clamps to zero). Adopter Redis stores wire `DECR`.
  - **NEW: `AdjudicateAndAuditDeps.rateLimitRollback?: () => Promise<void>`** — when supplied, fires after sink emission iff the Decision was non-EXECUTE. Adopters compose with `checkRateLimit().rollback`.
  - **NEW: `Ledger.recordExecution` returns `Promise<"acquired" | "exists">`** (T1 carryover, surfaced here too) — `adjudicateAndAudit` uses the tag to flip a racing EXECUTE to `ledger_replay_suppressed` when SET-NX collides, closing #37 (parallel callers cannot both side-effect).
  - **NEW: `DEFAULT_MAX_RESUME_CYCLES = 3`** + `ResumeDeferredIntentArgs.maxResumeCycles` — per-`intentHash` resume cycle counter prevents DEFER → resume → DEFER oscillation under a misbehaving signal source. Returns `{ resumed: false, reason: "cycle_cap_exceeded" }` past the cap. Set to `0` to disable; back-compat skip when `redis.incr` is not wired.
  - **NEW: `DeferRedis.incr?` and `DeferRedis.expire?`** — optional Redis methods used by the cycle cap. Old adopters whose client lacks `incr` see no behavioural change (cap silently disabled).
  - **NEW: `ParkRedis.evalIncrCheck?(counterKey, ttlSeconds, max)`** — optional atomic Lua-eval increment-and-check. When wired, `parkDeferredIntent` uses it instead of the INCR-then-check sequence, eliminating the small race window at quota − 1. Adopters whose Redis client exposes `eval` can supply this; the framework falls back to the non-atomic sequence (the existing behaviour) when omitted.
  - **CHANGED: `parkDeferredIntent` EXPIRE refresh.** The pre-T5 implementation set the counter TTL via `EXPIRE NX` — once, on first park. Now the TTL refreshes on every park (no NX flag), so the counter outlives the latest envelope, not the first one's. Resolves #36.
  - **NEW: `taintRank(taint)`** exported from `@adjudicate/core` — used internally by `withBasisAudit` REWRITE-taint regression check (T4 carryover).
  - **NEW: 3 unit tests** (`rate-limit.test.ts`) for `RateLimitResult.rollback` (decrement, idempotency, store-without-decrement no-op).
  - **NEW: 4 unit tests** (`defer-resume-cycle-cap.test.ts`) for default cap, custom cap, disabled cap, back-compat skip.

  **Migration:**
  - Adopters using `checkRateLimit`: `result.rollback` is additive — call it on non-EXECUTE outcomes to fix #41. Old call sites that ignore it continue to work (counter stays advanced).
  - Adopters using `parkDeferredIntent`: counter TTL behaviour changes — refreshes on every park. Implementations whose Redis `expire` rejects calls without the NX flag must accept the new signature (`expire(key, seconds, mode?)` — second arg now optional).
  - Adopters using `resumeDeferredIntent`: no migration needed; the cycle cap is opt-in via wiring `redis.incr`.

- Remove `Plan.forbiddenConcepts` and `AuditPlanSnapshot.forbiddenConcepts`.

  The field made a structural promise the kernel did not deliver. `Plan.visibleReadTools` and `Plan.allowedIntents` are enforced by the bridge — out-of-plan tool/intent names are refused before the kernel sees them. `Plan.forbiddenConcepts` was rendered into the system prompt as a hint to the model and never enforced; a motivated user could get the model to emit a forbidden phrase and nothing in the framework caught it. The asymmetry was misleading on a security boundary — adopters reading the type believed the framework enforced it.

  Removed across:
  - `@adjudicate/core` — `Plan.forbiddenConcepts`, `AuditPlanSnapshot.forbiddenConcepts`
  - `@adjudicate/anthropic` — `renderer-anthropic.ts` no longer injects the phrases into the system prompt
  - `@adjudicate/admin-sdk` — `AuditPlanSnapshotSchema.forbiddenConcepts`

  Adopters who need post-hoc content moderation should run their own filter on assistant text before surfacing it — that is a content-moderation concern outside this framework's scope.

- 663b572: Distributed kill switch via polled Redis + IBX_KERNEL_ENFORCE typo guard. Resolves #15, #17, #40, top-priority C.

  The kernel's `setKillSwitch` writes a module-level singleton — a single process can revoke its own authority but nothing propagates across replicas. Multi-replica deployments had no path to halt the fleet without redeploying. T7 ships an opt-in distributed primitive that keeps the kernel's `adjudicate()` strictly synchronous (no async-everywhere) by polling a Redis key into the runtime context's in-process kill-switch.

  Independently, `IBX_KERNEL_ENFORCE`/`IBX_KERNEL_SHADOW` accepted any comma-separated string. A typo like `IBX_KERNEL_ENFORCE=order.confrim` silently left `order.confirm` on the legacy path — exactly the cutover hazard the staged rollout exists to prevent.
  - **NEW: `startDistributedKillSwitch({ redis, key, pollMs?, context?, logger? })`** in `@adjudicate/audit` — polls a Redis key on a `pollMs` cadence (default 1000ms). When the key carries `{active: boolean, reason: string}`, the value flows into `RuntimeContext.killSwitch.set(...)`. Within `pollMs * 2` of a remote write, every replica's `adjudicate()` returns `kill_switch_active`.
  - **NEW: handle methods `trip(reason)` / `clear()` / `stop()`** — convenience wrappers around `redis SET` plus a poller-stop. `stop()` is idempotent and synchronous post-call (timer cleared).
  - **NEW: poll error observability** — Redis GET errors and malformed payloads emit `recordSinkFailure({ subject: "distributed-kill-switch", errorClass: "redis_get" | "redis_payload" })`, plus an optional structured `logger.warn` callback.
  - **NEW: `validateEnforceConfig(knownIntents, env?, warn?)`** in `@adjudicate/core/kernel` — call once at boot. Compares every token in `IBX_KERNEL_SHADOW`/`IBX_KERNEL_ENFORCE` against the known-intent set (typically the union of every installed Pack's `intents`). Unknown tokens emit a `console.warn` plus `recordSinkFailure({ errorClass: "enforce_config_typo" })`. Returns `{ unknownShadow, unknownEnforce }` for further inspection. Wildcard `*` is honoured.
  - **NEW: 8 unit tests** (`distributed-kill-switch.test.ts`) covering apply-on-poll, key-absent no-op, transition handling, trip/clear convenience, redis-error and malformed-payload observability, stop semantics, optional logger.
  - **NEW: 5 unit tests** (`enforce-config.test.ts`) for `validateEnforceConfig` — clean config, shadow typos, enforce typos, wildcard, both-typos.

  **Migration:** opt-in throughout. Existing single-process deployments continue to work via the module-level kill switch; multi-replica deployments call `startDistributedKillSwitch()` at boot. ENFORCE typo detection is a new boot-time check; adopters with `IBX_KERNEL_ENFORCE=*` or no env var continue without change.

- d8c11b7: Add `adjudicateWithTrace` — tracing variant of `adjudicate()` for simulation and verification tooling.

  `adjudicateWithTrace(envelope, state, policy)` returns `{ decision, trace }` where `trace` is an ordered array of `AdjudicationTraceEntry` describing which guards ran and which one matched. The `decision` is byte-identical to `adjudicate(envelope, state, policy)` — both functions share a single internal implementation, so trace fidelity is structurally guaranteed.

  ```ts
  import { adjudicateWithTrace } from "@adjudicate/core/kernel";

  const { decision, trace } = adjudicateWithTrace(envelope, state, policy);
  // trace: [
  //   { phase: "kill",     outcome: "pass" },
  //   { phase: "schema",   outcome: "pass" },
  //   { phase: "taint",    outcome: "pass" },
  //   { phase: "business", index: 3, guardName: "requestConfirmForMediumRefund", outcome: "match" },
  // ]
  ```

  **Trace semantics:**
  - One entry per evaluated step, in order. Steps that didn't run (short-circuited by an earlier match) are absent.
  - Single-step phases (`kill`, `schema`, `taint`, `default`) emit one entry per call.
  - Array phases (`state`, `auth`, `business`) emit one entry per guard actually invoked, carrying its 0-based `index` and best-effort `guardName` from `Function.name`.
  - The trace always ends with exactly one entry where `outcome === "match"` — that step produced the final decision.

  **Zero hot-path overhead:** `adjudicate()` delegates to the same internal implementation passing `undefined` for `traceOut`, paying no allocation cost. Existing call sites are unaffected.

  Use cases this unlocks:
  - Phase 6 `adjudicate simulate` CLI: renders the per-guard trace to terminal output.
  - Operator Console replay: can show _which_ guard short-circuited, not just the final decision.
  - Phase 7 static verification: enumerate guard reachability over closed-enum input spaces.

  New exports from `@adjudicate/core/kernel`:
  - `adjudicateWithTrace`
  - `AdjudicationTraceEntry`, `AdjudicationTracePhase`, `AdjudicationTraceResult`

- 663b572: Kernel-side audit emission, ledger consult, metrics + learning unification via `adjudicateAndAudit`.

  The pure deterministic `adjudicate(envelope, state, policy) → Decision` was the only kernel entry point — production callers had to bolt on metrics, learning, and audit emission themselves, leaving the framework's "every decision is reconstructable" claim resting on adopter discipline. The new sibling closes that gap by composing the four side-effecting concerns at one call site.
  - **NEW: `adjudicateAndAudit(envelope, state, policy, deps)`** — async wrapper around the sync kernel. Consults the optional Execution Ledger (short-circuiting to a `ledger_replay_suppressed` REFUSE on a cache hit), runs the pure kernel, calls `recordDecision`/`recordRefusal`/`recordOutcome`, builds the `AuditRecord`, and emits it through the supplied `AuditSink`. Returns `{ decision, record, ledgerHit }`. Sink failures propagate; learning-sink failures are absorbed (telemetry never blocks).
  - **NEW: EXECUTE-race fix.** After `adjudicate()` returns EXECUTE, `adjudicateAndAudit` calls `ledger.recordExecution()` and flips the Decision to REPLAY_SUPPRESSED if the SET-NX returned `"exists"`. Two parallel callers can no longer both side-effect for the same `intentHash`.
  - **CHANGED: `Ledger.recordExecution` returns `Promise<"acquired" | "exists">`** instead of `Promise<void>`. Existing callers that ignored the void return type continue to work; the kernel uses the tag for the race fix above.
  - **MOVED: `Ledger`, `LedgerHit`, `LedgerRecordInput`, `LedgerRecordOutcome`, `AuditSink`** interfaces relocated to `@adjudicate/core` so the kernel can depend on them without inverting the package dependency. `@adjudicate/audit` re-exports them — adopter import paths are unchanged.
  - **NEW: `noopAuditSink()`** — no-op sink for entry points that need a sink-shaped value when audit is intentionally unwired (`adjudicateAndLearn` continues to work this way).
  - **NEW: kernel refusal code `ledger_replay_suppressed`** added to `KERNEL_REFUSAL_CODES` so `withBasisAudit` does not flag it as Pack drift.
  - **NEW: 14 unit tests** (`tests/kernel/adjudicate-and-audit.test.ts`) covering EXECUTE/REFUSE Decision passthrough, ledger hit short-circuit, ledger race, sink-strict propagation, learning-sink absorption, plan snapshot.
  - **NEW: 1 property test** (`tests/kernel/invariants/audit-emission.property.test.ts`, 1 000 runs) — every `adjudicateAndAudit` call emits exactly one AuditRecord whose decision matches the returned Decision.
  - ADR-101 documents the sync/async split rationale.

  **Migration:** `adjudicate()` is unchanged — replay/property tests/legacy callers continue to use it. Production paths should migrate to `adjudicateAndAudit({ sink, ledger? })`. `adjudicateAndLearn` is preserved (no behavior change).

- 663b572: `RuntimeContext` — per-tenant container for kill switch + sinks + enforce config. Resolves multi-tenancy gap (#8) and kill-switch env-seed one-shot (#16).

  The kernel ships with module-level singletons for several mutable slots (kill switch, MetricsSink, LearningSink, ShadowTelemetrySink, IBX_KERNEL_SHADOW/ENFORCE parses). Single-process multi-tenant deployments could not give two tenants independent kill switches or telemetry routing. Operators who flipped `IBX_KILL_SWITCH=1` after a manual `setKillSwitch()` call had no path to re-seed the env.
  - **NEW: `createRuntimeContext(options?)`** — mints a fresh container with isolated kill switch, metrics/learning/shadow sink slots, and an `EnforceConfig`. Each tenant holds the handle and routes reads/writes through it. Custom env-var names (`killSwitchEnvVar`, `shadowEnvVar`, `enforceEnvVar`) let tenants seed from per-tenant env vars (e.g., `IBX_KILL_SWITCH_TENANT_FOO`).
  - **NEW: `getDefaultRuntimeContext()`** — process-wide singleton context. Existing module-level functions (`isKilled`, `recordDecision`, etc.) operate on it; back-compat is total.
  - **NEW: `KillSwitchControl.reseedFromEnv(env?)`** — re-reads the kill-switch env var even after a manual `set()`. Operator escalation pathway during incidents.
  - **CHANGED: `adjudicateAndAudit` accepts optional `context: RuntimeContext`** — when supplied, metrics + learning events route through the tenant context's slots. The tenant kill switch is consulted ahead of the kernel kill-switch (both gates apply). Without `context`, behaviour is identical to T1.
  - **NEW: tenant kill-switch refusal** carries the tenant id in `basis.detail.tenant` so audit can distinguish process-wide vs per-tenant authority revocation.
  - **NEW: 14 unit tests** (`tests/kernel/runtime-context.test.ts`) covering kill-switch isolation, env-var override, reseed, sink-slot isolation, enforce-config isolation, adjudicate-and-audit routing, and back-compat.
  - ADR-103 documents the abstraction.

  **Migration:** existing module-level callers continue unchanged. New tenant-aware code calls `createRuntimeContext()` and passes `{ context }` to `adjudicateAndAudit`.
