Skip to content

v0.0.33

Choose a tag to compare

@savant0x savant0x released this 22 Sep 18:22
· 566 commits to main since this release

0.0.33 — 2026-09-22

The last five hook events, and the contract that described them (FID-2026-0919-031) — Closed + archived 2026-09-22

  • The five declared-but-inert events now fire, each with the semantics it had
    to be given first.
    Stop / Interrupt split on who ended the turn
    (finished on its own terms vs cancelled; a failure fires neither, and both are
    main-agent only — children report via SubagentStop). PreCompact is the
    compaction attempt (the pruner spawn) and PostCompact only fires on the
    runtime's existing pruned predicate, so a compaction that removed nothing
    cannot claim the context was compacted — the unmatched PreCompact is the
    signal. Notification is the runtime handing control to the operator (an
    ask_user call), carrying the questions.
  • The census now asks for reachability, not spelling. Naming an event in a
    helper's parameter type is what a type annotation does without firing anything,
    so a helper-mediated event counts only with a caller outside the module — and
    only with a caller passing that event when the helper is event-parameterized.
    Both holes were found by trying to break the rule: deleting the Notification
    call site and deleting the Stop call site each now fail the probe by name.
  • The documented contract was read back against the source, and five drifts
    corrected.
    The events table (all twelve fire, with semantics), the
    payload-field table (outcome / compaction / notification fields), the Source
    section — which named a parser function in a file that does not contain it and
    three wiring files that fire nothing — the missing action config row, and the
    previously unstated behaviour of matcher on a tool-less event (it is not
    consulted: the hook runs, because a config typo becoming a hook that silently
    never runs is the failure mode the event classification exists to prevent).
  • Found and fixed by the new pins, not by review: fireNotificationHook
    hardcoded tool_name: 'ask_user', so a notification raised for any other
    reason would have named a tool that never ran.
  • 16 new pins / 4 negative legs; full chain 7593 pass / 0 fail at
    implementation.
  • Closed + archived 2026-09-22 on the operator's closure directive; the
    receipt was re-stamped LIVE at the archived path so the closed record is
    byte-consistent with its final content. Commit SHA pending operator git
    execution (G2 withheld).

The hook surface tells the truth: acting agent on PreToolUse, session outcome, fired-vs-declared event census (FID-2026-0919-030) — Closed + archived 2026-09-19

  • PreToolUse could not say who was acting. It is the only hook event that
    can BLOCK a tool, and it is documented as composing with the EHEL gate — which
    gates per agent. Both call sites (the native gate chain and the custom/MCP
    path) sent session_id + cwd + tool_name + tool_input only, so the natural
    policies were inexpressible: "deny writes unless the caller is forge" had no
    field to test, and a subagent's call arrived under the child's run id with
    nothing naming the child. Both sites now carry the acting agent, and a census
    pin makes a third site impossible to add without it.
  • SessionStart/SessionEnd were outcome-blind. SessionEnd fires in a
    finally with identity only, so a clean end and a crash were indistinguishable
    at the hook — the SubagentStop defect one boundary up. The outcome is now
    computed on every path (return, error-form return, thrown) and reported, with
    SessionStart deliberately outcome-free. Both lifecycles share ONE builder
    (hooks/run-outcome.ts; hooks/subagent-outcome.ts re-exports), because two
    implementations of the same boundary binding is how FID-2026-0919-027's
    governance leak started.
  • Five of the twelve declared hook events never fired, and nothing said so.
    PreCompact, PostCompact, Stop, Interrupt and Notification are in the
    config vocabulary (so they parse and load) and were documented in
    docs/design/hook-system.md as active — but had zero firing sites in the
    runtime. A hook declared for one of them was silently inert: no parse error, no
    warning, nothing to observe. The sets are now data (FIRED_HOOK_EVENTS /
    NEVER_FIRED_HOOK_EVENTS, each inert event with the blocker that keeps it
    inert) with compile-time exhaustiveness gates, verified against the runtime
    source by scripts/hook-events-check.ts, which also fails if this repo's own
    protocol.config.yaml declares an inert hook. The doc's Events table now states
    what fires today.
  • The blockers are named, not hidden: three of the five have no defined
    trigger in the repo's own docs (Stop/Interrupt are documented as the same
    thing; Notification has no trigger at all), and the compaction pair needs an
    attempt-vs-effect semantics ruling plus two scattered sites — the pruner can
    complete having compacted nothing, so firing "around compaction" without a
    predicate would emit an event that lies.
  • 10 new pins / 49 expectations, driven at the real boundaries (createHookGate
    for the payload the engine receives, mainPrompt for all three session
    endings) plus the source census. Full chain 7577 pass / 0 fail. Three negative
    legs: drop the agent field → 3 red; drop the session outcome → 3 red; give an
    inert event a firing site → 2 red and the probe exit 1. All sources restored.
  • Closed + archived 2026-09-19 (operator directive). Record at
    dev/fids/archive/FID-2026-0919-030-hook-surface-truth.md; receipt re-stamped
    LIVE at the archived path (5/5 gates), fingerprint sha256:724b6c62…. Commit
    SHA pending operator git execution (G2 withheld).

Boundary observability and enumeration: SubagentStop outcome payload + SessionState/FileContext field partitions (FID-2026-0919-029) — Closed + archived 2026-09-19

  • Operator hooks could not tell a finished child from a failed one.
    SubagentStart / SubagentStop fired with type, session and cwd only, so
    three genuinely different endings — the loop returned usable output, the loop
    returned the error form ({ type: 'error', message }, which is how a silent
    no-assistant-turn run already reports itself), and the loop threw — produced
    byte-identical payloads. The hook contract already supported
    tool_result/error_message (the same fields PostToolUse /
    PostToolUseFailure use); executeSubagent, the single funnel both spawn paths
    share, never set them. hooks/subagent-outcome.ts now builds the payload and
    every terminal branch sets it — including the catch, which reports the cause
    and still rethrows. The default before any branch is the truthful
    "unknown ⇒ failed", so an unhandled path fails loudly instead of reporting
    success. The payload carries identity and shape only (status, agentType,
    runId, creditsUsed, outputType, errorMessage): hook JSON is stdin for
    hook commands, so a child's transcript would be unbounded and would be a
    content-leak path. SubagentStart is pinned to stay outcome-free.
  • SessionState and ProjectFileContext now have enumerated boundary
    contracts.
    FID-2026-0919-028 made AgentState self-enforcing at the spawn
    boundary and recorded this as the natural extension. Both types have a boundary:
    cloneSessionState deep-copies mainAgentState (JSON round-trip, for
    byte-parity with the persisted snapshot) and shares fileContext by reference
    (~230ms per snapshot avoided on the render thread); run start rewrites 10 of the
    16 file-context fields across two modules and carries the other 6.
    common/src/types/session-boundary-fields.ts classifies every key once,
    AssertNever gates make an unclassified field a build failure, and each
    exemption carries the reason it is safe. gitChanges is now declared as
    carried — captured at session init and not refreshed on resume — rather than
    left for someone to discover.
  • The census, not just the type, is the gate. A boundary can drift by gaining
    a writer without gaining a type key, so the pins include a census over the two
    modules that assign fileContext.*: a new unclassified writer fails a test.
    scripts/handoff-transport-check.ts now asks all three boundary questions
    (child, snapshot, run start) reading the shipped lists.
  • 15 new pins (7 hook-outcome through the real handleSpawnAgents →
    executeSubagent boundary, 8 partition pins driven off the shipped lists, 81
    expectations). Full chain 7567 pass / 0 fail. Every gate was proven by a
    negative leg: dropping the toolResult argument → 3 pins red; a field injected
    into each type → error TS2344 at its gate; a new run-start writer → census
    unclassified; a writer on a carried field and a copied fileContext → the
    probe exit 1. All sources restored.
  • Closed + archived 2026-09-19 (operator directive). Record at
    dev/fids/archive/FID-2026-0919-029-boundary-outcome-and-enumeration.md; receipt
    re-stamped LIVE at the archived path (7/7 gates), fingerprint
    sha256:fb6eabb5…. Commit SHA pending operator git execution (G2 withheld).

Spawn boundary made self-enforcing: an unclassified AgentState field is a build failure (FID-2026-0919-028) — Closed + archived 2026-09-19

  • The mechanism that dropped the governance configuration is closed, not just
    the fields it dropped.
    createAgentState inherited a hand-written list of
    parent fields, so the next field added to AgentState would be silently
    absent on every child — exactly how FID-2026-0919-027's four governance fields
    went missing, and why finding them took a full audit. Every one of the 54
    AgentState keys is now classified exactly once in spawn-child-fields.ts:
    13 inherited, 14 child-owned, and 27 deliberately absent with a
    stated reason
    each (groundingCheckpoint, maxContextLength/digestCaps
    because createLoopContext re-stamps them per run, the goal/drive records owned
    by the root drivers, per-run compaction telemetry, anti-runaway counters).
  • Omitting a field now fails typecheck. UnclassifiedAgentStateFields = Exclude<keyof AgentState, …> plus AssertNever gates make the build red for an
    unclassified field, a category overlap, or a governance field missing from the
    inherited list. Proven by injection: adding probeUnclassifiedField?: string
    to AgentState yields error TS2344: Type 'string' does not satisfy the constraint 'never' at the gate. The module documents the measured diagnostic
    limitation (TS renders the deferred Exclude rather than the key) and points at
    the exported alias as the readable entry point.
  • The list IS the behavior. createAgentState no longer hand-copies the
    inherited half — it spreads inheritFromParent(), single-sourced from the same
    partition, so a field cannot be documented as inherited and then forgotten at
    the construction site. inheritRunGovernance is now a subset of that partition.
  • The probe reads the authority. scripts/handoff-transport-check.ts
    iterates the shipped lists (inherited must cross, by-design must not leak) and
    fails if the authority answers unclassified — it can no longer agree with code
    it does not read.
  • 7 new pins / 114 expectations (spawn-child-fields): inherited fields cross,
    per-run instances are shared by reference, no by-design field is set on a fresh
    child, categories are disjoint, unknown names answer unclassified, governance
    ⊆ inherited. Full chain 7552 pass / 0 fail.
  • Closed + archived 2026-09-19 (operator directive). Record at
    dev/fids/archive/FID-2026-0919-028-spawn-boundary-self-enforcing.md; receipt
    re-stamped LIVE at the archived path (6/6 gates), fingerprint
    sha256:d85e2256…. Commit SHA pending operator git execution.

Inter-agent handoff transport: children inherit the run's governance (FID-2026-0919-027) — Closed + archived 2026-09-19

  • A spawned child was not governed by the run it belonged to.
    createAgentState threaded identity, ancestry, protocol variant, FSM phase,
    echoCompliance and provenance into every child — and dropped the run's
    resolved governance configuration. All four are read back from agentState
    inside the child's own tool path, so every subagent ran under defaults: a
    strict EHEL run silently relaxed to the hybrid tier in every child
    (getTier(strict) = all_15 vs getTier(hybrid) = core_4 — 11 of the 15 Laws
    ungated), the design write gate saw no contract on the writes Forge (always a
    child) performs, embedded installs re-resolved the protocol as local, and a
    child could open a second ZTAP session in record when the operator had set
    off. Fixed at the single construction point (spawn-child-state.ts →
    inheritRunGovernance) and made a contract: the propagation snapshot
    carries the four fields and executeSubagent rejects a child state missing
    any of them. groundingCheckpoint is deliberately excluded (children are
    exempt from the boot gate and must not claim the root's reads).
  • Raw evidence reached one audit-spawn path of three. The batch path
    required a ROOT parent (!parentAgentState.parentId), so a nested spawn
    restored nothing, and the inline path never loaded records at all — an inline
    or nested Verifier/Adversary audited compaction sentinels. One loader
    (evidence/spawn-evidence.ts) now unions the run chain for both sites, which
    is what the spill's per-RUN keying actually requires.
  • The context-pruner was batch-spawnable and silently a no-op. It is in the
    Orchestrator's spawnableAgents (it must be — the inline path validates
    through the same allowlist), so spawn_agents accepted it while performing
    none of the wiring that makes a prune land: the child pruned its own discarded
    copy and the parent's compaction never advanced — full cost, zero effect. Now
    rejected before dispatch at both seams with a reason that names the mechanism.
  • The inline relay dropped a child's artifact. spawn_agent_inline returned
    a constant { message: 'Agent spawned.' } while spawn_agents relayed the
    child's output, so a structured_output child informed its parent on one path
    and not the other. The constant is kept for harness-owned inline agents (their
    effect IS the history swap) and the real output is relayed otherwise.
  • One binding, one authority. The batch path carried its own copy of the
    ZTAP verdict-receipt block while the inline path used applyVerdictReceipts;
    both now route through the extracted authority.
  • 27 new pins (spawn-handoff-transport 11, spawn-evidence 4, propagation
    contract +2 assertions); agent-runtime 1462/0; full chain 7545 pass / 0
    fail. New live probe scripts/handoff-transport-check.ts reports the boundary
    on demand — RED (6 fields missing, all_15 → core_4, off → record, diverged
    grounding identity) → GREEN, with the negative leg proven by reverting the
    fix (exit 1 naming the four fields).
  • Closed + archived 2026-09-19 (operator directive). Record at
    dev/fids/archive/FID-2026-0919-027-inter-agent-handoff-transport.md; receipt
    re-stamped LIVE at the archived path (6/6 gates), fingerprint
    sha256:c705a855…. Commit SHA pending operator git execution. The mechanism
    that produced the defect is closed by FID-2026-0919-028.

Release tooling: automation-mode clean-tree dead code (FID-2026-0918-003)

  • The automation release path could never succeed: the pre-audit's absolute
    clean-tree gate contradicted the required-changes automation commit, so a
    clean worktree still entered the "commit the changes" branch and dead-ended.
    commitAutomationChangesOrTagHead (scripts/public-release/git-publish.ts,
    wired at scripts/public-release/stages.ts:92) now records the current HEAD
    with committedFiles: [] and creates no commit when the tree is clean, while
    a dirty worktree still sweeps into the single chore(release): prepare v<version> commit with the credential scan and governance warning intact.
    The pre-audit gate itself is untouched; docs/public-release.md
    transaction-order step 5 was amended; 2 pins in
    scripts/public-release-git.test.ts. Commit 2cc0b854. Internal
    release-tooling fix — no user-visible product change.
  • Recorded at release-audit time (2026-09-19): the closure session shipped
    this fix without a CHANGELOG entry, and the tagged v0.0.32 tree does not
    contain it (git show v0.0.32:scripts/public-release/git-publish.ts → 0
    matches), so it belongs to this release rather than the previous one.

Automation levels documented + documented-version-surface integrity + test isolation (FID-2026-0919-023)

  • Automation levels are now written governance. session.autonomy_level: 3
    existed as a bare scalar with a one-line gloss, ECHO.md's "Execution &
    Autonomy Modes" named Guided/Supervised/Autonomous in a note that never
    defined them, and the single-agent protocol — the governing document in
    single-agent sessions — had no such section at all. A canonical
    ## Automation Levels section now defines level 1 (Guided: extremely
    limited, approval for everything), level 2 (Supervised: approved work plus
    reversible in-scope fixes), and level 3 (Autonomous: complete agent
    automation, in-project findings fixed in the same pass), plus the
    session-ceiling-vs-item rule, the Law 2 semantics at level 3 (a recorded
    presentation, not a skipped one), and the invariants no level lifts — G1–G9,
    credentials, destructive operations, Law 3, Law 11, and the
    project-directory boundary. ECHO.md carries the matching ladder,
    reconciled with its version-control laws (levels govern how far the agent
    proceeds before asking; they never authorize a commit or push);
    templates/FID-TEMPLATE.md gained a required Automation level field; every
    item in SCOPE.md is tagged.
  • Documented version surfaces became a checked contract — and the check found
    a real drift on its first run.
    updateDocSurfaces replaced an exact
    oldVersion string and silently skipped on a non-match, so a surface
    advanced only when it sat exactly one release behind: ARCHITECTURE.md
    claimed the "current state" was 0.0.26 through seven bumps (now 0.0.33),
    the localized README blurb label had no pattern at all, and the 0.0.33 bump
    relabelled **v0.0.32** — in README.md, so the README advertised the
    previous release's content under the new version (its v0.0.32 chain entry is
    restored). One DOC_VERSION_SURFACES table now drives both the writer and
    the checker: the writer converges each declared surface from whatever version
    it currently states, and collectVersionDrift reports drifted or missing
    surfaces through version:check (the desktop-family precedent — "reported as
    missing, never skipped"). 5 pins in scripts/version-docs-drift.test.ts plus
    the existing fixture extended.
  • The root bun run test gate was RED and is now green. Two
    teacher/progression receipt assertions failed only in the full chain: the
    SEC-5 suite installed a process-wide mock.module on
    @savant-code/common/crypto whose stub threw unconditionally, so the teacher
    keypair derivation in the other suite signed nothing and
    adaptAttemptReceipt returned null. The leak reproduced in both suite
    orders and survived both afterAll(mock.restore()) and a beforeAll
    installation; the stub is now input-scoped — it throws only for that suite's
    payloads and delegates to the real signer for every other caller. 12/12
    workspaces pass, 0 fail.
  • Six pre-existing format-gate violations (files committed unformatted by
    earlier sessions, unrelated to this change) were formatted, taking repo-wide
    prettier --check green.
  • Gates: typecheck ×12 exit 0; bun run test exit 0 / 0 fail;
    version:check PASS; prettier --check . PASS; quality PASS (1498 files);
    validate:repository PASS; eslint 0; lint:md 0;
    release:public:preview exit 0; changelog audit PASS (no [Unreleased]
    accumulator, completeness verified tag-aware against git ground truth).
    Closed + archived 2026-09-19 (receipt re-stamped live at the archived
    path: 9/9 gates, fingerprint sha256:e3c9453a…). Pending operator commit
    (G2 withheld).

No agent-side scope trimming — vocabulary removed + mechanical guard (FID-2026-0919-024)

  • The protocol was authorizing it. The operator reported that approved work
    kept being marked out-of-scope without their approval. Root cause: the Scope
    Boundary section instructed the agent to mark a dropped item
    [DEFERRED]/[OUT-OF-SCOPE] "with a one-line reason" and to present it — a
    requirement a summary mention discharges — while the same document's
    Step-Level Anti-Deferral rule says only the operator may set those statuses
    (already enforced for FID steps). Measured spread before the fix: 2 live
    register items, 1 FID-index line, 3 changelog lines, 12 session summaries.
  • Vocabulary removed, not discouraged. Nothing is ever out of scope: the
    labels are declared non-existent as statuses, the prose forms ("backlog",
    "separate work", "acceptable residual", "tracked for later") are prohibited,
    and every item is completed or blocked pending an operator ruling with
    a specific blocker named — "too large", "adjacent concern", and "would be
    better as its own task" are disqualified as the trim wearing a blocker's name.
    The only lawful exit is an operator-approved ruling recorded verbatim with its
    date. Applied to both protocols, the anti-pattern tables, the emergency
    procedures, and the register preamble.
  • Mechanical guard. New echo/scope-disposition-guard.ts is the single
    authority — token detection with inline-code awareness (a backticked token is
    a quotation, a bare one is the disposition, which is what lets the rule be
    stated in the very artifacts it governs). Wired as a write-time block in
    runPreWriteGates, as scope.prohibited-disposition in
    validate:repository, and as the scripts/scope-guard-check.ts probe. Live
    surfaces (register, agenda, active FIDs) are scanned unconditionally; session
    summaries and the current-release changelog block are windowed from
    2026-09-19 so pre-existing records stay history.
  • Verification: 10 pins incl. reachability through runPreWriteGates; 49/0
    across the 8 pre-existing pre-write gate suites; the probe exits 1 on a bare
    token and 0 on the backticked form; validate:repository PASS; typecheck ×12
    exit 0; bun run test 0 fail in 12/12 workspaces; eslint 0; lint:md 0;
    prettier PASS; quality PASS (1498 files). Closed + archived 2026-09-19
    (receipt re-stamped live at the archived path: 6/6 gates, fingerprint
    sha256:195a024a…). Pending operator commit (G2 withheld).

Register completeness: a line for every tracked item (FID-2026-0919-025)

  • The quiet half of the scope guard. FID-2026-0919-024 removed the label an
    agent trimmed scope with, which left the omission as the remaining path: an
    item with no line in SCOPE.md is invisible in the register, so it can be
    dropped with no operator decision and with no forbidden token to detect.
  • Two decidable legs. New echo/scope-register-completeness.ts is the single
    authority: every active dev/fids/FID-*.md must be named in SCOPE.md, and
    every Task NN / TNN-X cited by an active FID or a session summary dated
    on/after the guard's effective date must exist there as a ## Task NN section
    or a TNN-X item. A reference in inline code is a quotation and a bare one is
    the claim — the rule the disposition guard already applies, now shared through
    one exported helper rather than restated. Wired as scope.unregistered-item in
    validate:repository and as the scripts/scope-register-check.ts probe; not a
    write-time block by design, since a FID is created and registered in two writes
    (the FID-2026-0917-002 deadlock class). The requirement is stated in the
    protocol's Scope Boundary and the register preamble.
  • It caught its own author. Pointed at the real tree while this record was
    still unregistered, the check reported 8 gaps in its own FID; making the scan
    inline-code aware took that to 4, and the register line to 0 — a live RED →
    GREEN on the real repository rather than a fixture.
  • Verification: 15 pins / 24 expectations; 25/0 across both scope-guard suites;
    validate:repository PASS; probe PASS (0 issues); typecheck ×12 exit 0;
    bun run test 0 fail in 12/12 workspaces; eslint 0; lint:md 0; prettier PASS;
    quality PASS (1498 files). Closed + archived 2026-09-19 (receipt re-stamped
    live at the archived path: 5/5 gates, fingerprint sha256:dd7cd79f…).
    Pending operator commit (G2 withheld).

B.AI credit-gate audit + provider quota visibility (FID-2026-0919-026)

  • The audit: the integration is correct and the account is unfunded. Operator
    report: live use returns credit insufficient balance: balance=0 required=3672
    while the vendor's key page shows 100% free usage. Both keys in the working tree
    authenticate (/balance, /models → 200; x-api-key and Bearer identical) and
    resolve to the same account with personal_balance: 0; the live catalog resolves
    47 internal ids including all five named models; the production chain emits
    POST https://api.b.ai/v1/chat/completions with the correctly stripped upstream
    id and bearer auth for every one of them — and every one is refused
    400 insufficient_user_quota. B.AI is prepaid (1 USD = 1,000,000 Credits)
    and none of the five is free: they are the cheapest paid tier, and the only
    documented free credit is a bonus that expires after 30 days. The operator's
    required=3672 is therefore ~$0.0037 against a balance of exactly 0.
    Corroborated by this repository's FID-2026-0911-004 (same key, HTTP 200 on
    qwen3.8-flash on 2026-09-11). No repository claim is false and the vendor error
    reaches the operator intact, so no rendering fix was warranted.
  • The fix (operator ruled "Both"): provider quota is now visible where the
    operator is. A data-only quota?: { url; valuePath; unit?; note } on
    ProviderConfig (mirroring catalog) — declared for B.AI against its documented
    GET /v1/balance — drives a bounded, fail-silent reader
    (cli/src/utils/provider-quota.ts) and one **Quota:** line in /health for
    the active provider; quotaHint in the existing send-message hint seam appends
    the provider's declared note to a quota-class refusal and points at /health,
    resolving the provider from DIRECT_PROVIDER or the model id's prefix and never
    fetching. A registry pin enforces that only providers documenting such an
    endpoint declare one.
  • Verification: 15 new pins (reader 6, hint 4, /health line 3, registry 2);
    typecheck cli + common exit 0; quality PASS (the registry entry hit the 300-line
    ceiling at 303 mid-pass and was condensed back to 300); root test chain 0 fail;
    validate:repository PASS; both scope probes PASS. Closed + archived
    2026-09-19
    (receipt re-stamped live at the archived path: 8/8 gates,
    fingerprint sha256:5a85c231…). Pending operator commit (G2 withheld).

2026-09-19 — Verification-contract integrity: receipt-contract widening + repo-gate completeness (2 FIDs archived)

Verification-contract widening + explicit enforcement reporting (FID-2026-0919-021)

  • Ground-truthing FID-2026-0918-007 against its own verification contract
    found two holes in the CONTRACT, not the record. The FID-2026-0918-006
    sweep reported 0 errors / 0 warnings over a document whose two test
    suites were covered by no declared gate: its promise pattern matched only
    the literal wording "new (runtime) test", and it read only the
    ### Verification section, while the artifacts were named in the Steps,
    Implementation Evidence, and Resolution sections.
  • Widened rule: a promise is any line — in any section — that names a
    repo-relative *.test.ts(x) path AND carries a novelty marker (new,
    added) on that same line, in either order. A bare filename is a mention,
    not a commitment (the directory separator is required), and the pathless
    branch is scoped to ### Verification + ### Implementation Evidence
    after the rule rejected this FID's own narrative prose (audit finding,
    pinned).
  • Separately, the receipt contract skipped every status outside
    fixed/verified SILENTLY — measured: 284 of 315 archived closed
    records
    carry a drifted fingerprint, because closure edits the document
    after the last stamp by design. Causally proven on FID-2026-0918-007 with
    git blobs (matching at 6e816902 while verified; stale after the
    3a0fe8dd closure batch). New echo/fid-verification-enforcement module is
    the single authority for the enforced-status set; fid:verify --check now
    names every active record outside the contract with its reason, as a
    non-fatal information tier. validateFidVerification behavior is unchanged
    (pinned) so a by-design skip can never become a blocking error. Law 13:
    scripts/fid-gates.ts's duplicate status set/parser deleted.
  • Live proof on the real record, reproduced under the final rule: 0
    violations → 3
    after the widening → 0 after FID-2026-0918-007 was
    amended with the two gates and re-stamped (fingerprint matching).
  • Gates: typecheck packages/agent-runtime 0; new suites 11/0 + 1/0 + 8/0;
    five pre-existing contract suites green (50/0); echo tree 189/0; repo-wide
    fid:verify --check PASS; quality PASS; eslint 0; lint:md 0; prettier
    clean; receipt 10/10 stamped live, re-stamped at the archived path.
    Commit SHA pending operator git execution (G2 withheld).
    Closed + archived 2026-09-19.

Receipt-contract completeness: declarable repo-gate checks + closed-record docs (FID-2026-0919-022)

  • bun run validate:repository was RED with 13 issues, 2 of them shipped BY
    FID-2026-0918-007's own part-3 code: bare 'bun' spawns at
    sdk/scripts/ensure-ripgrep-vendor.ts:114 and
    sdk/src/__tests__/ensure-ripgrep-vendor.test.ts:39 — the v0.0.30
    ENOENT-under-sanitized-env class audit.gate-env-parity exists to catch.
  • The escape route was structural: a FID could not DECLARE a repo-gate check,
    and validate:repository itself can never be one — it re-enters FID gate
    execution (C3) and would recurse (Lesson, FID-2026-0915-004). Fix: both
    spawns moved to process.execPath (the FID-2026-0909-002 precedent), and
    the specific check was made independently runnable
    (scripts/audit-gate-env-parity.ts gained an import.meta.main entry
    point: exit 0 clean / 1 with one line per issue) so the EXISTING probe
    kind can express it. This record proves it on its own receipt:
    - gate: probe scripts/audit-gate-env-parity.ts → PASS. A new audit gate
    kind was rejected (Law 5 — no new capability, and it would add parser,
    receipt, template, and bundle surface).
  • The probe route, the recursion boundary, and the T69 closed-record receipt
    semantics are documented in templates/FID-TEMPLATE.md; the protocol bundle
    was regenerated (1 file, harness v0.2.0) with generate:protocol-bundle:check
    PASS. Also under this record (operator directive): 11 hygiene.scratchpad-clutter
    violations cleared — scratch scripts moved from the dev/scratchpad/ root to
    archive/.
  • Gates: typecheck sdk + common 0; sdk install-hook suite 4/0; audit suite
    13/0 (incl. 2 new entry-point pins); protocol-copies + embedded-protocol
    33/0; probe scripts/audit-gate-env-parity.ts PASS; quality PASS; eslint 0;
    lint:md 0; prettier clean; receipt 8/8 stamped live, re-stamped at the
    archived path. validate:repository FAIL (13) → PASS. Commit SHA
    pending operator git execution (G2 withheld).
    Closed + archived 2026-09-19.

2026-09-19 — Context-window resolution: ladder fall-through fix, kiosapi sweep, live fallback-table audit (3 FIDs archived)

Exact terminal-segment match for mid-id-version model ids (FID-2026-0919-018)

  • kiosapi/glm-5.3-flash-free resolved to the 200k conservative default
    instead of its true 1,310,720 window: after the registry prefix strip the
    canonical is glm-5.3-flash, whose version sits mid-id — branch 3's
    terminal-version regex can't reduce it, and branch 3b's own
    familyName !== canonical guard skipped the one exact upstream twin.
    New branch 2b (exact terminal-segment equality) closes the crack
    between branches 2 and 3; near-collisions (glm-5.3-flashx, :batch)
    fail strict equality, pinned negatively. Root cause live-proven via
    probe (dev/scratchpad/probe-glm-flash-window.ts) against the real
    OpenRouter catalog before any code moved.
  • Gates: 018 suite 5/0; 016 suite green (regression); lookup suite 6/0;
    typecheck cli; quality PASS; receipt 5/5. The insertion pushed
    lookup.ts over the 300-line cap — findGatewayModel extracted
    move-only to gateway-lookup.ts. Commit a3d37313 (shared ladder/table
    commit with FID-2026-0919-019, operator-authorized 2026-09-19).
    Closed + archived 2026-09-19.

Kiosapi roster sweep: case-insensitive terminal match + researched fallback rows (FID-2026-0919-019)

  • Live sweep of the authenticated kiosapi roster (19 ids) showed 8 landing
    on the 200k default, splitting into three causes: a case-sensitivity miss
    (kiosapi/Qwen/Qwen3-8B vs catalog qwen/qwen3-8b — branch 2b now
    compares case-insensitively), vendor-known windows with no fallback row
    (six researched rows added: agnes-2.0/2.5/3.0-flash 524,288;
    atria-dawn-preview 262,144; sensenova-6.8-flash-lite 262,144;
    diffusiongemma-26b-a4b-it 262,144), and genuinely absent models where the
    flagged default is honest (big-pickle deliberately rowless). Acceptance
    re-sweep through the real resolver: 1/19 defaults remaining
    (big-pickle, as predicted).
  • Gates: typecheck cli+common; 018 suite 6/0; fallbacks + window-truth
    green; quality PASS; receipt 5/5. The rows pushed context-windows.ts
    over the line cap — table extracted move-only to
    context-window-table.ts. Also root-caused (flagged, not changed): the
    resolver prefers top_provider.context_length (serving limit) over the
    top-level capability — a deliberate pre-existing policy. Commit
    a3d37313 (shared with FID-2026-0919-018). Closed + archived
    2026-09-19.

Fallback-table live audit: 17 corrections + shadow-pin deletion (FID-2026-0919-020)

  • Full audit of all 140 CONTEXT_WINDOW_FALLBACKS rows against today's
    live OpenRouter API plus each gateway's own keyed /v1/models roster:
    97 exact matches, 4 verified via normalized spelling, 6 verified via
    gateway rosters (zero new wrong values), kiosapi ×6 standing on FID-019
    vendor research, 8 unverifiable/inert rows dispositioned in-table.
    17 rows corrected (12 substantive — glm-5.3-flash ×3 up to 1,310,720,
    qwen3.8-27b ×3 to 1,000,000, deepseek/gemini free-tier ups, and two
    dangerous over-allocations down to true capability — plus 5 rounding
    artifacts), under the operator policy that the table records model
    capability, never an artificial restriction.
  • Bonus Law-13 catch: two shadow pinned-window tables in
    static-catalogs-gateways.ts (INFRON_CONTEXT_WINDOWS /
    UNOROUTER_CONTEXT_WINDOWS) were overriding the fallback table and had
    silently drifted — deleted; the fallback table is now the single source
    of truth, enforced by suites.
  • Gates: typecheck cli+common; 46/0 across the four affected suites;
    quality PASS; receipt 6/6. Commit 0acad450 (operator-authorized
    2026-09-19). Closed + archived 2026-09-19.

2026-09-19 — Security hardening batch SEC-1/3–7 + verification, gateway and compaction fixes (13 FIDs archived)

Compaction signal repin: armed retirement + dedupe-guarded summary block (FID-2026-0918-004)

  • The pinned compaction panel could re-pin after retirement when the next
    run's status was a live phase (armed never ended), and micro-compact
    outcomes had no permanent record. Retirement now ends on any non-matching
    status; runMicroCompactPass emits a dedupe-guarded compaction_summary
    event (WeakMap keyed by agentState) when tokensSaved > 0 and the agent
    has no parent, so outcomes land a permanent in-stream
    CompactionSummaryBlock.
  • Gates: chat-store-compaction 18/18; run-agent-step + compactor suites
    83/83; typecheck cli/agent-runtime/common; eslint + prettier clean.
  • This record was archived 2026-09-18 with its commit withheld; the code
    ships here under commit 20fe3180 (shared compaction lineage with
    FID-2026-0919-017, operator-authorized 2026-09-19). Closed + archived
    (retro-stamped) 2026-09-19.

EHEL Law-3 circular dependencies blocked at write time (FID-2026-0918-005)

  • A new two-part Law-3 pre-write gate (pre-write-gates-law3.ts) rejects
    dependency writes that would create circular block/allow relations before
    they land, instead of surfacing the contradiction on the next read.
  • Gates: echo suites green across agent-runtime; typecheck clean.
    Commit b8bb5a5d (shared echo-crediting commit with FID-2026-0919-015).
    Closed + archived 2026-09-19.

Machine-checkable FID verification contract sweep (FID-2026-0918-006)

  • FID "verification" was prose; nothing machine-checked that a FID's
    declared gate paths actually exist and ran. New scripts/fid-verify.ts
    • fid-gates.ts sweep: parse each FID's gate: lines, require the paths
      to exist, execute the declared suites, and stamp a fingerprinted receipt;
      --check mode re-validates stamped receipts (negative proof). Fingerprint
      module extracted for reuse.
  • Gates: scripts suite 30/0; typecheck clean. Commits 57bdb173 (fingerprint
    module) + 5e1a3cf5 (sweep). Closed + archived 2026-09-19.

Fail-closed vendored ripgrep resolver + install-time vendor guarantee (FID-2026-0918-007)

  • The SDK's ripgrep surface crashed with a bare ENOENT when the vendored
    binary was missing (reproduced live mid-session). New
    ripgrep-path-fallback.ts resolves the vendored binary fail-closed with a
    precise error, and an install-time hook vendors ripgrep so a fresh clone
    can't skip it.
  • Gates: sdk ripgrep suite 16/0; typecheck clean. Commit 0e0ffcd9.
    Closed + archived 2026-09-19.

Allowlisted child-shell env — spawned shells no longer inherit provider credentials (FID-2026-0919-008, SEC-1)

  • Spawned shells (terminal tool children) inherited the full parent env,
    including provider API keys. buildChildEnv now passes only an allowlist
    of safe vars (PATH, HOME, locale/term basics) — credentials never reach
    child processes.
  • Gates: child-env allowlist suite green (sdk); typecheck clean. Commit
    be2ca107. Closed + archived 2026-09-19.

Indirect-prompt-injection channel (SEC-2) — DECLINED, residual risk accepted (FID-2026-0919-009)

  • Finding: agent-visible content from untrusted sources can be inherited into
    a later turn's history, which is an indirect-injection channel. The
    sanitization-boundary layer (layer 3) was declined by the operator
    (2026-09-19)
    : the residual risk through inherited history is accepted and
    recorded in the FID, on the grounds that the channel's two weaponizable
    downstream payloads are closed by SEC-1 (child-env allowlist, FID-2026-0919-008)
    and SEC-3 (span-scoped redirect waiver, FID-2026-0919-010). Layers 1-2
    remain available on request but are not approved. Recorded here so the
    release notes state the exclusion and its reason, rather than leaving it
    implied by this section's SEC-1/3–7 heading alone.

Span-scoped waiver for Windows stderr-redirect readonly probes (FID-2026-0919-010, SEC-3)

  • The readonly-command validator false-positived on 2>nul-style Windows
    stderr-redirect probes. The waiver is scoped to the exact redirect span so
    the bypass cannot smuggle arbitrary commands.
  • Implementation committed earlier in 4d89d27d; closure recorded with this
    batch (commit 6e816902). Closed + archived 2026-09-19.

Value-shape secret masking before the telemetry fan-out (FID-2026-0919-011, SEC-4)

  • Secret values could pass through to telemetry when the surrounding text
    wasn't a known key. cli/src/utils/logger/sanitize.ts now masks by value
    shape (key prefixes, bearer/jwt patterns) before any fan-out.
  • Gates: logger masking suite green (cli); typecheck clean. Commit
    9c2dcd5e. Closed + archived 2026-09-19.

Signing failures are loud — signing_failed event + binding catches warn (FID-2026-0919-012, SEC-5)

  • Record-mode receipt signing failures were swallowed silently, producing
    unsigned evidence with no trace. Provenance now emits a signing_failed
    event and binding catches warn instead of dropping.
  • Gates: provenance/session suites green (agent-runtime); typecheck clean.
    Commit 85e877af. Closed + archived 2026-09-19.

Database templates get clamped, fail-closed capabilities (FID-2026-0919-013, SEC-6)

  • Agent-declared templates could self-declare capabilities beyond their
    grants. Template capabilities are now clamped to the actual grants,
    failing closed on mismatch.
  • Gates: template suites green (agent-runtime); typecheck clean. Commit
    79cb00ea. Closed + archived 2026-09-19.

Destructive-command denylist enforced in every mode including dev override (FID-2026-0919-014, SEC-7)

  • Unsafe/sandbox modes could skip the destructive-command denylist. The
    floor now applies in every mode — dev override included.
  • Gates: sandbox engine suite green (agent-runtime); typecheck clean.
    Commit cca075b1. Closed + archived 2026-09-19.

Outcome-aware verification crediting — failed commands earn nothing (FID-2026-0919-015)

  • Verification credit was exit-code-blind: a crashed command still earned
    credit for "running the check". commandSucceeded is threaded through the
    crediting path so only successful gates count.
  • Gates: 4 echo suites green (agent-runtime); typecheck clean. Commit
    b8bb5a5d (shared with FID-2026-0918-005). Closed + archived
    2026-09-19.

Registry-driven gateway prefix strip + exact-version family matching (FID-2026-0919-016)

  • kiosapi/grok-4.6-free matched the wrong family (x-ai/grok-4.20, 2M
    context) because prefix stripping was version-blind. Matching now strips
    registry-declared gateway prefixes and prefers the exact-version model in
    the family — it resolves the correct 500k entry.
  • Gates: openrouter-models suite 7/0 (cli); typecheck clean. Commit
    246008fd. Closed + archived 2026-09-19.

Compaction signal is in-flight-only; retirement keyed on outcome identity (FID-2026-0919-017)

  • After a compaction, the sticky-bottom CompactionSignal panel painted as
    a fake last message — later turns rendered above it and it only cleared on
    restart. The slot now renders only the in-flight compacting phase;
    terminal outcomes live in CompactionSummaryBlock. Retirement dropping
    keyed on sameCompactionStatus drifted with percentUsed (a 62%→64%
    remirror disarmed retirement and re-pinned) — the drop now keys on the
    percent-blind epoch identity phase:tokensSaved, null-guarded. Sidebar
    formatter gained the missing blocked / ineffective labels so hiding
    the signal orphans nothing.
  • Gates: signal 9/0, retirement 10/0 (incl. percent-drift pin), sidebar
    format 6/0 (cli); typecheck clean; receipt 5/5 + --check PASS. Commits
    20fe3180 + cf3bebf2 (follow-up carrying the inverted suite missed by
    the first commit). Closed + archived 2026-09-19.