loki-mode

Changelog

All notable changes to Loki Mode will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

v11.17.58 (2026-10-17)

Fixed

v11.17.57 (2026-10-17)

Fixed

v11.17.56 (2026-10-17)

Added

v11.17.55 (2026-10-17)

Added

Changed

Fixed

Known

v11.17.54 (2026-10-17)

Added

Changed

Fixed

v11.17.53 (2026-10-17)

Added

Fixed

v11.17.52 (2026-10-17)

Added

Fixed

v11.17.51 (2026-10-17)

Fixed

v11.17.50 (2026-10-17)

Wave AO (28 TEN rows, all reviewed APPROVE)

Known red, fix-forward in flight

v11.17.49 (2026-10-17)

P0 fix-forward: main Tests green again

v11.17.48 (2026-10-17)

Wave AN (30 TEN rows, all reviewed APPROVE)

v11.17.47 (2026-10-17)

Fixed

v11.17.46 (2026-10-17)

Added

Changed

Fixed

v11.17.45 (2026-10-17)

Fixed

v11.17.44 (2026-10-17)

Added

Fixed

v11.17.43 (2026-10-17)

Fixed

v11.17.42 (2026-10-17)

P0 fix-forward for the nightly 39016259458 failures.

Fixed

v11.17.41 (2026-10-17)

Wave AJ: 9 reviewed slices.

Added

Fixed

v11.17.40 (2026-10-17)

Wave AG: 14 reviewed slices.

Added

Fixed

v11.17.39 (2026-10-17)

Added

Fixed

Known

v11.17.38 (2026-10-17)

Added

Fixed

Tests

Known

v11.17.37 (2026-10-17)

Added

Tests

Known

v11.17.36 (2026-10-17)

Added

Fixed

v11.17.35 (2026-10-17)

Wave Y: 4 HIGH slices.

Added

Fixed

Known

v11.17.34 (2026-10-17)

Fixed

Board

v11.17.33 (2026-10-17)

Wave W and wave AB: 16 reviewed slices.

Added

Fixed

Known

v11.17.32 (2026-10-17)

Added

Changed

v11.17.31 (2026-10-17)

Fixed

v11.17.30 (2026-10-17)

Fixed

v11.17.29 (2026-10-17)

Added

Fixed

v11.17.28 (2026-10-17)

Added

Fixed

v11.17.27 (2026-10-17)

Changed

v11.17.26 (2026-10-17)

Added

Fixed

v11.17.25 (2026-10-17)

Added

Fixed

Changed

v11.17.24 (2026-10-17)

Added

Fixed

v11.17.23 (2026-10-17)

Fixed

v11.17.22 (2026-10-17)

Added

Changed

Fixed

v11.17.21 (2026-10-17)

Fixed

v11.17.20 (2026-10-17)

Added

Changed

Fixed

v11.17.19 (2026-10-17)

Changed

Fixed

v11.17.18 (2026-10-17)

Changed

v11.17.17 (2026-10-17)

Added

Fixed

v11.17.16 (2026-10-17)

Fixed

v11.17.15 (2026-10-17)

Fixed

v11.17.14 (2026-10-17)

Added

Fixed

Changed

v11.17.13 (2026-10-17)

v11.17.12 was tagged but never published: its full test suite failed on one stale test assertion, so npm stayed at 11.17.11. Everything listed under v11.17.12 ships in this release.

Added

Fixed

Changed

v11.17.12 (2026-10-17)

Fixed

v11.17.11 (2026-10-17)

v11.17.10 never published: its Tests run was green, but the Release gate (run 38989985829) found that the committed loki-ts/dist/loki.js.map was not a fresh build of that commit. This release ships the v11.17.10 content with a freshly built dist and source map.

Fixed

v11.17.10 (2026-10-17)

v11.17.9 never published. Its Tests run (38988194257) went red on bf3d2ae24 because two FC-25c(a) safe_git tests failed in the shared bun process. This release ships the v11.17.9 content plus the fix and the slices merged since.

Added

Fixed

v11.17.9 (2026-10-17)

v11.17.8 never published: its Tests run (38986362613) was red on 7fc08f186 because the real-run harness fixture receipt did not name the check that Q17-04 pinned. This release ships the v11.17.8 and v11.17.7 content plus the fix and the changes below.

Fixed

Added

v11.17.8 (2026-10-17)

v11.17.7 never published: its Tests run was red on e66c51a09 because the Tier A test selector check (FC-89) found the new M-25w modernize producer-coverage test walking all of loki-ts/src without the walk-all-src marker. This release ships the v11.17.7 content plus the fix.

Fixed

Added

v11.17.7 (2026-10-16)

Fixed

Changed

v11.17.6 (2026-10-16)

Fixed

Changed

v11.17.5 (2026-10-16)

v11.17.4 never published: its Tests run was red because loki grill returned “provider returned no output” when the provider CLI ignored --output-format json. This release ships the v11.17.4 content plus the fix.

Fixed

v11.17.4 (2026-10-16)

v11.17.3 was written into this changelog but never published to npm: its required CI failed on line budgets, process substitution in run.sh, and a ShellCheck warning. This release publishes everything listed under v11.17.3 plus the changes below. loki start with Claude remains the primary path.

Changed

Fixed

v11.17.3 (2026-10-16)

v11.17.2 was written into this changelog but never published to npm: its required CI failed on help drift. This release publishes everything listed under v11.17.2 plus the changes below. loki start with Claude remains the primary path.

Changed

Fixed

v11.17.2 (2026-10-16)

Hardening release for loki start and its resume path, the steer channel, the Seal verifier, and the command safety hook. loki start with Claude remains the primary path and every change here applies to it; chat routes through the same commands.

Added

Changed

Fixed

v11.17.1 (2026-10-16)

v11.17.0 was never published to npm. Its release failed required CI because the changelog named loki start --resume while the start help and bash completion did not. This release fixes that, and it is the first to publish everything listed under v11.17.0 and v11.16.0, plus the fixes below. loki start with Claude remains the primary path, and every change here applies to it.

Fixed

v11.17.0 (2026-10-16)

v11.16.0 was written into this changelog but never published to npm: its release failed required CI on four guard checks. This release fixes those checks and is the first to publish everything listed under v11.16.0, plus the engine-plan slices below. loki start with Claude remains the primary path, and every change here applies to it; chat and Slack route through the same commands.

Added

Changed

Fixed

v11.16.0 (2026-10-16)

Engine-plan v1.2 slices, part two. loki start with Claude remains the primary path; chat and Slack steers now route through the same loki steer command and only report success when the steer’s revision is confirmed.

Added

Changed

Fixed

v11.15.0 (2026-10-16)

v11.13.0 and v11.14.0 were written into this changelog but never published to npm: both failed required CI. This release fixes those failures and is the first to publish everything listed under v11.13.0 and v11.14.0, plus the modernize work below. loki start with Claude remains the primary path.

Added

Fixed

Changed

v11.14.0 (2026-10-16)

v11.13.0 was tagged in the changelog but never published to npm: its required CI failed on the credentialed push guard, because loki verify-pr respond pushed with credentials outside the trusted push path. This release fixes that and ships everything listed under v11.13.0, plus the modernize and GitHub loop work below. loki start with Claude remains the primary path.

Added

Fixed

v11.13.0 (2026-10-16)

The factory-parity release: twelve lanes that close the gaps called out in the Factory comparison, from pull request repair and tracker writeback to a per-run cost cap that holds in every start mode and cheaper default effort. loki start with Claude remains the primary path, and every change below applies to it.

Added

Changed

Fixed

v11.12.1 (2026-10-16)

v11.12.0 was tagged in the changelog but never published: its CI run failed three guards on Linux. This patch carries all of v11.12.0 plus the fixes below, so upgrade straight from v11.10.1 to v11.12.1.

Fixed

v11.12.0 (2026-10-15)

The legacy modernization unit runner (M-15). loki start with Claude remains the primary path; this is a building block for the modernize program and is not yet wired to a user command.

Added

Changed

Fixed

v11.11.0 (2026-10-15)

Four new operator commands and options. loki start with Claude remains the primary path.

Added

Changed

v11.10.1 (2026-10-15)

Fix-forward release. v11.10.0 was cut but failed CI and was never published to npm. v11.10.1 carries every v11.10.0 change listed below, plus this fix.

Fixed

v11.10.0 (2026-10-15)

Two concurrent implement attempts behind a flag, plus fixes for the advisories left open by the v11.9.0 train. loki start with Claude remains the primary path.

Added

Changed

Fixed

v11.9.1 (2026-10-15)

Fix-forward release. v11.9.0 was tagged but failed CI and was never published to npm. v11.9.1 carries every v11.9.0 change listed below, plus these fixes for the guards that failed.

Fixed

v11.9.0 (2026-10-15)

Factory parity, phase 3. Work can now be delegated from Jira and Microsoft Teams, deploys carry a signed record bound to the receipt that authorized them, rollouts can be staged, and the workspace graph learns dependencies from code. Every capability extends the module that already owned its concern. loki start with Claude remains the primary path, and loki chat reaches the same code.

Added

Fixed

v11.8.1 (2026-10-15)

Fix-forward release. v11.8.0 was tagged but failed CI and was never published to npm. v11.8.1 carries every v11.8.0 change listed below, plus these fixes for the guards that failed.

Fixed

v11.8.0 (2026-10-15)

Phase 2 of the multi-repo company operating system. Parallel group runs on loki start can now be proven, interrupted groups resume without redoing verified work, multi-milestone specs run as missions, and work can be delegated from Slack and Linear. Every capability extends the module that already owned its concern. loki start with Claude remains the primary path, and loki chat reaches the same code.

Added

Fixed

Not in this release

v11.7.1 (2026-10-15)

Patch for v11.7.0, which was pushed but never published: its CI Tests run failed, so npm did not receive it. This release ships everything listed under v11.7.0 together with the fixes below.

Fixed

v11.7.0 (2026-10-15)

Phase 1 of the multi-repo company operating system. Workspaces become reachable from loki start and loki chat, a whole multi-repo change set can be sealed into one signed composite receipt, autonomy is set by policy, and verified deploys gain a health check with automatic rollback. Every capability is an extension of an existing module; no parallel implementation was added. loki start with Claude remains the primary path.

Added

Changed

Documentation

Not in this release

v11.6.0 (2026-10-15)

The consolidation release. Under the binding rule “extend existing code, never duplicate”, every duplicate module, parallel implementation and copied document was found, then merged into the owner of its concern or deleted. The result is 3,354 fewer lines net (5,984 removed, 2,630 added, mostly restored spec and ported tests) with no lost behaviour. Each removal was ported to its owner, with the owner’s tests extended to cover it. loki start behaves exactly as before; it now runs on one implementation per concern.

Changed

Removed

Documentation

v11.5.1 (2026-10-15)

Patch for v11.5.0, which was pushed but never published: its CI Tests run failed, so npm did not receive it. This release ships everything listed under v11.5.0 together with the fixes below.

Fixed

v11.5.0 (2026-10-15)

Less human attention per accepted task. Loki now discovers your repo and runs a baseline on its own, maps acceptance criteria to checks, verifies according to risk, recovers interrupted runs without double-publishing, and ends every run with a review handoff you can act on in under two minutes. Each feature extends a module, test file or doc that already owned its concern; no parallel duplicates were added. loki start is still the primary path, and loki chat reuses the same pieces.

Added

Changed

Process

v11.4.2 (2026-10-15)

Fix-forward for v11.4.1, which was tagged on main but never reached npm. The full Bun test job on the release commit had 13 failures, so required-ci stopped the publish. v11.4.2 contains everything listed under v11.4.1 and v11.4.0 below, plus these fixes.

Fixed

v11.4.1 (2026-10-15)

Fix-forward for v11.4.0, which was tagged on main but never reached npm. Its release run stopped at required-ci because the Tests run on the release commit was red. v11.4.1 contains everything listed under v11.4.0 below, plus these fixes and the remaining parity gaps.

Added

Changed

Fixed

Usability release: Loki now matches or beats opencode on everyday use, onboarding, interactive work, small changes, provider and local-model flexibility, multi-agent setup, docs consistency and predictability. Loki stays autonomous. The RARV-C loop, receipts, Seal and council are unchanged, loki start is still the primary path, and Claude Code is still the default provider. Plan and scorecard: docs/v10/OPENCODE-PARITY-PLAN.md. Comparison: docs/COMPARE-OPENCODE.md.

This release was built and shipped without running the test suites, on the founder’s direct instruction (velocity first). Report anything that misbehaves and it will be fixed forward.

Added

Changed

Breaking

Known gaps

Upgrade

v11.3.9 (2026-10-09)

Security follow-up to v11.3.6 plus a release-pipeline fix that lets verified releases reach the latest tag sooner.

Security

Changed

Upgrade

v11.3.8 (2026-10-09)

Release pipeline speed patch. Nothing changes in what Loki builds or verifies for users; this release changes only how Loki itself is packaged and published.

Changed

Behaviour notes

v11.3.7 (2026-10-09)

Maintenance patch release for contributors running many parallel worktrees. Nothing changes in what Loki builds or verifies for users.

Added

Fixed

v11.3.6 (2026-10-09)

Security patch release. Removing the GitHub token variables from a child environment did not remove the user’s other GitHub credentials: the gh CLI could still read its login from its config directory or the OS keyring, and git could still call the user’s credential helpers. Every token-free environment Loki builds now closes those paths through one shared mechanism.

Security

Changed

Fixed

v11.3.5 (2026-10-08)

Patch release that makes the fast gate always run the guard tests that scan the whole source tree, and corrects a public isolation claim to what the code actually guarantees.

Changed

Fixed

v11.3.4 (2026-10-08)

Patch release that hardens the release path and clears every red on main. A release-only commit now reuses its parent’s CI verdict, the fast gate selects only guards with a green baseline while keeping exact-path guards for nested manifests, Post-Release Smoke fails when a release drops a check or the Wall test, the promote gate recognizes a legacy-titled smoke run by its head SHA, and a dedupe-gated hourly full-suite backstop now measures main between releases. The Wall base run no longer misreads colored test output, and spec intake decides single-directory layout from the full tracked file list. Seven CI reds are fixed at their cause, and a test plan too large for its shard cap now falls back to the full suite. v11.3.3 was never published (its release run failed), so this is the first npm release since v11.3.2 and it also ships the v11.3.3 changes listed below that section.

Changed (behavior): project tests run without your GitHub token or SSH agent

Added

Fixed

v11.3.3 (2026-10-08)

Patch release that tightens the release path itself. Promotion of latest now requires a green nightly and a green Post-Release Smoke tied to the exact release, a new guard fails any workflow that masks a test failure, and a SIGPIPE guard keeps | grep -q from turning large output into a false red. No product behavior changes; loki doctor gets a test fix only.

Added

Fixed

v11.3.2 (2026-10-08)

Patch release that builds on the 11.3.1 feature set: loki start --attempts N can now open a PR, loki undo, loki verify-pr, loki issues run and loki export --sarif are added, receipts stop reporting unmeasured usage as zero, and the engine10 core is split so optional features live under contrib/. Most new behavior ships behind a flag that is OFF by default (LOKI_UNDO, LOKI_XVENDOR_DEFAULT, LOKI_AI_MARKING, LOKI_MCP_TASKS, LOKI_MCP_2026_07, LOKI_OTEL_GENAI, LOKI_E10_WALL_CONCURRENT, LOKI_LESSONS_REJECTED), and ER-01 effort right-sizing is off unless LOKI_E10_EFFORT_POLICY=rerun. Only opencode remains on the bash loop; the docs now say so.

Added

Changed

Fixed

v11.3.1 (2026-10-08)

Patch release that ships the ten v1 features of the 11.3 program: cost preview, mutation proof, intent card, reviewer brief, attempts, memory with proof, overnight queue, before/after proof, provider failover and the supply-chain guard. It also adds the CI-FAST gate, a docs cleanup and help-text truth fixes. The router still ships OFF, and so does the T2 downgrade.

Added

Changed

Fixed

Known issues

Build

Measured

v11.3.0 (2026-10-08)

Minor release. Adds the model router’s escalation chain and plan-time routing, both shipped OFF by default (LOKI_ROUTER unset or 0 is byte-identical to v11.2.2 across the 156-scenario differential), and hardens skill-link healing so it only heals from a durable global install. The remaining v1 features of the 11.3 program (cost preview, mutation proof, intent card, reviewer brief, attempts, memory with proof, overnight queue, before/after proof, provider failover, supply-chain guard) and the CI-FAST gate roll to 11.3.1.

Added

Changed

Fixed

v11.2.2 (2026-10-07)

Patch release. Adds measured fields to the B9 router scoreboard, derives the usage-governor seat cap from a measured claude -p /usage read, collapses loki doctor onto a single implementation, and gives the router one guarded plan-scope reader. The router still ships off; with LOKI_ROUTER unset, run behavior is unchanged from 11.2.1.

Added

Changed

Fixed

v11.2.1 (2026-10-07)

Patch release. Adds per-unit plan routing and the route line on receipts and PRs (both active only when LOKI_ROUTER is on), fixes the router telemetry flag known issue from 11.2.0, and ships four failure-class fixes. With the router flag unset, run behavior is unchanged from 11.2.0.

Added

Fixed

v11.2.0 (2026-10-07)

Minor release. Ships the model router core, off by default. With no flag set, behavior is byte-identical to 11.1.0. Set LOKI_ROUTER=1 to opt in. Opus plans and assigns, Sonnet is the default executor, and Haiku runs a unit only when Opus assigns it and the shape history has earned it.

Added

Changed

Fixed

Known issues

v11.1.0 (2026-10-07)

Minor release. Prepares the model catalog, pricing and plumbing for model routing: the Agent SDK moves to 0.3.293, Haiku 5.5 and Sonnet 5.5 join the catalog, and the groundwork for the advisor probe and per-shape outcome memory lands. The router itself is not switched on by this release.

Added

Fixed

v11.0.3 (2026-10-04)

Patch release. A one-line product brief no longer skips the Wall because a common word sits inside a longer name.

Fixed

v11.0.2 (2026-10-04)

Patch release. A one-line product brief no longer skips the Wall just because a token appears on most paths in the repo.

Fixed

v11.0.1 (2026-10-04)

Patch release. No product behavior changes: this release brings the Tests workflow on main back to green after 11.0.0 (run 37172048345) and ships the Control Plane container closure fix.

Fixed

v11.0.0 (2026-10-04)

Major release. The legacy engine is gone: every loki run uses the Loki 10 engine, and the signed receipt is the single source of truth for a run’s outcome. The Control Plane gets a new information architecture, truthful run detail and an opt-in Ask Loki page. Per the founder’s 02:08Z directive, the tests and reviews for this release were deferred; each is listed under “Tests owed after Oct 7” in docs/v10/RELEASE-11.md. tsc, the dist build and the dist guard ran; moat P9 runs in CI.

Removed (BREAKING)

Changed (BREAKING)

Added

Fixed

Not in this release

v10.11.2 (2026-10-04)

This republishes the v10.11.1 fixes. The v10.11.1 publish never reached npm because its release gate timed out on a cold Go compile, so FC-25 (safeGit hardening, moat P9 green) and FC-16b ship here.

Fixed

Added (off by default)

v10.11.1 (2026-10-04)

Moat P9 is green again, and Go and unittest passes can no longer be forged. FC-25 routes every git call that the token-holding supervisor and CLI make inside the agent’s repo through one hardened helper, so a planted fsmonitor, hook, sshCommand, ext:: transport or credential helper no longer runs with the real GitHub token. This closes the P9 regression open since v10.10.4. FC-16b stops reporting a Go exit 0 or a forged unittest trailer as a pass. FC-22 S2 and S3 land package-scoped test selection, which is opt-in behind LOKI_E10_SCOPE=1, and the install pre-step library, which S4 will wire into verify.

Added

Fixed

Known issues

v10.11.0 (2026-10-04)

Engine honesty and Ask Loki foundations. FC-23 makes Wall compile failures harness-owned and matches Wall tests to the package’s module system; FC-22 S1 adds the package graph and cited install command to the Project Model (not yet used for selection); CP-ASK slices 1, 4 and 8 land the read-only MCP mode, the Ask schema and the stream parser, all inert until LOKI_CP_ASK is enabled.

Added

Fixed

v10.10.5 (2026-10-03)

Release-gate fix for 10.10.4: the 10.10.4 publish was blocked at its gate (release run 37160286619, spawn env guard), so nothing reached npm as 10.10.4. 10.10.5 ships everything listed under v10.10.4 below, plus the fix here.

Fixed

v10.10.4 (2026-10-03)

Engine honesty train for the FireLater#17 gate: three failure classes (FC-15, FC-16, FC-21) plus FC-02 ship together. A success verdict now needs at least one Loki-executed test with a confirmed count, a run is never refused for its starting branch, and an implement time limit no longer skips verify. Behavior change: runs that previously ended VERIFIED or ALREADY_SATISFIED on unconfirmed test output (for example any Go run that exits 0, since go test output cannot be confirmed) now end PARTIAL with “test count could not be confirmed” in NOT PROVEN, and the CLI --json outcome reads PARTIAL where it read FAILED for a PARTIAL receipt (exit code stays 1).

Changed

Fixed

v10.10.3 (2026-10-03)

Fixed

Changed

Docs

v10.10.2 (2026-10-03)

Added

Changed

CI

v10.10.1 (2026-10-03)

Release-gate fix for 10.10.0: the 10.10.0 publish was blocked at its gate (spawn env guard and the MCP status contract test), and ten CI checks were red on its commit, so nothing reached npm as 10.10.0. 10.10.1 ships everything listed under v10.10.0 below, plus the items here.

Added

Fixed

CI

v10.10.0 (2026-10-03, not published)

Added

Changed

Fixed

CI

v10.9.1 (2026-10-03)

Release-gate fix for 10.9.0: the 10.9.0 publish was blocked at its gate by one loki-ts type error and an engine10 size budget overrun, so nothing reached npm as 10.9.0. 10.9.1 ships everything listed under v10.9.0 below, plus these fixes.

Fixed

v10.9.0 (2026-10-03)

Legacy dashboard removal release: the classic dashboard UI is deleted (the browser UI is the Control Plane), two guards keep it gone, and the Control Plane now serves the audit, checkpoint, memory, context, focus, tasks, session control and Completion Council routes the legacy dashboard answered. Also fixes the Python 3.10 pricing pin that turned 10.8.0 Tests red. Shipped under the founder CI waiver (D88); new tests for these slices are owed after the Oct 7 reset. It also fixes forward every 10.8.0 CI and Docker red (10.8.0 reached npm next only), retires 34 more legacy routes with named replacements (CPE24-P6), adds Project Model discovery behind a flag (EL-W1-01) with the L0 guard (EL-W1-00), and folds in FC-10, MW-2, L5, L8 and the opt-in fix-round resume.

Fixed

Added

Changed

Removed

v10.8.0 (2026-10-03)

Enterprise Control Plane release: the new Control Plane UI replaces the classic dashboard UI, the legacy /api shim is mounted, metrics, cost and fleet routes are ported, and test runs use a hermetic HOME. Shipped under the founder CI waiver (D88, “skip all CIs for next 10 releases”): this release did not wait for the Tests, Bun Parity or Security Audit workflows, and new tests for these slices are owed after the Oct 7 reset.

Fixed

Added

Docs

Fixed

Added

Changed

Fixed

Security

Deprecated

v10.7.1 (2026-10-03)

Loki Mode 10.7.1 is a fix-forward train. Test suites no longer flood a developer’s live Control Plane with fixture runs (acme/widget), and loki control prune --repo acme/widget removes any that already leaked (add –dry-run to preview). Control Plane ingest now verifies the sealed event log and shows tampered runs as TAMPERED. Engine Law L1: implement and fix run on the run’s own model by default (LOKI_E10_CASCADE=1 opts in to the sonnet downgrade). Scope control is advisory and never reverts an edit; out-of-scope files are listed in NOT PROVEN, the receipt and the PR body. The PR body now carries what the issue asked, files in scope and tests from the run’s recorded stage outputs instead of “not recorded”.

Changes

v10.7.0 (2026-10-03)

Loki Mode 10.7.0 turns on the fast path and new defaults. Runs start faster with the speed path on by default (LOKI_SPEED=0 opts out), and quiet mode shows one live status line. A cost cap is on by default: no dollar cap on a subscription, $100 on an API key. New in this release: Playwright end-to-end checks with video, the loki merge queue, loki review –risk, project memory, mobile emulator tests, a REST runs API, ACP for VS Code and JetBrains, and Sentry intake. The Control Plane now ingests runs with zero setup, the legacy dashboard entry points open it, and runs stream live. Default-on listeners bind loopback only.

Changes

v10.6.14 (2026-10-03)

train/100 green after full Tests rerun

Changes

v10.6.13 (2026-10-03)

The P7 no-fabricated-data moat check now pins its scalar-read exclusion with isolating honest and negative fixtures, so a regression that starts fabricating scalar values fails the moat suite instead of passing unnoticed.

Changes

v10.6.12 (2026-10-03)

Workspace runs API and E2E, interrupt handling, a bounded prep lock, honest dashboard and api runs reads, unmeasured cost shown as unmeasured, fast_verify exit codes, loki verify hardened against planted Python modules, a Jira site check for the tracker, a fail-closed backlog ledger, control-plane 400 on bad encoding, a speed spinner that no longer holds the process open, the Wall manifest library for engine10, an MCP run that reports an early crash with its exit code, the pre-run git status snapshot that refuses to run repo-configured fsmonitor or filter drivers, and the CI moat job renamed to Moat rules (no regression).

Changes

v10.6.11 (2026-10-03)

loki workspace run now works across linked git worktrees (all 9 D65 stub E2E legs pass from a clean install), the delivery contract is frozen at intake so a later edit cannot change what the receipt proves, and train pushes scan only their own range for secrets while main keeps the full scan and a red daily scan halts releases.

Changes

v10.6.10 (2026-10-03)

Loki 10.6.10 ships the loki.yaml schema in the npm package so loki workspace run works from an install, fixes the Control Plane container build, and makes help text agree on one UI command: loki opens the Control Plane.

Changes

v10.6.9 (2026-10-03)

Loki 10 makes the Control Plane the default, adds per-run cost caps, signed-receipt verification by key, workspace PR comments, and an off-path already-done check under LOKI_SPEED=1.

Changes

v10.6.8 (2026-10-03)

Loki 10 adds opt-in visual evidence in receipts and PR bodies (LOKI_VISUAL_EVIDENCE=1), Jira and Linear issue intake, two-way Slack and multi-repo workspaces behind flags, spec-to-contract (LOKI_CONTRACT=1), MCP run/status/verify tools, a Control Plane container with Helm and ECS examples, and the root GitHub Action now runs Loki 10.

Changes

v10.6.7 (2026-10-02)

loki start owner/repo#N (or an issue URL or a quoted task) now runs Loki 10, every Loki 10 run shows and enforces a cost cap ($20 default, --max-cost, loki.yaml budgets.per_run, exit 3 on BUDGET_STOP), PRs open with a reviewer-first body, the dashboard prints the port it actually bound, and the issue-to-PR Action runs Loki 10 with a nightly loki backlog example.

Changes

v10.6.6 (2026-10-01)

A next release. It ships the changes prepared for v10.6.1, v10.6.3, v10.6.4 and v10.6.5, none of which was published.

Scope control

Loki Control Plane v0

Release tooling

Docs

Not in this release

v10.6.5 (2026-10-01, not published)

Not published. Its loki start routing change regressed moat property P9 and was withdrawn; everything else shipped in v10.6.6.

v10.6.4 (2026-10-01, not published)

Not published. Its CI hit a transient npm registry 404 on a dependency tarball; the same content shipped in v10.6.5.

v10.6.3 (2026-10-01, not published)

Not published. The dashboard port fix failed the bare loki UI test and was withdrawn; everything else shipped in v10.6.4.

v10.6.2 (2026-10-01, not published)

Not published. Its loki start routing change failed the test suite, including three moat properties, and was withdrawn; it returns in a later release.

v10.6.1 (2026-10-01, not published)

Not published; shipped in v10.6.2.

Scope control

Loki Control Plane v0

Dashboard

Release tooling

Docs

Release note

v10.6.0 (2026-10-01)

A next release. This minor release marks the turn to v10 only: the plan to retire the legacy engine and dashboards is set, and the install and release pipeline is hardened for it.

Install

Release safety

Direction (docs only)

Coming in 10.6.x

Known issues

v10.5.35 (2026-10-01)

A next release.

Changes

v10.5.34 (2026-10-01)

A next release. loki start now always opens a working dashboard instead of a “dashboard_frontend_not_found” error page.

Fixed

Changed

v10.5.33 (2026-10-01)

A next release. A Loki 10 run that fails no longer opens a pull request made of files you had already changed before the run.

Fixed

v10.5.32 (2026-10-01)

A next release. Stopping a Loki 10 run with Ctrl-C or SIGTERM now returns your checkout to the branch you started on.

Fixed

Changed

v10.5.31 (2026-10-01)

A next release. A Loki 10 run that refuses to start in a checkout with uncommitted changes no longer commits anything to the user’s branch.

Fixed

v10.5.30 (2026-10-01)

A next release. A Loki 10 run that finds the task already satisfied now leaves the repository as it found it, and loki why --json and loki status --json have published JSON Schemas.

Fixed

Added

Documentation

v10.5.29 (2026-10-01)

A next release. A Loki 10 run whose commit stage fails no longer lands a backstop commit or opens a pull request.

Fixed

v10.5.28 (2026-10-01)

A next release that closes a false-VERIFIED path in the Loki 10 engine’s Seal: a run whose commit stage fails now ends FAILED and is never sealed VERIFIED.

Fixed

v10.5.27 (2026-10-01)

A next release. Provider calls can no longer hang on an open stdin, loki-seal recovers its stop-hook counter correctly, and the repository refuses commits by a foreign author in its main checkout.

Fixed

Changed

v10.5.26 (2026-10-01)

A next release. The promote gate now recognizes the exact message loki prints when the Loki 10 engine cannot run, so the two-leg gate can promote; the Docker images ship the same bun version as the npm package; and the repo hook judges a detached checkout by the directory a command actually changes into.

Fixed

Changed

v10.5.25 (2026-10-01)

A next release. npm install -g loki-mode now brings its own bun, so the Loki 10 engine (the default since v10.5.22) runs on machines that never installed bun. When it still cannot run, loki says so plainly on the first line instead of falling back quietly, and the release gate now checks both kinds of machine before moving latest.

Added

Changed

v10.5.24 (2026-10-01)

A next release of repo tooling and test fixes: no test can launch a real loki start against the repo root any more, the funnel-privacy test measures only off-machine traffic, and the fast CI tier stops running helper scripts as tests.

Fixed

v10.5.23 (2026-10-01)

A next release. Jira auto-sync starts when configured, the release gate checks the Loki 10 engine the way users with bun run it, and agent sessions can no longer detach the shared main checkout.

Fixed

Changed

v10.5.22 (2026-10-01)

A next release. v10.5.21 was never published: its Release run failed because a test still expected the engine-default note under Unreleased after the release moved it. This release ships everything listed under v10.5.21 (the Loki 10 engine is now the default for loki "<task>", issue mode and loki quick, and a bare loki verify after a Loki 10 run uses the v10 verifier with exit 4 for a non-VERIFIED outcome), and adds third-party receipt verification.

Added

Changed

v10.5.21 (2026-10-01)

A next release. The Loki 10 engine is now the default for loki "<task>", issue mode and loki quick, and a bare loki verify after a Loki 10 run checks its receipt with the v10 verifier, exiting 4 when the run’s outcome was not VERIFIED.

Changed

v10.5.20 (2026-10-01)

A next release. Quiet loki quick failures show the real error lines, standalone autonomy/verify.sh reports its attestation status instead of staying silent, and CI spends less time on slice branches.

Fixed

Changed

v10.5.19 (2026-10-01)

A next release. v10.5.18 was never published, because its Release run was blocked by CodeQL findings in the new /start onboarding API. This release fixes those findings and carries everything listed under v10.5.18. Legacy loki quick now exits non-zero when tests were weakened to make a run pass.

Fixed

Changed

v10.5.18 (2026-10-01)

A next release. Running loki with no arguments opens a local dashboard to connect a provider and GitHub, pick a repo and complete its open issues; v10 runs start even when a task’s setup rewrote a lockfile; and the docs were swept for stale claims.

Added

Changed

Fixed

v10.5.17 (2026-10-01)

A next release. loki verify now refuses unsigned receipts and catches a tampered event log even when the end of the log is deleted, loki backlog runs a repo’s issues headless from one loki.yaml, and v10 runs post to Slack.

Added

Changed

Fixed

v10.5.16 (2026-10-01)

A next release. Quiet loki quick output stays short and clean, the v10 Reason line never prints a credential, and a stray config file in your home directory is never moved.

Fixed

v10.5.15 (2026-10-01)

A next release. A v10 run that renames or deletes an existing test file can no longer be sealed as VERIFIED, and verify counts progress on an edited relevant test file correctly.

Fixed

v10.5.14 (2026-10-01)

A next release. v10 is quiet by default and never reports a tampered run as verified, a run stops on a real provider authentication or credit failure, and the signing key file name no longer trips the secret scanner.

Changed

Fixed

Internal

v10.5.13 (2026-10-01)

A next release. It carries everything in v10.5.12, whose version commit landed on main but was never tagged or published (its Release run stopped on three secret-scan false positives on the signing key’s file name), plus the work below. The headline: v10 refuses more ways of claiming success it has not earned, receipts are signed natively by default, and every v10 run ends with one named outcome and an honest exit code.

Added

Changed

Fixed

Internal

v10.5.12 (2026-09-30, not published)

Not published to npm: the Release run stopped on three secret-scan false positives. Everything below ships in v10.5.13. loki quick on an existing repository now commits only the fix, receipts get a local signing key by default, the Wall detects node:test repositories, and the new packages/loki-seal Claude Code plugin blocks “done” on red or weakened tests.

Added

Changed

Fixed

Internal

v10.5.11 (2026-09-30)

The first release on the npm next tag. loki verify no longer calls a freshly sealed receipt tampered, v10 commits only the fix, and the Wall no longer mistakes a broken test run for a failing test.

Changed

Fixed

Internal

v10.5.10 (2026-09-30)

v10 briefs now open with a byte-identical rules block so the cacheable prefix is shared across tasks, large eval tasks must carry frozen, upstream-first hidden tests, and a set of test-reliability fixes stops tests from leaking processes, reading the host’s real data or timing out under CI load.

Changed

Internal

v10.5.9 (2026-09-30)

v10 implement and fix briefs now name the files and impacted tests that matter instead of a raw list of repository paths, verify reports a system ruff lint separately from the system test interpreter, and the medium eval tier grows by three tasks.

Changed

Internal

v10.5.8 (2026-09-30)

Releases now come only from trains that pass structural checks in the fast tier, a release can publish after a dispatched security audit fix, test cleanup can no longer delete another run’s temporary directory, and the medium eval tier grows by three tasks.

Fixed

Internal

v10.5.7 (2026-09-30)

Main now moves only to a release train whose full test tier already passed, the eval scorecard reports 95% confidence intervals and refuses undersized samples, and the building blocks for issue intake, repo memory and parallel attempts land behind the v10 engine (not yet wired into a run).

Changed

Fixed

Internal

v10.5.6 (2026-09-30)

Parallel worktrees stop reinstalling npm dependencies from scratch, v10 sessions can trim oversized tool output behind a flag, and the eval harness now refuses to report a cost it did not fully measure.

Added

Changed

Fixed

Internal

v10.5.5 (2026-09-28)

Ships everything prepared for v10.5.4, which never reached npm because its release gate ran the v10 Wall tests without pytest: the Wall no longer mistakes a test run that never started for a failing test, and a Seal refuses any run with tests that did not run.

Fixed

v10.5.4 (carried): Fixed

v10.5.4 (carried): Added

v10.5.4 (carried): Internal

v10.5.4 (carried): Known issues

v10.5.4 (2026-09-28)

The v10 Wall no longer mistakes a test run that never started for a failing test, a Seal now refuses any run with tests that did not run, and loki modernize gains its pre-sealed Wall and depth-limited re-slicing, each refusing to call behaviour proven without a real, narrowing check.

Fixed

Added

Internal

Known issues

v10.5.3 (2026-09-28)

CI secret scanning can no longer be switched off by a pushed gitleaks configuration, and loki modernize gains an equivalence checker that never reports behaviour as proven when it cannot be.

Security

Added

Internal

v10.5.2 (2026-09-28)

A SPEC_CONFLICT receipt now states why the run stopped, safely, and the dashboard, web app and Python services pick up current patch and minor dependency releases, including a fix for two high-severity advisories in the web app’s router.

Changed

Security

Dependencies

Internal

v10.5.1 (2026-09-28)

A local pre-push secret scan that covers every pushed commit and file, so a secret can no longer reach the repository just because it sits outside the eval fixtures.

Fixed

Added

v10.5.0 (2026-09-28)

Loki 10 engine (opt-in: LOKI_ENGINE=v10): the verify stage now actually runs a Python project’s tests and the receipt can no longer call a run verified when the tests ran against the installed package instead of the repo. Also fixes the published package resolving an older Agent SDK than the engine was built for.

Changed

Fixed

v10.4.1 (2026-09-28)

loki modernize <repo> --to <target> now runs on the v10 engine whatever your default engine is, and the release tooling can no longer publish a version without its built files. This release carries v10.4.0, which was not published: its release was blocked by the secret scan (see Fixed).

Added

Fixed

v10.4.0 (2026-09-28)

loki modernize <repo> --to <target> now runs on the v10 engine whatever your default engine is, and the release tooling can no longer publish a version without its built files.

Added

Fixed

v10.3.1 (2026-09-28)

A user guide for loki modernize, and a pre-push secret scan that can no longer be weakened by a pushed commit’s own allowlist.

Added

Fixed

v10.3.0 (2026-09-28)

Loki 10 engine (still opt-in: LOKI_ENGINE=v10): runs that cannot finish now end with a visible result instead of stalling, and the first loki modernize command entry point lands. Also a pre-push secret scan for eval fixtures and swarm tooling.

Added

Fixed

v10.2.5 (2026-09-28)

Test guard and planning docs. No change to how loki runs your builds.

Added

v10.2.4 (2026-09-28)

Eval tooling release: one size rule for the medium and large eval tiers. No change to how loki runs your builds.

Added

Changed

v10.2.3 (2026-09-28)

loki modernize oracle groundwork and swarm tooling fixes. The modernize modules are internal and not yet reachable from the CLI; nothing changes for existing loki users.

Added

Fixed

v10.2.2 (2026-09-28)

Tooling release: a dependency inventory for the modernization workstream, plus the fix that restored the test suite after it landed. No change to how loki runs your builds.

Added

Fixed

v10.2.1 (2026-09-28)

Publishes the v10.2.0 changes below. 10.2.0 was bumped but never reached npm: its release gate stopped on a secret-scan false positive (a pinned upstream werkzeug commit in an eval task, allowlisted by exact fingerprint), and a version that is already bumped cannot be re-released by dispatch.

Added

Fixed

v10.2.0 (2026-09-28)

Loki 10 engine (still opt-in: LOKI_ENGINE=v10).

Added

Fixed

v10.1.1 (2026-09-28)

loki modernize groundwork. Both modules are internal building blocks of the upcoming loki modernize command and are not yet reachable from the CLI; nothing changes for existing loki users.

Added

Safety

v10.1.0 (2026-09-28)

Loki 10 engine improvements (still opt-in: LOKI_ENGINE=v10; the default engine is unchanged).

Added

Fixed

v10.0.1 (2026-09-28)

Republishes v10.0.0, which was tagged but never reached npm: its release commit failed two Tests shards because a test used 9.99.0 as a “far-future” version, which is older than 10.0.0. No product change; the v10.0.0 notes below apply in full.

Fixed

v10.0.0 (2026-09-28)

Loki 10: a new engine (TypeScript/Bun, under 5,000 lines, one state machine: intake, plan, Wall, one implement session, fast verify with at most 2 fix rounds, Seal, PR, async deep verify; 15-minute cap; one append-only event log per run). It ships OPT-IN: run LOKI_ENGINE=v10 loki "<task>" or LOKI_ENGINE=v10 loki owner/repo#N. The default engine is still legacy, because the release gate below was not met. Legacy is unchanged and is removed only after v10 becomes the default.

Release gate (D30), small tier, 29 tasks with hidden tests, all arms claude-opus-5-5

arm completed cost per completed task p50 / p90 time to PR
raw claude -p 27/29 (93.1%) $0.2363 39s / 68s
v10, default settings 26/29 (89.7%) $0.3946 (2 runs unmeasured) 85.5s / 133s
v10, lean configuration (LOKI_E10_PLAN=0 LOKI_E10_WALL=0) 27/29 (93.1%) $0.1616 (1 run unmeasured) 40s / 67s
legacy 15/29 (26/29 pass the hidden tests; legacy opens no PR) not measured 190s / 441s

Added in this release

v9.81.0 (2026-09-28)

Added

Fixed

Measured (not a release claim)

v9.80.1 (2026-09-28)

Fixed

v9.80.0 (2026-09-28)

Added

Fixed

v9.79.0

Loki 10 engine wave 1 and the release-gate eval harness land (still opt-in behind LOKI_ENGINE=v10). Released from green tree e86e846a (Tests run 36357562143 and Bun Parity run 36357562160 both success before the bump).

v9.78.0

First Loki 10 engine code, opt-in behind LOKI_ENGINE=v10; an ORPHAN guard for stuck runs. Released from green tree 4aa09e07 (Tests run 36355193993 and Bun Parity run 36355193981 both success before the bump).

v9.77.0

The dashboard focus notification respects a disabled dashboard. Released from green tree a4aad86c (Tests run 36352563585 and Bun Parity run 36352563557 both success before the bump).

v9.76.0

Council, checklist and proof readers are immune to user-site .pth files; auto-PR refuses a branch that still holds user files. Released from green tree 800ac46e (Tests run 36351690452 and Bun Parity run 36351690372 both success before the bump).

v9.75.0

Verdict readers ignore user-site .pth files; the cockpit says “not loaded”. Released from green tree 04af133a (Tests run 36349960792 and Bun Parity run 36349960827 both success before the bump).

v9.74.0

Unmeasured reads as unmeasured: tests, cost and dashboards. Released from green tree 28e62e44 (Tests run 36348570005 and Bun Parity run 36348570021 both success before the bump).

v9.73.0

Council votes honor failed counts; the log stream says when its API is down. Released from green tree ec01d54b (Tests run 36346770258 and Bun Parity run 36346770129 both success before the bump).

v9.72.0

Failures read as failures across the web app and dashboard. Released from green tree 2a024770 (Tests run 36346150997 and Bun Parity run 36346151002 both success before the bump).

v9.71.0

Honest metrics JSON and bump-commit verdict reuse for the Footer. Released from green tree c6ed3882 (Tests run 36344585850 and Bun Parity run 36344585823 both success before the bump).

v9.70.0

Faster CI on version bumps, honest heatmap, stricter council readers. Released from green tree b61045fd (Tests run 36343940285 and Bun Parity run 36343940266 both success before the bump).

v9.69.0

Trust page says when there is not enough history. Released from green tree 7ef5ca28 (Tests run and Bun Parity run both success before the bump).

v9.68.0

A faster release gate, honest dashboard states, and a current Footer version. Released from green tree b7a8dd50 (Tests run 36341812369 and Bun Parity run 36341812334 both success before the bump).

v9.67.0

Delegate pushes never target the default branch, verdict honesty fixes, and https-only attestation keys. Released from green tree 28926937 (Tests run 36341205226 and Bun Parity run 36341205233 both success before the bump).

Security:

Honest verdicts:

CI: Tier A caches its installs and its R0 step no longer times out before its job (S-96). The pulse no longer flags a claim that was later cited or retracted.

v9.66.0

Council honesty, a faster moat, and a clean quarantine list. Released from green tree 51ae52fe (Tests run 36338540948 and Bun Parity run 36338540897 both success before the bump).

v9.65.0

Honest proofs, a louder release tag step, and fewer vacuous tests. Released from green tree 55521236 (Tests run 36337415795 and Bun Parity run 36337415765 both success before the bump).

v9.64.0

Pulse machine protection and a 10x faster pulse. Released from green tree c2b84c36 (Tests run 36336659599 and Bun Parity run 36336659592 both success before the bump).

v9.63.0

First release under the verified-tree rule (D28): this tree passed Tests, Bun Parity and Security Audit on commit 1bc02bce before the version was bumped. It carries trains 5 to 7 plus the P0 fixes.

Versions that were never published: v9.58.0 was bumped but superseded by v9.59.0, which contains all of it. v9.60.0, v9.61.0 and v9.62.0 were bumped and tagged on trees whose Tests were red (a CI-only P9 harness gap and the lineage race below), so their releases were stopped before publishing. Their CHANGELOG entries below describe changes that first ship in this release.

Fixes:

CI: the Python suite runs serially again (the pytest -n auto switch is reverted until the race fix is proven on CI). Four suites are time-boxed in tests/quarantine.txt until 2026-10-03 (S-106, S-110).

v9.62.0

Release train 7: test hygiene, a test quarantine, and two P7 fabricated-data scanner gaps closed. Released on the founder’s instruction to ship completed work without further review cycles.

Tests and CI:

Moat P7 (no fabricated data):

v9.61.0

Release train 6: no shipped GitHub Action step runs the agent while holding a token.

Security (S-54, BACKLOG 139):

v9.60.0

Release train 5: Rule of Two credential withholding closes its remaining bypasses, a release reuses its parent’s CI verdict, and the pre-push hook drops to under a second. Released on the founder’s call without further review cycles on work already completed (D27).

Security (S-18, BACKLOG 149):

Release (S-84, Part C items 3 and 5):

Tooling (D27):

v9.59.0

Release train 4: council readers hardened, a Tier A fast test gate, and a release script that bumps every file.

Security:

CI:

Release:

Docs: a 60-line slice-card template and the dispatch-from-card rule in docs/v10/SWARM.md (S-77, D26 guard 6).

v9.58.0

Release train 3: the release-speed fixes from CEO Part C. Shell tests run in 8 duration-balanced shards with a 10-minute job cap, the argmax suite drops from about 22 minutes to seconds, Homebrew no longer waits on Docker, and Docker builds arm64 natively.

CI and release:

v9.57.0

Release train 2: the D26 guardrails go live, CI stops cancelling itself, and the Python suite runs in parallel. 21 code commits since v9.56.0.

Guardrails (D26):

CI:

Fixes:

Docs: docs/v10/OPERATING-MODEL.md is loaded in every session; CLAUDE.md shrinks to 150 lines with the rest under docs/dev/ (S-78).

v9.56.0

Release train 1 under the new release-train model: 157 commits since v9.55.0, verified together on one frozen commit. Rule of Two now covers every agent workflow, the tamper seal on the session-created record closes a FIFO and symlink bypass, several dashboard panels stop showing a guess or a zero as if it were measured, and the macOS 27 sandbox profile actually blocks the ports it claims to.

Security:

Honest verdicts and receipts:

Dashboard and web app:

Moat and tests:

Build: loki-ts/dist rebuilt from source (the committed bundle had been built in a different environment; the only semantic difference is the version string).

v9.55.0

A repo-wide sweep for the process-isolation bug class fixed in v9.54.1 found and closed several more real gaps: two agent-repo-code-execution paths, and two rounds of a tamper-detection seal being itself forgeable. Plus a default-security tightening, the v9.54.1 kill-scoping fix extended to more surfaces, and several trust-core correctness fixes.

Security fixes:

Correctness fixes:

CI reliability: several shell test files were found sourcing autonomy/run.sh (or a copy of it) into a shell still working out of the shared repository checkout; a side effect of sourcing it (provider auto-detection) wrote state into that shared checkout, silently breaking whichever later test in the same CI shard happened to read that state next. This intermittently broke unrelated tests depending on shard composition. A repo-wide sweep closed 22 more instances of this pattern (beyond the ones already fixed), and the test that kept surfacing this bug class is separately isolated against any future instance.

v9.54.2

Re-cut after v9.54.1 failed the release gate’s Security Audit step before any publish job ran: a gitleaks secret scan flagged a synthetic, API-key-shaped test fixture in tests/test-branch-lifecycle.sh (used to prove the session-commit secret-scan-abort path actually aborts on a real secret) as a possible leak. Not a real credential; the fixture predates this fix and simply lacked its .gitleaksignore entry. v9.54.1 was never published to any channel. This release carries the same fix described below plus two additional, independently reviewed and approved slices: BACKLOG 22 (an orphaned sleep 300 from the resource monitor after loki start exits, now reaped by its own recorded PID on shutdown) and a moat test-control correction (BACKLOG 46).

v9.54.1

A loki-mode session ending could terminate other, unrelated Claude Code sessions on the same machine. This is now fixed.

What happened: when a loki start run ended via a signal (a supervisor signal, pressing Ctrl+C twice, or pressing Ctrl+C once while running in perpetual/autonomous mode), its cleanup step ran pkill -f against the provider process names (claude, codex, aider, cline) with no scoping at all. pkill -f matches the full command line of every matching process on the whole machine, not just the ones this run started. In practice this meant: any time a loki-mode run ended that way, every other Claude Code session open in a different terminal or a different project on the same machine, whose command line happened to match, could be terminated along with it.

Who was affected: anyone who ran loki start (directly or through the loki-mode skill) and ended that run with Ctrl+C, a supervisor signal, or by letting a perpetual/autonomous run be interrupted, while other Claude Code sessions (claude, codex, aider or cline processes) were open on the same machine. A run that finished normally, or was stopped with loki stop (no signal), was not affected; the bug was specific to the function’s signal-driven cleanup path (kill_provider_child in autonomy/run.sh).

The fix: that cleanup step now only ever signals a process that shares this run’s own operating-system process group, which a completely separate Claude Code session never does. It still cleans up the provider processes this run itself launched, including ones that were reparented after an earlier crash or retry, but it can no longer reach outside this run’s own process tree. Covered by a new regression test (tests/test-kill-provider-child-scoping.sh) that proves both directions: a leaked process belonging to this run is still cleaned up, and an unrelated process in a different process group survives. Registered in the release gate so this class of regression cannot ship silently again. See docs/v10/DECISIONS.md D14 through D17 for the full record, including two related bugs the same investigation found and fixed in test code.

v9.54.0

Resuming a session no longer endangers the user’s files. Six council rounds on this cycle’s brownfield work found and closed these paths, each pinned by a test that fails on the code before its fix:

Test evidence from an earlier session is never this session’s. A new session restarts at iteration 0, so on both routes it now drops the previous session’s freshness marker, unit-tests.pass and test-results.json; before, a previous run’s pass could read as fresh on the Bun gate and be reported by the receipt. A zero-test bash run no longer leaves unit-tests.pass or tests.pass: true in the evidence detail (the latter is now the string "inconclusive" for non-affirmative outcomes, a type change for external readers). The Bun npm-test fallback detects zero tests, honours failed_count, and treats a stale test-results.json as no evidence. New case P2.zero-test-never-affirmative.

More verdict paths cannot import from the agent’s repo (D7). Checklist verification (prd-checklist.sh), the council helpers (voter-agents.sh, council-v2.sh, done-recognition.sh, proof-check.sh) and load_state run their inline Python with python3 -E and the cwd removed from sys.path; the managed council imports from the Loki install, never the cwd. New case P2.checklist-verify-not-shadowed.

Moat: 1 of 9 proven; 55 cases registered, 32 pass, 23 pending; the ratchet checks v9.52.0 and v9.53.0.

Known gaps (docs/v10/BACKLOG.md 61-89): with LOKI_BRANCH_PROTECTION=false on a leftover session branch a stale snapshot is used (also in v9.53.0; next in line); agent files under an adopted ignored directory are neither committed nor listed; deletions and still-ignored edits are not disclosed; the in-flight provider turn at a SIGKILL or SIGHUP is not recorded; __pycache__ from the test gate can reach the commit when not ignored.

v9.53.0

P6 in-place brownfield is proven: the moat now reads 1 of 9. A Loki session no longer sweeps the user’s pre-existing untracked files into its session commit. The session branch records a snapshot of the untracked, non-ignored files at the moment it is created (.loki/state/preexisting-untracked.z), and the session commit unstages exactly those paths, so agent-created files are still committed while the user’s own files stay untracked and untouched on disk. Before this, those files landed on the Loki branch and a later checkout of the base removed them from the working tree. The receipt no longer lists them as changed by the run. Filenames with spaces, newlines and glob characters are handled; if the unstage cannot run (git older than 2.25) the session commits nothing rather than sweeping. The static-analysis Python syntax check also stops writing __pycache__/*.pyc into the user’s repo, which the session commit used to pick up. P6.untracked-not-swept is promoted and a new case, P6.no-gate-artifacts-committed, passes. The ratchet checked both against the v9.52.0 baseline: 23 cases pending (was 24), 48 registered (was 45).

An inconclusive test result is no longer a pass on the default route’s Bun gate. runTestCoverage counts only pass: true as a pass; a missing, null, "inconclusive" or non-boolean value reads inconclusive, which is logged and recorded as such and never counted as affirmative (a zero-test project still reaches the council, per the #82 design). The bash council now names an unrecorded pass no_pass_recorded instead of no_tests_executed. New case P2.bun-inconclusive-not-pass.

The agent’s repo cannot supply modules to the council’s verdict. The council and the runner run inline Python from inside the agent’s repo, where a committed json.py (or a sitecustomize.py with an empty PYTHONPATH component) could print the verdict. All 43 inline Python sites in completion-council.sh and 27 verdict-bearing sites in run.sh now run python3 -E with the cwd removed from sys.path first (decision D7, which v9.52.0 applied to the verifiers). New case P2.council-readers-not-shadowed drives the real evidence gate and convergence reader over a failing result in a repo that commits both shadows.

Known gaps, next in line (docs/v10/BACKLOG.md 53-60): resuming a session does not re-snapshot, so a file the user creates between sessions can still be swept; gitignored user files can be swept when the agent rewrites .gitignore; an agent edit to an already-untracked user file is now neither committed nor listed in the receipt (before, the whole user file was committed); checklist verification and several council helper scripts still run unguarded inline Python.

v9.52.0

The moat is now an executable suite, and it says 0 of 9 proven. Loki’s v10 plan (docs/V10-VISION.md) rests on nine properties: portable proof, an honest verdict, the Wall, model freedom, sovereignty, in-place brownfield, no fabricated data, load-bearing proof and the Rule of Two. tests/moat/ turns each into cases that actually run, and bash tests/moat/run.sh reports the result. Today that is 45 cases, 21 passing and 24 failing, and 0 of the 9 properties fully proven. Nothing here claims otherwise.

A failing case is allowed only if it sits in tests/moat/pending.txt with the milestone that builds it. That list can only shrink: the runner reads it at every release tag reachable from HEAD and fails on any entry that was not pending at all of them, so parking a red case can never buy a green run. A second list, tests/moat/cases.txt, can only grow, so deleting a case is not a way out either. The suite also fails on a crash, a timeout, a script that emits nothing, a malformed or unregistered case, and a pending case that starts passing but was not promoted. It runs in the local fast tier and in a new “Moat suite” job in the Tests workflow, on macOS and Linux, with network egress blocked by the kernel (sandbox-exec on macOS, sudo unshare -n on the Linux runner) for the cases that claim offline behavior. Without a real block those cases fail; a proxy setting does not count. This release is the first to carry the lists, so it sets the ratchet’s baseline.

What is proven today, case by case: a signed receipt verifies offline with only a public key and fails on a different tree, an edited verdict field, a wrong key (including the victim’s key id over attacker key bytes), a stripped signature, and an empty or mistyped key argument, on both CLI routes; a model’s “looks good” cannot turn a failing check into a pass; unknown gates fail closed; the factory runs in an existing repo without moving it and produces a proof that verifies; and start, seal and verify complete with remote egress blocked.

What is not, with its milestone: signed verification metadata (M2), the Wall (M2), load-bearing ablation (M2), council approval on inconclusive evidence (M2), loki verify exit codes (v10.0.0, breaking), the model catalog, routing and seeded-defect corpus (M0), fabricated console data (M7), the Rule of Two (M3), doctor --airgap on the default route (M9), and pre-existing untracked files swept into the session commit (M2).

Verifier fixes found by building the suite and by nine review rounds.

Also: the tamper-claim scanner flagged the v10 build prompt’s own rule against that claim, which kept main red since the vision docs landed; the quickstart degradation test linked macOS Xcode shims that cannot run before the license is accepted; docs/INSTALLATION.md pinned the Docker example to 8.0.0 for about 150 releases and now uses :latest. requirements-test.txt gains cryptography and pyyaml: the Ed25519 signing tests skipped in CI without the first, and the Rule of Two workflow scan needs the second.

Open risks, recorded in docs/v10/BACKLOG.md: inline Python on runner and council verdict paths still runs with the agent’s repo importable (item 43); the Bun route passes a test result marked inconclusive (item 37); a PATH with an empty component lets a committed tool answer (item 48); one reviewer saw P1.modified-field-fails fail once in seven local runs, which 60 further runs under load did not reproduce. The v10 program state lives in docs/v10/.

v9.51.1

A completion member could vote COMPLETE with work still in flight. The council’s requirements_verifier counted unfinished work by reading pending.json alone. That was safe only by accident: nothing in the current runtime promotes a task out of pending, so the count never dropped while work was outstanding. The moment a work selector claims an item, pending goes to 0 and that member flips COMPLETE with the claimed task still running, plus anything sitting in blocked. Unfinished work is now the sum across pending, in-progress and blocked.

The guard is pinned by new Cases 6 and 7 in tests/test-completion-council-affirmative-evidence.sh, each verified RED before the fix so the test is known to be able to fail. Case 8 is the positive control: it proves COMPLETE is still reachable when every queue really is empty, so the fix cannot have passed by making the verdict unreachable.

That suite is now registered in scripts/local-ci.sh in both _FAST_KEEP and a run_check call site. It was in neither, so no pre-push gate ran this trust-core guard at all.

Docs. Adds docs/ENTERPRISE-SCALE-RESEARCH-2026-09.md, the six-desk enterprise research this work derives from.

The v9.51.0 tag was never published. Its release run failed required-ci because the council change left a dead pending assignment that shellcheck flagged as SC2034, failing the lint step inside Shell tests. The lint is fixed and the repo is clean at CI’s bar (423 passed, 0 failed); this is the same change, re-cut.

v9.50.4

Gate-stuck was the only terminal that told the user nothing. When the same quality gate failed for the same reason three times, the run stopped rather than grinding, which is correct. But those three exits (static analysis, mock integrity, mutation integrity) were the ONLY terminals in run_autonomous that never called emit_completion_summary. Every other one does, including the council force-stop. So they wrote no COMPLETION.txt, rendered no completion card, and sent no notification: a --bg user got no ping and nothing in the one file they are told to read.

All three now write the summary with their own outcome, each outcome has a literal label arm in both build_completion_summary and print_completion_card, and the three statuses joined the ENT-3 terminal-failure arm on BOTH routes, so exit 20 is classified by intent instead of falling through the arm that logs “crash, retryable”.

No PR is opened, deliberately. The council force-stop precedent bans the PR and mandates the summary in the same breath; this applies it rather than departing from it.

This also partly refutes ONE-RUN-AUDIT finding 2, which claimed the work “stays on the session branch for the user to find by hand”. commit_session_changes is commit-always and runs from main() after the 20, and print_pr_advice already prints the push and PR commands. The surviving defect was the missing summary, not a missing PR.

A failed PAUSED.md write was invisible (Bun route). handlePause writes .loki/PAUSED.md inside a bare catch {}. The write goes through atomicWriteFileSync -> withFileLockSync, which throws when it cannot acquire the per-target lockfile within LOCK_MAX_WAIT_MS (5s). Nothing re-writes the file afterwards – one write against four rmIfExists sites – so a swallowed failure was terminal and surfaced no error anywhere: a paused run simply had no notice file and no explanation.

Reproduced by holding PAUSED.md.lock and calling handlePause: the file is absent for the whole wait. Negative control with the lock free: present.

The catch is kept deliberately. Every consumer treats PAUSED.md as a human-readable notice (autonomous.ts::cleanStaleSignalFiles only unlinks it; run.sh:4900 only points the user at it), and a pause that HANGS on a contended lockfile is worse than a pause with no notice file. The caught error is now logged so the next occurrence names itself.

v9.50.3

A paused run could hang forever or resume on data nobody typed, and the Evidence Receipt it wrote was never announced. Both reach users here.

A non-interactive pause could hang, or falsely resume (#205). When a quality gate escalated to a PAUSE, the wait loop polled for a keypress. Off a TTY (--bg, a container, a CI job) that read can never succeed, so the run spun forever with nobody able to press anything. Worse, when stdin was a pipe or file carrying bytes, the read SUCCEEDED on the first stray byte and the next line removed .loki/PAUSE, silently resuming a run that a blocking gate had stopped. The keypress arm is now gated on an interactive stdin. The file-based escapes are untouched and still poll every second: measured off a TTY, removing .loki/PAUSE exits in 4 loops and touch .loki/STOP in 5. A fully unattended run still waits, which is deliberate; a bounded wait would invent a new terminal outcome and could fail a legitimate long human pause.

The pause banner and .loki/PAUSED.md both advertised “press Enter” where no key can be read. Both now state the truth per tty state.

The Evidence Receipt is now announced (#209). The receipt was already generated automatically and opt-out, but the only mention to the user was a single line. A user who had never heard of loki proof could finish a run without learning a checkable receipt existed. The end-of-run summary now names the receipt path, the deterministic verdict, and the re-check command. The verdict is the generator’s honesty.headline, never the council’s AI judgment. The page line is gated on index.html actually existing, because the generator writes proof.json and then renders the page unwrapped, so a render failure would otherwise name a page that is not there.

A run could advertise the previous run’s receipt (#211). .loki/state/last-proof-id.txt was never cleared at run start, so a run that died before generating a proof left the previous run’s id behind, and COMPLETION.txt printed that receipt under the heading “Evidence Receipt (this run):”. A receipt naming the wrong run is worse than no receipt: absence reads as “no data”, a stale one reads as evidence. The pointer is now cleared during run init, beside the existing stale-metrics reset that exists for the same reason.

The re-check command now works from any directory. loki proof verify <id> resolves the receipt from the run’s target dir, which need not be where the user is standing, so the printed command failed for exactly the user it invited to check the work. It now emits a cd-wrapped form when the cwd differs.

The run id is validated before use. The headline was sanitized and the id was not, so an interior newline or tab in the pointer split the tab-separated record and handed the consumers a field boundary that is not there. Ids are confined to [A-Za-z0-9._-] and rejected rather than repaired, since a pointer outside that alphabet is corrupt and a repaired id would name a directory that does not exist.

v9.50.2

Three fixes reach users here. The security fix landed on main after v9.50.1 was already published, so npm users are receiving it for the first time in this release.

A public placeholder was accepted as the JWT signing key. web-app/auth.py read PURPLE_LAB_SECRET_KEY from the environment and used whatever it found. The shipped documentation and compose files carry CHANGE_ME_TO_RANDOM_64_CHAR_HEX as the example value, so any deployment that copied the example and did not edit it signed its tokens with a string published in this repository. Anyone could mint a valid token. The placeholder set is now rejected and a random 64-char key is generated instead, with a CRITICAL log line. Verified by mutation in both directions: each placeholder is rejected, a real 64-char secret is not.

npm could publish a stale dist that Docker would catch too late. prepack rebuilds loki-ts/dist during npm publish, so a worktree assertion could pass while the PACKED tarball carried a different build. The publish-npm job now unpacks the tarball it is about to publish and asserts the dist inside it carries the version being released. Measured across five sampled release commits, one had a worktree dist lagging the tarball.

The MCP registry guard read a stale row and called a good publish drift. The registry keeps every published version as its own row and every row stays status=active. The guard iterated servers[], took the first name match and stopped, so it reported whichever row the API happened to order first. After 9.50.1 was published and flagged isLatest, the registry still ordered the 7.34.1 row first, so the guard reported drift against a registry that was already current. It now selects on isLatest rather than array order.

A guard that cannot see success is worse than no guard: a red nobody can clear trains everyone to ignore reds. Verified three ways rather than one, since a single green reading proves nothing here. Live (reads 9.50.1), negative control (a response with isLatest on the old version still selects the old version, so genuine drift is still detected), and positive control (reversing the live array still selects 9.50.1, so the result is order-independent).

v9.50.1

Two published SWE-bench figures were never retracted, though they measure the same broken counter that v9.42.0 retracted everywhere else.

generated_count incremented on any NON-EMPTY model_patch string, and the validator used substring tests ("---" in patch, "@@" in patch) that prose quoting a diff satisfies. Re-measuring the stored 300-instance run found 179 of 300 were prose, not diffs. Four instances of the resulting 99.67% figure were annotated in place in v9.42.0. Two were missed:

Both now carry a correction in place. No corrected figure is published, for the same reason v9.42.0 published none: producing one requires re-running the harness, and an estimate would repeat the original error of publishing a number nobody measured.

The HumanEval 98.78% figure was audited in the same pass and is SOUND. Recounted independently from the cited results JSON: 164 problems recorded, 162 with passed is True, and the strict count equals the truthy count, so no string-counting inflation is present. Every passed field is a real boolean. It is left unchanged and its “Self-reported” framing in README.md is accurate.

Also audited clean in this pass, with controls: both benchmark scorers gate passed_count on a real pass signal rather than truthiness (run-benchmarks.sh:463 on test_result["passed"] from actual test execution, :880 on problem_result["passed"]); a timed-out cell returns passed: False rather than vanishing, so it cannot inflate a rate by shrinking the denominator; and benchmarks/bench/equivalence_report.py deliberately handicaps Loki’s own signal so false-green cannot be biased in our favour.

v9.50.0

A quality gate was calling four real tests fake, and a guard nobody could clear was failing every push.

The mock detector’s source-import check missed two real forms

tests/detect-mock-problems.sh is a BLOCKING gate: run.sh invokes it with --strict, which exits 1 on CRITICAL or HIGH. Two legitimate import forms were invisible to it, so it reported the tests using them as having no source import at all.

Mutation-verified in both directions against this repository: CRITICAL went 14 -> 10 with HIGH unchanged at 2, and ZERO new findings appeared. The four that cleared are genuine source-importing tests (loki-dashboard-grid, loki-session-control-focus, oauth_dev, sdk_query_provider). --strict still exits 1 on the 10 remaining, which are a different shape.

A guard only a credential holder could clear was reddening every push

tests/test-mcp-registry-not-stale.sh compares the local VERSION to the live MCP registry. It was registered in run-all-tests.sh, which every CI shard runs on every push, and it exited 1 on drift. Closing that drift needs registry publisher credentials nobody working in the repo holds, so the red was unclearable. It is now ADVISORY by default: it still prints the FAIL line and still reports the drift, and exits 0. LOKI_MCP_REGISTRY_STRICT=1 asks for the hard signal, and only the nightly parity-drift job sets it.

Two defects surfaced while wiring that nightly job, both caught by controls: rc=$? could never run because GitHub runs steps with bash -e and a step’s own set -uo pipefail does not disable it, so the STRICT exit killed the step before rc was read (now || rc=$?); and the output had no consumer, so a separate reporting step was added that references only what the registry guard itself wrote. Verified end to end: the mechanism opened issue #198.

Documentation that described artifacts no code writes

Four .loki/ paths were named in skills/ and references/ with zero producing code, including a draft-07 schema and a cat ... | jq recovery command for .loki/state/circuit-breakers.json, a file that has never existed. Corrected to name the real artifacts: the three circuit breakers hold state in memory, the terminal queue is dead-letter.json, the memory index lives at the memory root, and the metrics actually written are efficiency/iteration-<N>.json, budget.json and trust-events.jsonl.

check-phase6-ready.yml also carried a disable reason that stopped being true in May (“issues disabled on repo”; they are enabled). The cron stays off for a durable reason instead: it gates “Phase 6 - Sunset of Bash” for a v8.0.0 release that shipped 168 tags ago.

Two security boundary suites were registered but never ran

test_tenant_isolation.py and test_oidc_rbac_mapping.py had a run_check call site and no _FAST_KEEP entry, so the fast tier deferred both and neither ran before a push. An auth-boundary suite no pre-push gate runs is indistinguishable from one that does not exist.

v9.49.4

Mostly a deletion release. 4,794 lines removed against 905 added, because the largest item is code that could never run: 38 web-app modules unreachable from the app entry point, including a complete deploy-connections page the router never referenced.

Fixed

Removed

Added

Changed

v9.49.3

Six fixes, four of them the same defect class: a READER reading a key or file that no WRITER produces. Each was invisible because the tests that should have caught it were written against the shape the reader wanted rather than the shape production writes.

Fixed

Added

Changed

v9.49.2

v9.49.0 and v9.49.1 both failed CI and published nothing: no tag, no npm release. This is that release, with both causes fixed.

Fixed

Known issue

doctor --json writes 108 bytes to stderr on a provider-less host while stdout is 5176 bytes of valid JSON. Non-blocking. docs/KNOWN-ISSUES.md records six tested and refuted hypotheses plus the strongest remaining lead.

Everything else is v9.49.0’s content unchanged: ten corrected client paths, a deploy-status route that no longer returns null, an API catch-all that returns JSON 404, two deleted surfaces that fabricated data, and the client/server route contract guard.

v9.49.1

v9.49.0 never published. Its Release workflow died in the gate job on a TypeScript error, so required-ci and every publish job were skipped: no tag, no npm release, nothing shipped. This is that release, corrected.

Fixed

Everything else in this release is v9.49.0’s content unchanged: ten corrected client paths, a deploy-status route that no longer returns null, an API catch-all that returns JSON 404 instead of telling users to restart the server, two deleted surfaces that fabricated data, and the client/server route contract guard.

v9.49.0

Every integration reaches real data, and the client can no longer drift away from the server.

Fixed

Removed

Added

Verification

The guard went RED first on all ten drift lines, then GREEN. A positive control reintroduced one drift and confirmed it went RED again, because a zero that cannot go non-zero proves nothing. Two mutations failed on different assertions. The deploy status and disconnect routes were driven live, including a path-traversal refusal. The catch-all was driven with both an SPA control and a real-route control. 157 server tests pass, and pk_live_ is confirmed absent from the rebuilt bundle.

Honest limit

The quickstart greenfield-rejection path could not be driven end to end: quickstart refuses non-TTY stdin, so its suite assertion and a mutation are the evidence there, not a live drive. That is stated rather than papered over.

v9.48.2

A flag that announced work it never did.

Fixed

What this does NOT do

This does not implement multi-repo orchestration, and the fix is careful not to imply otherwise. Real orchestration needs dependency ordering, per-repo gates, per-repo receipts, and a decision about partial failure (repo 2 of 5 fails: stop, or continue and report?). That work is tracked separately.

No behaviour changed. The migration that ran before is the migration that runs now. Only the claim about it is corrected.

The guard asserts the output is HONEST, not that orchestration works. If orchestration is built later, those assertions should be REPLACED by ones verifying every repository was migrated – not deleted to make room.

v9.48.1

Hand Loki a GitHub issue and get a pull request, with nothing installed. And two commands that used to contradict each other now agree.

Added

Fixed

A correction caught by CI, before anyone saw it

v9.48.0 was tagged in this repo’s history but never published – its required-ci job went red, so every publish job was skipped. The cause was mine, and it is worth naming.

The first version of the resume fix printed “Stopped at iteration 47 … It picks up from iteration 47” for a capped run. That is false. load_state resets ITERATION_COUNT=0 for max_iterations_reached and budget_exceeded, so a fresh loki start after a capped run is a NEW session from iteration 0. The message promised a continuation the runtime does not honour – the exact false-claim class this project exists to prevent.

tests/test-resume-discoverability.sh had guarded that direction since before this change and failed the build. It was in the deferred tier, so the local fast gate did not run it.

Capped runs now say plainly that they start a new session from iteration 0, and the “picks up from iteration N” line is printed only for a genuinely interrupted run, which is the one status that really does resume its count. The guard that let the wrong version through now asserts this too, with a positive control so it cannot pass vacuously (19 assertions, up from 16).

Honest limits

v9.47.0

A second repository the agent can actually read, plans that arrive whole, and the flake that blocked two releases.

Added

Fixed

Guards

tests/test-add-dir-reaches-provider.sh (7 assertions) and tests/test-queue-tasks-not-truncated.sh (6 assertions). Both drive the real code rather than asserting on source text, both carry a vacuity guard, and both are mutation-verified in both directions.

v9.46.0

The agent was handed a manual with half its pages missing.

Fixed

The portability trap this nearly shipped with

The obvious replacement is a single sed -E 's|(^|[^.])skills/|\1.loki/skills/|g'. BSD sed rejects it – RE error: parentheses not balanced – which fails SILENTLY on macOS and leaves every path unrewritten. It was caught by executing the function rather than reading it, and the guard now reproduces that exact mutation: substituting the -E form turns the suite red with “the rewrite is a no-op”.

Guard

tests/test-skills-references-copied.sh (6 assertions) EXECUTES copy_skill_files against the real repo rather than asserting on source text, and checks the thing that actually matters: every references/ path cited by a copied skill must resolve inside .loki/. It guards against vacuity by asserting the skills still cite references at all. Mutation-verified both ways: removing the copy goes red, and the BSD-incompatible sed goes red.

v9.45.0

The first artifact a user sees no longer misstates the product or ignores the user.

Fixed

Guard

tests/test-issue-prd-is-honest.sh (7 assertions) drives the real generator rather than asserting on source text, and checks BOTH directions: a body with a checklist must surface the user’s items and must not show filler, and a body without one must fall back AND label the fallback. Mutation-verified: restoring the hardcoded version goes red, and disabling extraction goes red.

Provenance

From a 71-agent research fan-out against primary sources (1,298 tool uses, 0 errors). Every claimed gap was adversarially verified: 61 claimed gaps -> 9 BUILD, 31 not worth it, 19 already built, 1 unsourced. An 85% kill rate is the point; most “missing” capabilities were already present.

v9.44.0

An MCP tool that was broken on every call that did real work.

Fixed

How it was found, and a correction I owe the record

A research agent proposed wiring a cross-repo graph merge. Verifying that proposal REFUTED it (the existing graphify CLI already merges cross-repo, and mcp/server.py:2566 already builds the merged path, so the wrapper would have been exactly the fix-shaped non-feature this project has shipped too many of) and uncovered this instead.

I then wrongly refuted the agent’s finding. I ran an AST check confirming a local import subprocess was present and concluded the tool worked. It was present because the agent had already patched the file; I validated the fix, not the bug. Checking the committed version showed the name in neither scope, and executing the original statement sequence reproduced NameError directly.

The lesson is specific and worth keeping: when verifying a claim about code an agent may have touched, check the COMMITTED version, not the working tree.

v9.43.0

The pull request is the deliverable. It no longer needs a flag.

Never published: required-ci failed with Tests: completed/cancelled at this SHA (run 34703144204), so release and every publish job was skipped. The Tests run was cancelled rather than broken – v9.44.0 pushed eight minutes later and superseded it – so nothing here was reverted or refixed; the content shipped in v9.44.0. No tag or npm version for 9.43.0 exists.

Changed

Why this release exists

Measured against this repo’s own history: 671 releases in eight months, 248 fix-shaped and 37 feature-shaped. The project has been repairing itself rather than shipping product. A separate measurement explains the retention gap that comes with that: 80 LOKI_* flags default OFF, and they gate the best parts of the engine – the auto-PR path, the simple-build fast path, the entire Bun/SDK route. A user installs, runs loki start, and gets the slow, unautomated version of a product whose good half is behind flags they have no reason to know exist.

npm downloads are healthy and accelerating (1,819 in a day against 4,923 in the week). The defect is not discovery. It is what happens after install.

This is the first of those defaults to flip. Each one is a separate release with its own guard, because a default that takes an ACTION is only safe while every condition around it holds.

Guarded by tests/test-auto-pr-default-on.sh (7 assertions). It checks the default AND each safety condition individually, so a future edit that loosens the auth check or the default-branch check fails here. Mutation-verified in both directions: reverting the default goes red, and removing the gh-auth guard also goes red.

v9.42.0

A benchmark number that counted strings, an unbounded spend default, and a supply-chain gate that did not exist.

Fixed

Added

Provenance

Found by a web-sourced research fan-out: 91 agents, 1,770 tool uses, 0 errors, 82 proposals across eight domains. Every proposal was independently verified against the codebase and its sources fetched. 27 BUILD, 22 ALREADY_BUILT, 27 NOT_WORTH_IT, 4 UNSOURCED. Only a third survived, which is the point: 22 proposals were things Loki already had, and 4 cited sources that did not support the claim.

v9.41.0

The upgrade that could not work, and the diagnosis that never fired.

The bug, as a user hit it

$ bun install -g loki-mode
installed loki-mode@9.39.0 with binaries: - loki
$ loki --version
Loki Mode v9.22.3
A newer Loki Mode is available: 9.35.0 (you have 9.22.3). Update: bun install -g loki-mode

Three different version numbers in four lines, and the advice is unfollowable: the user just ran that exact command. Reported from the field twice.

Cause: PATH shadowing. An older copy sits EARLIER on PATH than the one the package manager writes (typically ~/.local/bin/loki, symlinked into a Homebrew node prefix, ahead of ~/.bun/bin/loki). Every reinstall updates the copy that is not winning, so the user loops forever.

Fixed

Guard

Three tests in loki-ts/tests/util/update_check.test.ts, covering the two cases that were unreachable before (registry down, stale cache quoting a wrong version) plus a no-shadow case proving the ordinary nudge is unchanged. All 19 pre-existing tests in that file pass unmodified. Mutation-verified: putting the registry gate back in front of the shadow check turns the registry-down test red, which is exactly the production symptom.

Why this took two releases to get right

v9.36.0 fixed the wrong half. It made the message better while leaving it behind a gate that the failing case never passes, so the improved message could not reach the users who needed it. A fix that is correct but unreachable is not a fix, which is the same defect class as the reachability audit in v9.39.0.

v9.40.0

Authored reviewer personas now reach the prompt, and a shipped test that could silently invert itself.

Fixed

Guard

tests/test-reviewer-persona-reaches-prompt.sh (6 assertions) checks the persona is carried AND that the focus constraint survives AND that an absent persona is byte-identical to before. It guards against vacuity by asserting that agents/types.json still carries personas at all, so it cannot pass while protecting nothing. Mutation-verified both directions.

How the second fix was found

By writing the same trap twice. The SIGPIPE inversion was already recorded in this project’s memory, and it was written into a new test anyway, where it broke four assertions against code proven correct. Passive memory did not intercept authoring, so the rule moved into a skill that runs as a checklist WHILE a guard is written. Applying that checklist to already-shipped tests found the live instance above. A rule that only fires after the failure is not a control.

v9.39.0

Three capabilities the product advertised and could not actually perform.

Fixed

Guard

tests/test-provider-arm-coverage.sh (12 assertions) asserts each provider arm INDIVIDUALLY at each dispatch site, never a count – a count cannot say WHICH provider vanished. It also guards against vacuity: if opencode ever stops being selectable, the suite says so rather than passing over a provider nobody can choose. Mutation-verified in both directions: removing the opencode arm or the aider arm each goes red, restoring goes green.

This is the cross-cutting-registration defect that broke main for three releases when the fifth provider shipped. Nothing enforced that a new provider reached every dispatch site; now something does.

Provenance

Found by a 64-agent reachability audit (1,081 tool uses, 0 errors) asking one question per capability: can a real user actually reach this? Every claim was adversarially refuted by three independent skeptics with distinct lenses before it counted. 19 claims -> 9 confirmed, 10 refuted (53%).

The refutations are the point. Three separate prior “findings” died on inspection this round and the record has been corrected: the policy engine is reachable (the writer is a REST endpoint, dashboard/api_v2.py:791, not a CLI command); mergeability_score.py is an operator-run benchmark tool working as documented, not a dead gate; and “41 agent types documented vs 5 that run” compared a prompt-role catalogue against the review council, two unrelated subsystems – references/agent-types.md:9 already says the 41 are “prompt-defined specifications … not separate processes”.

v9.38.0

Four receipt fields that degraded to values indistinguishable from success.

Fixed

Not fixed, and why

wall_clock_sec returns 0 on unparseable timestamps and on a negative delta, so “could not measure” would be indistinguishable from “no time elapsed”. It is left OPEN-UNREPRODUCED rather than fixed: the real artifact reports 396 correctly against its own timestamps, and neither collapse path has been observed. Changing a JSON contract and the HTML renderer that reads it, on a defect that cannot be demonstrated, is the speculative work this program exists to avoid.

Provenance

These are 4 of 9 confirmed findings from a 34-agent receipt-integrity audit (606 tool uses, six evidence surfaces in parallel). Every candidate was adversarially refuted by an independent agent before counting: 28 candidates -> 9 confirmed, 19 refuted. That 68% refutation rate is the point – nineteen plausible-looking findings would have become false claims about our own product. With v9.37.0’s HIGH chain, 7 of 9 are now fixed.

Guarded by tests/test-static-analysis-noop-not-pass.sh, which now carries 10 assertions across all four fixes plus the v9.37.0 pair. Each fix was mutation-verified in both directions: reverting it goes red, and over-correcting goes red too.

v9.37.0

A gate that scanned nothing no longer reports a pass, and the receipt says NOT VERIFIED when nothing was verified.

Fixed

How this was found

A receipt-integrity audit ran 34 agents across six evidence surfaces of proof.json, asking one question per field: is this populated by an independent measurement, or can it silently degrade to a value a reader would interpret as success? Every candidate finding was then adversarially refuted by a separate agent.

28 candidate findings -> 9 confirmed, 19 refuted. The refutations were substantive (“the explicit unmeasured marker EXISTS”, “cosmetic, no reader could be misled”), and that 68% refutation rate is the point: nineteen plausible claims about our own receipt did not survive contact with the source.

The three HIGH findings were all the same defect and are all closed by one change. One MEDIUM finding (unit_tests) turned out to be the same defect again and is fixed here too. Five MEDIUM findings remain and are being worked in order.

Honest limits

v9.36.0

The most expensive step in a build now reports how long it took.

Added

Fixed

Why this was worth a release

Because the alternative is what I did first: I inferred the cost profile from artifact mtimes and got it wrong.

Reading a 58s gap between two file timestamps, I attributed it to a specific step. That inference is invalid: an mtime records WHEN a file was written, not how long the step that wrote it took.

Then I compounded it. Re-measuring the suspected step directly gave 0.09s, so I retracted the claim – but that measurement was taken against a project whose artifacts already existed, so a cache short-circuited the work. I had measured the cache-hit path and reported it as the cost of the step.

What finally settled it was watching a live run with ps: eight concurrent claude processes, with several steps in flight at once. The run is not a sequence of additive phases, so per-phase attribution that assumes serial execution is measuring overlapping work no matter how carefully the timestamps are read.

That is the case for emitting the number. A stage_complete record brackets a real start and end; a profile assembled from file timestamps is archaeology, and one assembled from a cache hit is worse.

Honest limits

v9.35.0

When the model you pinned is not the model that ran, the receipt now says so.

Added

What this release deliberately does NOT do

Honest limits

v9.34.1

v9.34.0 did not publish: its own new test failed CI, so Release refused. The gate worked. This ships the same content plus the fix.

Fixed

v9.34.0

The buyer’s verification command gets a front door, and post-release smoke stops failing with no diagnostic.

Added

Fixed

Added

Honest limits

v9.33.0

Code that ships to every user and that nothing can reach. This release does not delete it; it makes the repo unable to acquire more of it silently.

Fixed

Added

Honest limits

v9.32.0

Who did it. An audit trail that cannot name an actor is not an audit trail, and a role label that enforces nothing is worse than no label.

Fixed

Added

Honest limits

v9.31.0

Two defects that together meant the policy engine, which is real and correct, almost never ran. This is the enforcement-before-execution capability Factory sells to regulated buyers; ours was built and unreachable.

Fixed

Added

Honest limits

v9.30.1

A doc correction and a guard on behaviour that already works. No product code changed – and the reason it did not is worth recording, because I nearly shipped a fix for a defect that did not exist.

The investigation that produced no code change

Specialized agent roles are the axis Factory.ai competes on, so the claim was worth checking. agents/types.json ships 41 role definitions and the specialist loader in autonomy/run.sh reads LOKI_AGENTS_TYPES_FILE. A grep showed that variable being set only by tests, and a probe that ran the selector standalone showed the reviewer pool going from 4 to 14 when the file was supplied.

That looked conclusive: 41 shipped roles never loading for any user. It was wrong. autonomy/run.sh:14764 already exports LOKI_AGENTS_TYPES_FILE="${PROJECT_DIR}/agents/types.json" unconditionally, ten lines above the selector in the same function. The roles load today. My probe omitted the export the real code performs, so it measured the absence of something I had removed myself.

It was caught by mutation testing before anything shipped: a mutation that repointed the default at a nonexistent file stayed green, which meant the assertion was not attributing. Chasing that produced the real export. A mutation that fails to go red is information, not a nuisance.

The redundant change was reverted. What remains is the guard.

Fixed

Added

v9.30.0

v9.29.0 said the audit chain is not tamper-proof and named the fix: a witness. This release wires it, and closes the larger half of the gap that wiring alone would have left open.

Fixed

Added

Honest limits

v9.29.0

The audit chain is not tamper-proof, and we were saying it was. This release corrects the claim, publishes the threat model with a reproduction, and adds a guard so the overclaim cannot come back.

This is the first release of a competitive program aimed at Factory.ai and 8090. Both sell governance: Factory through certification (SOC 2 Type II, ISO 27001, ISO 42001), 8090 through a governed workspace with an end-to-end audit trail. Both ask the buyer to trust the vendor. Our wedge is the opposite – evidence the buyer verifies without trusting us – and that wedge is worth nothing if our own integrity claim does not hold. So it gets checked first.

Fixed

Added

v9.28.1

Four test suites that had been failing or silently not running, plus the test behind v9.28.0’s one unbacked MEASURED label. No product code changed – every fix was a test asserting something about its environment that was not true, which is the same defect class v9.28.0 shipped to correct, found this time in our own suites.

Added

Fixed

Changed

v9.28.0

Four defects, all the same species: the product asserting something it had not checked, or documenting a control it does not enforce. Found by an evidence-first research pass against Factory.ai and 8090, then verified against source before any code moved.

Fixed

Added

Changed

v9.27.3

Fixed

v9.27.2

Fixed

v9.27.1

Added

v9.27.0

Added

Fixed

v9.26.3

Fixed

Added

v9.26.2

Committed and pushed but never released: required-ci failed with Tests: completed/failure at this SHA, so every publish job was skipped (run 34432149958). The cause is the environment-conditional yq assertion corrected in v9.26.3 above. No tag or npm version for it exists.

Fixed

Changed

v9.26.1

Fixed

Note on v9.26.0

v9.26.0 was committed and pushed but never released: a ShellCheck warning failed the Tests workflow, so the release gate correctly refused to publish. No v9.26.0 git tag, GitHub release, or npm version was ever created – verified against both git ls-remote --tags and the npm registry. Its changes ship here in 9.26.1. The entry is folded in below rather than left standing, because a changelog entry for a version nobody can install is the same class of defect as a receipt that reports a stage it never ran.

Added

Changed

v9.25.2

Fixed

v9.25.1

Fixed

Changed

v9.25.0

Changed

Neither doc-gen change is visible in a log: the documents generate either way, so no gate goes red and only a bill shows the difference. Both are guarded.

v9.24.0

Added

Changed

v9.23.1

Fixed

v9.23.0

Committed and tagged locally but never released: required-ci failed with Security Audit: completed/failure at this SHA, so release and every publish job was skipped (run 34395800201). The audit failure is the fast-uri advisory set fixed in v9.23.1 below, which is the version that reached npm. No remote tag or npm version for 9.23.0 exists.

Fixed

Changed

v9.22.13

Tagged but never published to npm: gate, required-ci and release all succeeded and the tag was pushed, so no test or gate blocked it. Both npm publishing jobs then failed on the registry PUT (run 32751584247): publish-npm with npm error 404 Not Found - PUT https://registry.npmjs.org/loki-mode, and publish-ts-sdk with the same E404 on .../loki-mode-sdk, while publish-docker and publish-python-sdk succeeded. v9.22.14 did not publish either; v9.24.0 is the next version on npm.

Added

Fixed

v9.22.12

Fixed

v9.22.11

Added

v9.22.10

Added

v9.22.9

Added

v9.22.8

Added

Fixed

v9.22.7

Fixed

v9.22.6

Added

v9.22.5

Added

v9.22.4

Added

Fixed

v9.22.3

Added

Fixed

v9.22.2

Added

Fixed

v9.22.1

Fixed

v9.22.0

Source-bound measurement closes the outcome-routing canary loop without invoking or switching a provider.

Added

Fixed

v9.21.0

Four features from the parallel lanes plus one post-release fix to v9.20.0’s lineage guard.

Added

Fixed

v9.20.0

The Evidence Receipt becomes portable. A receipt from any build – laptop, CI, or cluster – can now prove WHO produced it to someone holding only proof.json and a public key set: no API token, no gpg key import over a side channel, no network, and no Loki install. That closes the gap that made receipts checkable only by people who already had our infrastructure.

Two security fixes ship alongside it, and both were reachable in production.

Added

Fixed

Performance

Measurement

v9.19.1

Ships v9.19.0’s payload, whose release was blocked by two CI failures of my own making: a ShellCheck warning in a new test, and a suite whose failing assertion moved between adjacent cases run to run.

Fixed

Added

Closes the spec-to-production loop. Two gaps, both of which broke the promise of an end-to-end autonomous lifecycle.

Added

Re-release of v9.18.3, whose release was blocked by a FALSE test failure of my own making: a helm test guarded on helm and python3 but not on PyYAML, which is not stdlib. Without it every manifest parse raised, the redirect swallowed the traceback, and four assertions compared against empty strings – reporting “the scaling knob is not wired” about a chart that renders correctly. It now skips with a named reason, and fails loudly if helm template cannot render at all rather than blaming the knob for a chart-wide failure.

No product change from 9.18.3. The two false-green fixes below are the payload.

Fixed

Both were found by auditing delivered work rather than by a failing test, and the second was more serious than the first: the first was a wrong default, the second was a privilege boundary.

Third attempt to publish the session-killer fix. 9.18.0’s release was blocked when its Tests run was cancelled by later pushes; 9.18.1’s gate failed because the pushed commit contained an intermediate state where a test referenced a method that did not exist yet. Both were my sequencing errors, not defects in the fix. No code change from 9.18.1 beyond the receipt-verdict hardening below.

Fixed

Re-release of v9.18.0, whose release job was blocked when its Tests run was cancelled by subsequent pushes to main (required-ci correctly refuses to treat cancelled as a pass). No code change from 9.18.0 beyond the additions below.

Added

Fixed

Added

v9.17.2

Ships the tree v9.17.0 and v9.17.1 were both meant to. 9.17.0 on npm carries gitHead 00b9f4e2 (an older commit, published by a concurrent release run) so it lacks the fixes its changelog describes; 9.17.1 was blocked by required-ci while shell shard 2/4 was red. That shard now passes. No code change beyond the test fixes below.

Fixed

v9.17.1

Re-release of v9.17.0. The 9.17.0 tarball on npm was published from an older commit (gitHead 00b9f4e2) by a concurrent release run, so it did not contain the fixes its changelog described – verified by grepping the published tarball with a positive control. No code change; this ships the intended tree.

v9.17.0

Fixed

Added

Changed

v9.16.0

Completes the 10-item competitive roadmap.

Added

v9.15.0

Added

Fixed

v9.14.0

Added

v9.13.0

Added

v9.12.6

Added

Fixed

v9.12.5

Fixed

v9.12.4

Fixed

v9.12.3

Security

v9.12.2

Security

v9.12.1

Fixed

v9.12.0

Dashboard honesty

The dashboard reported things it had not measured. Four fixes, each verified by mutation testing:

Operator API and CLI parity

Security

Fixed

v9.11.0

Two verification tools, released as features rather than as a fix

v9.10.1 committed receipt-stats.py and receipt-verify-batch.py to unbreak collection. They are user-facing capabilities, and this is what they do.

receipt-stats.py summarises an archive of receipts:

2 receipt(s): 2 FAILED. Cost: total $2.5000, median $2.5000 across 1 measured
receipt(s). 1 receipt(s) EXCLUDED from both figures: cost was never measured,
and summing an absent measurement as 0 would understate the total and drag the
median down.

The exclusion is the feature. With the unmeasured receipt summed as 0 the median reads $1.25 instead of $2.50 – a 2x understatement of real spend, produced by a number nobody measured. An empty archive exits 3: zero receipts is not a clean archive.

receipt-verify-batch.py verifies an explicit LIST, so CI can check exactly the receipts a PR touched rather than walking a whole workspace.

A path that does not exist is NAMED and exits 66, never silently skipped – a batch that quietly drops an input reports a stronger claim than it earned. An empty list exits 3: verifying nothing is not a pass. The rollup is weakest-link, so one FAILED sinks the batch; “9 of 10 passed” is not a verdict an auditor can use.

Both reuse verify() and record_is_measured() rather than restating them. 34 tools now ship, each asserted individually in the packaging guard.

v9.10.1

A test shipped without the tool it tests, and every Python job died

v9.10.0 staged with git add -A tests/, which swept in test_receipt_stats.py and test_receipt_verify_batch.py. Their tools were UNTRACKED, so the tests were committed and the tools were not:

ERROR collecting tests/test_receipt_stats.py
E  FileNotFoundError: tools/receipt-stats.py

pytest fails at COLLECTION, so all four Python versions died before running a single test – 2635 items collected, 2 errors, zero executed.

Both tools exist and work: 66 tests pass once they are present. The fix is to commit them, not to delete working code.

git add -A <dir> is how this happened

A directory-wide add cannot tell a file that belongs in this commit from one that happens to be sitting there. Every other release in this session staged files BY NAME, which is why this is the first time it bit.

Both tools are now in tests/test-runtime-libs-are-packaged.sh, which asserts each shipped artifact individually – so a tool that goes missing from the package is named, rather than inferred from a count.

v9.10.0

Two security controls were documented that do not exist

These are the sharpest findings of the docs sweep, because a reader believed they had a control they did not have.

Egress policy. docs/network-security.md presented LOKI_NETWORK_EGRESS_POLICY, LOKI_ALLOWED_HOSTS and LOKI_BLOCK_METADATA_ENDPOINT as live configuration. None is implemented. Anyone who set them had no application-level egress control at all. wiki/Network-Security.md already said “Planned”; docs/ contradicted it.

Metrics exposure. docs/metrics.md implied /metrics could be turned off. It is an unconditional route (dashboard/server.py:9637) with no auth scope dependency, unlike /api/*. LOKI_METRICS_ENABLED=false never closed it.

A 572-line document for a feature that does not exist

docs/openclaw-integration.md documented seven CLI subcommands, REST endpoints, six env vars, a config block and Prometheus metrics.

grep -c openclaw autonomy/loki   ->   0

What exists is integrations/openclaw/bridge/, whose own docstring says “This is a foundation/skeleton. The WebSocket gateway client is not yet implemented.” Replaced with an accurate 52-line stub quoting that limitation.

The rest, all verified against source

Verified and left alone: 34 MCP tools, 41 agent roles across 8 domains, 8 quality gates. docs/EVALUATING.md was already exemplary, scoping its competitive claim to six installed CLIs and explicitly excluding Devin and Replit.

The tool that finds this class

tools/audit-docs.py scans markdown for checkable claims and verifies each against the repo. On this repo, at the moment it shipped:

docs scanned:  474
claims checked: 2205  (false 178, passed 1949, uncheckable 78)

Seventy-eight claims read UNCHECKABLE, not passing. A claim the tool cannot verify is reported as unverified rather than counted as correct, which is the same absent-is-not-zero rule the cost surfaces enforce.

docs/TOOLS.md is new: all 34 tools, grouped by what a user is trying to do, with every example verified by running it.

v9.9.0

Main is green: five shard failures, all of them my own guards lying

Every one was a false alarm in a test, not a product defect. That is worse than it sounds – a guard that cries wolf is a guard nobody reads.

1. SIGPIPE (v9.7.0). printf | grep -qF had grep exit on first match and close the pipe, so the FIRST library in the loop was reported MISSING while shipping fine.

2. stat ordering (v9.8.0). macOS-first at four call sites; the portability guard exists to catch exactly that before it bites on a Linux runner.

3. Caveman moat audit (v9.8.0). Flagged LOKI_PROVIDER=claude as an unnamed claude subcall. It is a variable ASSIGNMENT, and the exclusion list already exempted every other way this file names claude as a value.

4. jest (v9.8.1). My own version bump string-replaced 9.7.0 into "jest": "^29.7.0", producing a version that never existed. npm install died with ETARGET and ALL ELEVEN JOBS failed before running a test. The bump now edits the JSON field.

5. pre-push scoping. The test stubs bin/python3; the hook now prefers python3.12 (v8.63.0), so the stub was never invoked and recorded no argv. Four assertions read as failures while the hook worked correctly.

Three trust-core probes were blind, and one hid a real hole

The detector reported the test PASSED with the invariant broken -- it is blind three times.

Two were ambiguity. return 20 now appears twice, so the probe mutated static_analysis while the test drives mutation_integrity. And the JSON compare moved from python3 -c to a heredoc, so its search string matched nothing. Both re-anchored on unique, verified strings.

The third was a genuine coverage hole. The wiring test asserted that an incoherent cost fails ok – but its fixture used a deadbeef hash, so hash_ok was ALREADY False and ok was False regardless. Deleting and result["cost_coherent"] is not False from the verdict left the suite GREEN: a receipt claiming $0.00 as measured would have verified clean.

The fixture now carries a genuinely valid hash, computed the way the verifier computes it, so only the cost gate can fail it. Deleting that line now turns the suite red.

Trust-core detector: 95 invariants, 0 blind.

What actually kept main red

Not the defects – each took minutes. It was releasing faster than a Tests run completes, so every run was cancelled by the next push and none ever reported. Two of the five do not reproduce on macOS at all, so the local gate stayed green throughout. A cancelled run is an absent measurement.

v9.8.1

My own release script took down all eleven CI jobs

The v9.8.0 bump did a blind string replace of 9.7.0 -> 9.8.0 across package.json. That string also lives inside a dependency spec:

"jest": "^29.7.0"   ->   "jest": "^29.8.0"

jest 29.8.0 has never existed – 29.7.0 is the highest 29.x. npm install failed with ETARGET, so every job died before running a single test: four shell shards, every Bun matrix entry, every Node version.

The three red shards before this were genuine test failures, now fixed. This one was pure collateral from the release process itself, and it was worse: it turned three specific failures into eleven wholesale ones.

The bump now edits the version FIELD, not the file text

json.load, set ["version"], json.dump. A blind replace cannot tell a project version from a dependency’s, and on this release it could not tell a real version from one that does not exist.

tests/test_version_bump_safety.py asserts the property that catches the CLASS: every exactly-pinned dependency must resolve to a published version. It skips, and says so, when npm is unreachable – an absent measurement is not a pass. Restoring ^29.8.0 turns it red.

An audit of every other pinned dependency found no further collateral.

v9.8.0

The remaining two red shards, both false alarms in my own guards

v9.7.0 fixed the SIGPIPE one. These are the other two, and neither was a product defect.

stat portability. tests/test-help-and-log-cap.sh tried stat -f%z (macOS) before stat -c%s (GNU) at four call sites. On a Linux runner the macOS form fails, falls through, and works – so the code was not broken, but the ordering is a latent trap and the portability guard exists precisely to catch it before it becomes one. All four are now GNU-first.

The caveman moat audit. It flagged autonomy/run.sh:1643:

LOKI_PROVIDER=claude

as an “unsuppressed parsed claude subcall”. It is a variable ASSIGNMENT, not a subcall – the auto-detect fallback added in v8.64.0, which keeps the historical default so the “not installed” error names an actionable provider rather than an empty string.

The audit’s exclusion list already exempts every other way this file names claude as a VALUE (--provider, Provider:, (claude default), supports the claude). This form was simply never added. The fix belongs in the exclusion, not in production code.

Widening a moat guard requires proving it still bites

The exemption is one literal string. Replacing that exact line with a genuine unnamed subcall:

claude --print "leak"

still fails the audit, verified by mutation probe. The guard was narrowed by one false positive, not blunted.

What actually kept main red

Not these three defects – they were findable in minutes. It was releasing faster than a Tests run takes to finish, so every run was cancelled by the next push and nothing ever reported a verdict. Two of the three failures do not reproduce on macOS at all, so the local gate stayed green throughout.

v9.7.0

Three CI shards were red for a packaging failure that did not exist

tests/test-runtime-libs-are-packaged.sh: line 131:
  printf: write error: Broken pipe
FAIL: tarball is MISSING autonomy/lib/proof-verify.py
PASS: tarball contains autonomy/lib/efficiency_cost.py
PASS: tarball contains autonomy/lib/cost-summary.py

proof-verify.py ships. Every other library in the same list passed against the same listing. Only the FIRST one failed, because printf '%s' "$_listing" | grep -qF has grep exit on its first match and close the pipe, so printf takes SIGPIPE and the pipeline reports failure.

A checker that fabricates a MISSING is worse than one that fabricates a PASS. It burns trust in the only guard watching the shipped artifact, and the next real miss reads as another false alarm. This is the guard that has caught two genuine packaging breaks.

grep now reads from a here-string, so nothing writes into a pipe grep may close early. Same assertion, no SIGPIPE surface.

It does not reproduce on macOS, which is why it survived

Verified directly: with the real 36,421-byte npm pack listing, the old form produces NO error locally. The failure is Linux-specific, so a green local gate was compatible with three red shards for hours.

Same class as the Python 3.14-vs-3.12 divergence in v8.63.0: the local gate and CI were not running the same thing, and the local green was not evidence.

The honest note on how long this stood

Releases were going out faster than a full Tests run takes to finish, so each push cancelled the previous run. The last two Tests runs before this one were CANCELLED, not passed. The 2,539 passing tests reported alongside those releases were the pytest suite; the shell suite that was failing is DEFERRED by the fast tier.

A cancelled run is an absent measurement. Reading it as a pass is the same error this codebase has spent sixteen surfaces removing, applied to my own release process.

v9.6.0

Four gates were exempt from the strictest rule in the repo

_is_gate() matched only the SUFFIX form, "-gate." in name. Every prefix-named tool escaped:

gate-status.py   gate=False
gate-log.py      gate=False
gate-explain.py  gate=False
gate-badge.py    gate=False
ci-gate.py       gate=True

Only ci-gate.py was covered. The other four were silently classified as advisors and exempted from the green-leak check – the rule that stops a tool exiting 0 while its own output says it could not evaluate.

All four already comply, verified by running each against a blind workspace (exit 2, 64, 64, 64; never 0). So this closes a COVERAGE gap rather than a defect, and that is exactly why it was worth closing: an exemption nobody noticed is one that is still there when a tool eventually stops complying.

The guard restated the rule it was meant to guard

The membership test hardcoded the same suffix-only pattern:

if "-guard." in tool.name or "-gate." in tool.name:
    self.assertTrue(_is_gate(tool.name), ...)

So it was blind to prefix-named tools BY CONSTRUCTION. A guard that reuses the predicate’s own rule cannot detect a gap in that rule – it agrees with the bug.

The first mutation probe proved it: reverting the pattern SURVIVED, because nothing asserted the membership independently. The expectation is now derived from the tool NAME rather than from _is_gate, plus a second test naming all seven gates explicitly. A count would pass while silently losing one.

Reverting the pattern now turns the suite red.

On the verifier that found it

A wave-D verifier flagged this as a one-tool judgement call about gate-status.py. Checking it directly showed FOUR tools affected, not one, and the guard itself was the reason none had been caught. The report was right to raise it and wrong about the scope, which is why the flag was checked rather than filed.

v9.5.0

Is the merge gate actually set up, and does the badge tell the truth

gate-status.py answers “is this repo’s gate real?” in one screen, composing the existing tools by subprocess rather than reimplementing any of them.

would_run      PROBLEM   no: the policy does not load, so ci-gate would run with
                         nothing to enforce, which is green without checking

GATE: UNKNOWN -- the merge gate cannot be fully verified from here, so it is
                 not known to be working
exit=2

UNKNOWN is the state the file exists to keep. A component that cannot be checked is neither OK nor a failure, and the overall verdict is weakest-link, never a count.

gate-badge.py renders the live verdict for a README:

{"schemaVersion":1,"label":"gate","message":"unevaluable","color":"orange"}

A badge is read at a glance, so a blind gate showing green is the worst possible rendering of that state. UNEVALUABLE gets its own colour, and recolouring it green turns the suite red.

A tool was built, tested green by its author, and dropped here

cost-forecast.py reported “26 passed” in its own worktree. Integrated here it failed 6 tests and crashed outright on an empty history:

AssertionError: 0.0 is not None : zero recorded runs produced a forecast;
absent is not zero
AssertionError: 0.0 is not None : three unmeasured runs were averaged as 0 and
projected; that is the $0.00-means-free lie pointed at the future
TypeError: must be real number, not NoneType

Its own tests were correct and caught it; the reported green did not match this tree. Stale bytecode was ruled out by clearing __pycache__ and re-running.

It is NOT in this release. A cost forecaster that projects $0.00 from zero measured runs is the exact defect this codebase has spent sixteen surfaces removing, aimed at a number someone would budget against. Shipping it because three of four were ready would have been the worst trade available.

v9.4.0

Four tools, and the same rule holds in all four

gate-log.py         verdict history: how often does the gate block, on what
receipt-export.py   one evidence file a third party can check
cost-attribute.py   where a run's cost actually went, per stage
gate-explain.py     a verdict a human can act on

Each was built in an isolated worktree, then handed to an adversarial verifier that re-ran its mutation probe BY HAND to confirm the failure was a clean assertion rather than a traceback. All four probes were then re-run independently here; all four bind.

A blind gate never reads as healthy

$ ci-gate <blind run> --json | gate-log record | gate-log report
  pass                       0
  unevaluable (NOT a pass)   1
exit=2

gate-log keeps UNEVALUABLE in its own bucket, and blind OUTRANKS failed in the rollup – the same precedence ci-gate uses, because “your gate is partly blind” is worse news than “this run breached”. Collapsing it into pass makes the tool loudest, a confident green, exactly when its instrumentation broke.

gate-explain states the third state in words:

POLICY: cost [UNEVALUABLE]
  CHECKED: the gate could not check this, so it is neither a pass nor a
           failure -- the axis is unverified

Defects the builders found in their OWN code before reporting

A 100%-corrupt log printed “most failing policy: none – no FAIL in any readable record”. Nothing was read, so that was an unmeasured value claiming to be a measured none. It now prints UNKNOWN, with a test.

A stored "corrupt" state was double-counted against the real unreadable-line tally, so the buckets no longer summed to the record total.

An unreadable log path escaped as a traceback and exited 1 – “checked and FAILED” under this convention, when the truth is 2 (could not check).

The contract adopted all four without a violation

tests/test_tool_exit_contract.py globs tools/*.py, so these four were subject to the exit convention from their first run: --help exits 0 without a verdict, a nonexistent path never exits 0, and no tool exits 0 while its own output says it could not evaluate. Six contract tests pass with the new tools adopted.

gate-log deliberately uses exit 64 for usage errors rather than argparse’s default 2, so a typo can never be mistaken for a blind gate.

v9.3.0

A policy edit now shows its safety DIRECTION, not just its diff

Once a gate policy is in version control, the dangerous change is a LOOSENING. Raising a ceiling or dropping require_receipt weakens the gate, and inside a large PR it looks like any other one-line JSON edit.

$ policy-diff.py old.json new.json --fail-on-weaken
policy-diff: 2 weakening(s) require an explicit human ack
WEAKENS: max_usd: 5.0 -> 50.0
WEAKENS: require_receipt removed (was true) -- this axis is no longer enforced at all
exit=1

A tightening passes:

TIGHTENS: max_usd: 5.0 -> 2.0
exit=0

A diff that only reports max_usd: 5 -> 50 makes the reviewer do the safety reasoning. Doing it for them is the entire value.

Removing a key is treated as the most dangerous edit, not as a neutral “removed”: the gate then stops enforcing that axis completely, and the message says so.

An unclassifiable change reads UNKNOWN DIRECTION, never “unchanged” and never assumed safe – an unrecognized key is exactly where a silent loosening hides. Both files must be VALID policies first, because diffing garbage produces a confident nonsense verdict.

An exit-code collision worth recording

tests/test_tool_exit_contract.py globs tools/*.py and auto-adopts any new tool, so policy-diff.py had to obey the convention from its first commit. argparse defaults a usage error to exit 2, which in this repo means “could not be checked” – a usage typo would have been indistinguishable from a blind gate. ArgumentParser.error() is overridden to exit 64, leaving --help’s exit-0 path intact.

66 (file absent) and 2 (present but invalid) are separated by an explicit existence check, since policy-load collapses both into one error.

A requirement collision, resolved rather than papered over

Requirement 2 (an unclassifiable key reads UNKNOWN) collides with requirement 5 (both files must be valid): policy-load rejects unknown keys, so an unclassifiable key can never reach the diff through a file. Handling it at the parse layer would be untestable dead code.

Instead the direction table defaults to UNKNOWN and is tested by calling classify() directly, plus a guard that walks the real policy_load.KNOWN_KEYS. That catches the REACHABLE version of the bug: KNOWN_KEYS growing while the direction table does not.

v9.2.0

The exit-code convention is now enforced, and it found a third violation

Twenty-five tools compose in CI: ci-gate maps child exit codes, shell chains use &&, gate-report re-emits a verdict. The convention that emerged – 0 passed, 1 FAILED, 2 could not check, 3 nothing to check, 64 usage, 66 input missing – was enforced nowhere.

Two violations had already been found one at a time (model-advisor exiting 0 on a nonexistent workspace in v8.96.0, receipt-attest judging --help as a proof path in v8.95.0). Written as a CONTRACT over the real tools/ directory, it immediately found a third:

AssertionError: estimate-run.py reported success for a path that does not exist

preflight.sh consumes that tool, so a mistyped or unmounted path silently became “NO BASIS: no measured, priced iteration to project from” instead of an error. Now exit 66, while a REAL workspace with no history still exits 0.

The distinction the rule had to make: gate versus advisor

A GATE exiting 0 while its own output says it could not evaluate is a green-leak – CI reads the code, sees success, merges on an unchecked axis. That is the defect this whole tool line exists to prevent.

An ADVISOR is genuinely different. model-advisor answering “NO BASIS” HAS answered honestly: there is no recommendation to make, the output says so, and no gate consumes its exit code. Forcing it non-zero would train operators to ignore a failing advisor, which is worse than the gap.

So the green-leak rule applies to gates only – and the membership of that list is itself asserted. Anything named *-guard or *-gate is covered by construction, so a new gate cannot silently escape the stricter rule, and model-advisor’s exemption is pinned with its reasoning so a later reader does not “fix” it into a worse state.

On the agent that died

The subagent assigned this stalled after reporting “four failures, two distinct tools” and never wrote its test. That claim was NOT taken on trust: the file it appeared to have produced turned out to be a pre-existing ENT-3 test with a different purpose, passing 2 of 2. The contract was re-derived by running the rules against the real tools, which is how the estimate-run violation was actually found.

v9.1.0

Gate on tokens, because dollars move under you

cost-guard gates on DOLLARS, which depend on model pricing: a dollar ceiling silently loosens on a cheaper model and tightens on a dearer one. Tokens are the provider-independent unit of work.

Demonstrated on this project’s own 307:1 measurement, one real iteration:

--max-output-tokens 50000   exit 0   (34,729 out: the WORK signal)
--max-total-tokens  50000   exit 1   (10.6M total: the CONTEXT signal)

Same run, two verdicts. That distinction is invisible in dollars, which is why a token gate is not a duplicate of the cost gate. The total’s field list is one constant used by BOTH the sum and the printed definition, so the two can never disagree.

The predicate had to be asked a narrower question

record_is_measured() is satisfied by cost OR tokens. Feeding it all five fields the way cost-guard does would be a hole in a TOKEN gate: a record with a real cost and no usage (the codex shape) would have printed “0 output tokens, WITHIN BUDGET” for a run whose tokens were never recorded.

Verified against the real library before deciding:

all five fields   -> measured: True
four token fields -> measured: False

It is fed the four token fields only. That is choosing which fields to ask about, not restating the rule.

An inherited limit, stated rather than defended

A JSON true in output_tokens DOES fabricate a 1-token measurement and exits

  1. collect_efficiency() runs _to_int() over every field, so True is a genuine int 1 before the guard sees it. The bool check in the tool is dead on the CLI path, the fix belongs in _to_int() upstream, and the test pins the ACTUAL behaviour with a note to re-examine if upstream changes. Claiming a defense that does not exist would be worse than the gap.

Finding receipts without pretending unmeasured means cheap

receipt-find.py filters by cost range, failure, and date.

An unmeasured receipt matches NEITHER --min-usd NOR --max-usd, and is reported as excluded:

EXCLUDED from the cost filter: cost was never measured, so it is neither above
nor below the threshold

Both directions are asserted deliberately: a tool substituting 0.0 still passes a --min-usd-only suite, because 0.0 correctly fails to match a positive floor. Testing one direction would have let the exact defect through.

Zero matches and no receipts are DIFFERENT facts with different exits (1 and 3): one means the filter ran, the other means nothing was searched. Malformed receipts are counted separately and never silently dropped.

--failed-only matches NOT VERIFIED only. VERIFIED WITH GAPS verified with recorded gaps and does not match; a receipt with no headline is never claimed as failed, because “we cannot tell” is not “it failed”.

v9.0.0

Adopting the merge gate is now two commands

gate-init.py scaffolds a reviewable policy, baseline-pin.py fixes the reference run it compares against. Together with the existing chain that closes CI cost-governance end to end.

$ tools/gate-init.py .
max_usd = 2.6, derived from: median of 4 measured run(s) = $1.3000, times 2.0x headroom
validated by policy-load.py; ci-gate args: --max-usd 2.6 --require-receipt

With no history it invents nothing. The max_usd key is ABSENT, not guessed, and stderr says the operator must choose one. A fabricated ceiling is worse than none: CI goes green while enforcing an arbitrary limit nobody chose.

The shape was probed rather than assumed. policy-load rejects "max_usd": null (“must be a number, got None”) AND rejects a JSON-embedded note (“unknown policy key”), so the only form satisfying both the no-invention rule and the must-load rule is key-absent with the note on stderr.

The 2.0x headroom is a CHOSEN constant, not derived. The median has a basis in measured runs; the multiplier is judgment (a ceiling at the median blocks half of all normal runs). Only half of “derived from measured spend” is evidence, and the output says which half.

The tampered-baseline hole cost-guard documented against itself

cost-guard.py’s own docstring recorded the gap: “NON-GOAL: the baseline receipt’s integrity hash is not verified. A tampered baseline that inflates the allowed ceiling is out of scope.”

baseline-pin.py closes it. It REFUSES to pin a receipt that fails integrity (“every later comparison would be against a number nobody can verify”) or one with no measured cost (“a percentage increase against an unmeasured number is undefined. Unmeasured is not $0.00”). It records the hash, the cost and the time, so show detects a receipt edited AFTER pinning, and path refuses to emit a drifted baseline rather than quietly feeding it to the gate.

Proven composing end to end: pinned $1.25 against an actual $1.30 passes at --max-increase-pct 10 and fails at 1.

It hashes raw file bytes, not the canonical verification-stripped digest. That digest is blind to edits confined to the verification block, so swapping a whole verification block would have read as “unchanged”.

v8.99.0

--json produced non-JSON on the path a consumer most needs to read

Twelve tools accept --json, and CI composes them by piping stdout into json.load. Every one handled SUCCESS correctly. run-replay.py did not handle failure: on a workspace with no artifacts it printed a bare text line and raised SystemExit before --json was consulted, so the caller got

json.decoder.JSONDecodeError: Expecting value: line 1 column 1

The failure was unreadable to precisely the automation the flag exists for. A structured error is still structured output:

{ "status": "no_data", "exit_code": 66, "workspace": "...", "iterations": [] }

The process still exits 66. Both halves are pinned, because a tool that started emitting JSON but LOST its exit code would turn a blind gate green – the exact defect this tool line exists to prevent, reintroduced through the fix for a different one.

Asserted as a CONTRACT, not per tool

tests/test_json_contract.py walks the real tools/ directory rather than listing cases, so a new tool is covered the day it lands instead of whenever someone remembers. The rule only has value if EVERY tool obeys it; a per-tool test proves one is fine while the next quietly breaks the convention.

The contract is deliberately narrow: if a tool writes to stdout under --json, that text parses. Some tools legitimately put their payload on stderr and keep stdout clean, and asserting “stdout is always non-empty” would have failed them for a choice that is correct.

cost-history.py was initially suspected of the same defect and cleared: it emits a full structured payload, and the first probe simply captured the wrong stream.

v8.98.0

See the verification work, without running a build

tools/verify-demo.sh demonstrates the whole chain at zero cost. Run for real:

 THESE RECEIPTS ARE SYNTHETIC.
VERIFIED -- every axis checked here passed
  integrity VERIFIED  drift VERIFIED  headline VERIFIED  cost VERIFIED  tree VERIFIED
  -> exit 0.

FAILED -- drift, integrity did not pass here
  integrity FAILED
    hash mismatch: recorded 67e6f0a9..., computed 83319644... -- proof.json was
    edited after it was written
  drift FAILED
  -> exit 1. Caught, with the reason printed above.

GATE: FAIL -- 0 of 1 policies passed

The differentiator was previously invisible without a full paid build: of six installed competitor CLIs, none verifies its own output, and a prospective user could not see ours do it.

The good receipt is REAL, not hand-built

tests/test_ci_gate.py records that a clean attestation “needs git state this test cannot honestly fabricate”. That was probed rather than accepted: a scratch git init plus the actual proof-generator.py DOES reach exit 0. So the demo generates a genuine receipt and the real verifier passes it.

SYNTHETIC means no build was run, no provider contacted and no money spent – it does NOT mean a fake receipt. The disclosure prints BEFORE the first verdict, because a reader who stops at the first VERIFIED must already have seen it.

The tamper is caught on TWO independent axes, integrity and drift, which was not designed for and is a stronger result than one.

Two probes that survived, and what they exposed

Deleting the tamper guard survived at first. The attester and the downstream gate BOTH cover that scenario, so blinding every tool at once proved the demo failed somewhere without isolating which guard fired. Fixed with a fixture that blinds only the attester while stubbing the gate to correctly FAIL, so nothing can supply the exit code on the guard’s behalf.

Falsifying the SYNTHETIC banner survived, because the word appears twice and the grep found the surviving one. It now asserts the disclosure precedes the first verdict.

Both are the “asserts a string, not a behaviour” class this repo has paid for repeatedly. The required probe – printing VERIFIED without running the verifier – is caught even though the keyword IS present, because the test re-derives the receipt digest the way receipt-attest.py:182 does rather than grepping for a word.

v8.97.0

The merge gate is now driveable from version control and visible on the PR

Three tools complete the CI cost-governance chain. Verified end to end through a real shell pipe:

$ ci-gate.py $(policy-load.py --as-args)   -> exit 0 within budget, 1 over
$ ci-gate.py ... --json | gate-report.py --format github
::warning title=gate: cost (UNEVALUABLE)::cost is UNMEASURED ... this gate
cannot say whether the run complied, so it reports no verdict rather than a
green one.

policy-load.py makes the policy a reviewable file instead of CLI flags. An unknown key is an ERROR naming the key, never ignored: silently skipping max_usd_ means the operator believes a ceiling is enforced when nothing is. An empty policy is refused for the same reason.

gate-report.py renders the verdict as GitHub step-summary markdown or workflow annotations. UNEVALUABLE is a ::warning, never a ::notice and never silent – a gate that could not check must not look green on the run page.

signing-status.py proves whether this machine can produce SIGNED receipts by doing a real sign-and-verify round trip. gpg installed + key id set is NOT proof: the key may be expired, revoked, or need a passphrase this environment cannot supply. Four states, never a boolean. No key material is ever printed, including on error.

A probe that passed for the wrong reason

policy-load’s unknown-key probe first reported OK – but only the multi-key test failed, via an incidental KeyError traceback from VALIDATORS[key] being indexed with an unknown key. A crash also exits non-zero, so the check looked tested while the operator would have received a stack trace naming only the first typo.

The validator loop now iterates KNOWN_KEYS so a crash cannot mask the named message, and the test asserts "unknown policy key" in stderr AND assertNotIn("Traceback"). Under the corrected probe, {"max_usd": 5.0, "require_reciept": true} exits 0 with the receipt requirement silently dropped – which is the defect in the flesh.

A green verdict over an empty table, then a second layer beneath it

gate-report rendered {"state":"PASS","policies":[]} as green. Fixing the row synthesis exposed a deeper one: _verdict_state trusted the JSON header independently, so the summary line still read PASS above an UNEVALUABLE table. It now reconciles against the rows by weakest link, and exit_code reuses it, so the exit code can never disagree with the report a human just read.

Three signing findings, stated rather than implied

proof-generator.py:_gpg_detached_sign returns None on EVERY failure and proof-verify.py:_verify_gpg returns "n/a" when gpg is unavailable, so a broken key produces silently unsigned receipts with no error anywhere. That silence is what the tool makes visible.

ok and gpg_absent were proven by PATH shim only: this keyring has zero secret keys and creating one was out of scope. not_configured and broken were verified against real gpg 2.5.20.

Follow-up NOT taken: autonomy/loki:11764 still does the boolean check and prints PASS for an expired or revoked key.

v8.96.0

Cost across many runs, where one null row changes the answer by 56%

tools/cost-history.py appends each run’s measured cost to a durable JSONL history and reports the trend. cost-guard gates ONE run; receipt-diff compares TWO; nothing tracked spend over time.

5 run(s), 4 measured
costs oldest to newest: $0.1100, $0.1200, $0.4000, $0.4600
median: $0.2600
direction: rising (median of the oldest 2 ($0.1150) vs the newest 2 ($0.4300); +273.9%)

The median is $0.2600. Had the unmeasured run been stored as 0 it would read $0.1150 – understating real spend by 56%. That single number is the whole feature.

An unmeasured run is recorded with an explicit null and EXCLUDED from the trend, rather than refused. Refusal leaves no trace: a month with four broken-instrumentation runs would look byte-identical to a month with eight clean ones. A null row is countable, so the report says “5 run(s), 4 measured” and the gap is visible instead of inferred from a short file.

A trend from one point reads INSUFFICIENT DATA, never “flat” – flat is a claim. A corrupt line is COUNTED and reported, never skipped. An empty history exits non-zero: zero runs is not a flat trend.

On durability, the append is a single O_APPEND write, and the reasoning is recorded rather than assumed: write-temp-and-rename would be WORSE here because it rewrites the whole history each time, turning a crash into loss of every prior run instead of a partial final line. The residual risk is a truncated last line, which is why the newline terminator is load-bearing and why the corrupt counter exists.

model-advisor reported success on a workspace that does not exist

$ model-advisor.py /nonexistent/path ; echo $?
NO BASIS: no measured, priced iteration in this workspace.
0

The TEXT was honest. The EXIT CODE was not: a CI job doing model-advisor.py "$WS" && ... on a mistyped or unmounted path saw green and carried on. “This workspace has no history” and “this workspace does not exist” are opposite diagnoses, and exit 0 collapsed them.

A missing workspace now exits 66, matching how every sibling distinguishes the case (run-replay 66, cost-guard 2, receipt-bundle 3). A REAL workspace with genuinely no history still exits 0, because “no basis” is an honest answer rather than a tool failure – both directions are asserted.

v8.95.0

receipt-attest --help issued a verification verdict about a file nobody named

$ receipt-attest.py --help
UNVERIFIABLE -- proof file not found: --help

The flag fell through to the positional slot and was read as a proof path. On the tool whose entire job is issuing trustworthy verdicts, that is the worst available failure: to a script parsing the output, an answer about a nonexistent file is indistinguishable from a real finding about a real receipt.

Now --help prints usage and exits 0, and an unrecognized flag is an ERROR (exit 64) rather than a filename – a typo’d --jsonn previously reported UNVERIFIABLE, which reads as a finding about the receipt rather than about the command line.

Isolated, and checked as a class

Every sibling tool (receipt-diff, receipt-bundle, cost-guard, ci-gate, model-advisor, tool-index, run-replay) was probed with a nonsense flag and all seven reject it. They use argparse; this one hand-rolled its parsing, which is exactly how it drifted alone.

The test that kept failing on correct code

Three drafts asserted that the word UNVERIFIABLE never appears in help output. All three failed – because the usage text legitimately DEFINES the three states, so the assertion was matching documentation rather than a verdict.

The fix was to assert the defect’s own sentence (proof file not found) rather than a state name that appears in both correct and incorrect output. An assertion broad enough to match the docs is not measuring the defect; it is measuring the vocabulary.

v8.94.0

Replay what a run actually did, iteration by iteration

tools/run-replay.py reconstructs a completed run from artifacts alone. Run against a real recorded workspace it surfaces something the aggregate stage table cannot:

RUN REPLAY -- reconstructed from artifacts only.
Nothing was started and nothing was spent; this reads .loki/ and exits.
  unparseable lines skipped: 0 (whole recording read)

ITERATION 1  [completed]
  code_review                  57s   fail
  test_suite                    1s   pass
  cost                     $0.1767  in 106 / out 15241 / cache-read 439834 tok  [haiku]
  gates failed             code_review

code_review failed in BOTH iterations and consumed 125s of 129s total stage time. That is a diagnosis; a single aggregate row is not.

A dropped line must be counted, not vanish

scripts/measure-run.sh skips unparseable lines with except Exception: continue and the comment “a truncated tail must not abort the report” – correct as far as it goes, but the drop is silent, so a corrupt recording reports a cleaner run than happened. The replay COUNTS skipped lines and prints the count. Replacing skipped += 1 with pass turns the suite red.

The subtler case: a truncation that parses perfectly

A cleanly truncated events.jsonl – exactly what a killed run leaves – has no unparseable lines at all. skipped_lines is 0, the recording looks whole, and an entire iteration is simply absent with no signal, in precisely the scenario this tool exists for.

Efficiency records are now read as a SECOND, INDEPENDENT WITNESS to which iterations existed. Without it, that iteration’s cost vanished from both headline claims silently.

Also fixed: prev_cost carried across an unmeasured gap, so a measured/unmeasured/measured sequence compared iteration 3 against iteration 1 while printing “vs previous iteration”.

Absent is not zero, on every axis

An iteration with no efficiency record reads cost not recorded, never $0.00. A stage that never emitted stage_complete reads not-recorded rather than 0s, because 0s reads as “instant” – a different claim. A genuine 0s duration survives as 0s; collapsing it into “not recorded” is its own mutation and is caught.

A probe that failed for the right reason

The mandated probe first reported MUTATION SURVIVED because a comment quoted the find-string verbatim, so the replacement hit the comment instead of the code – the exact ambiguity mutation-probe.sh’s own header warns about. The comment was reworded rather than the probe weakened.

v8.93.0

An audit trail across a sequence of runs

tools/receipt-bundle.py collects every receipt under a workspace into one verifiable bundle. Single-receipt verify, cross-run diff and portable attestation existed; proving a SEQUENCE did not.

The rollup is the weakest link, never an average. One FAILED receipt among a thousand VERIFIED ones makes the bundle FAILED:

FAILED -- 1 of 3 receipts FAILED verification; the bundle is only as good as
its weakest receipt. total cost $2.0600 across 3 of 3 receipts measured

Any scoring rule – mean, median, majority, “90% or better” – lets a forged run hide behind good ones, which is the laundering this whole product line exists to prevent. Replacing min(states) with a majority vote turns the suite red.

An unverifiable receipt is COUNTED and NAMED, never silently dropped: a bundle that quietly excludes what it could not check reports a stronger claim than it earned. An empty workspace exits 3 with “Zero receipts is not a passing audit.”

ok was the wrong source, and reading it would have collapsed two states

proof-verify.py:706 documents that diff_drift: None (unverifiable) makes ok False BY DESIGN. So ok is False both for a tampered receipt and for an honest one read from the wrong directory. Deriving state from it would have merged FAILED with UNVERIFIABLE and made the naming requirement unsatisfiable. Per-receipt state reads the axes individually.

What a run would have cost on a cheaper model

tools/model-advisor.py recommends a cheaper model from this workspace’s MEASURED history and quantifies the saving:

  Model used:     opus        Measured cost: $1.6558  (basis: 2 of 3 priced)
    gpt-5.6-luna  0.04x  ->  $0.0595   saving $1.5963
    haiku         0.20x  ->  $0.3312   saving $1.3246
    fable         2.00x  ->  $3.3116   +$1.6558 more

The ratio is token-weighted across the full rate card, not an input-rate ratio, and it scales the RECORDED cost so the saving stays anchored to a measured number.

MiniMax M2.5 is not in the pricing table, so it cannot be a candidate. It appears only as a cited external benchmark with its figures attached (75.8 vs Opus 4.6’s 75.6 at 7.5x lower cost), never as a recommendation about the user’s workload. Candidates come from the rate card; the benchmark ranks nothing.

No history prints NO BASIS ... no saving is projected (not $0.00). Forcing has_basis true turns the suite red. Every output carries the caveat that a cheaper RATE is not a cheaper RUN – token counts differ between models, and that assumption is stated as unverified rather than buried.

Two defects found by verifying rather than assuming

A more-expensive candidate rendered $-1.6558 in a column headed SAVING, which skims as a saving. It now reads +$1.6558 more.

An ok fallback branch was decorative until a probe SURVIVED against it: cost_coherent is a real failure axis that sinks ok but matched none of the enumerated branches, so it reached FAILED only through a path nothing tested. Deleting it left the suite green. That branch is what stops a FUTURE upstream check from arriving here as a pass.

v8.92.0

One exit code over every policy

tools/ci-gate.py is net-new: the command a CI job runs after an agent build. It composes the existing policy tools by subprocess and returns a single verdict.

CASE 1 ALL PASS      cost PASS within_budget                GATE: PASS         EXIT=0
CASE 2 FAILED        cost FAIL $0.5000 exceeds $0.0001      GATE: FAIL         EXIT=1
CASE 3 UNEVALUABLE   cost is UNMEASURED                     GATE: UNEVALUABLE  EXIT=2
CASE 4 NO POLICY     (empty table)                          GATE: UNEVALUABLE  EXIT=2
CASE 5 MIXED         cost UNEVALUABLE / receipt FAIL        GATE: UNEVALUABLE  EXIT=2

Zero policy logic is reimplemented – cost-guard.py and receipt-attest.py run via sys.executable with --json. A second copy of a rule is how the rule drifts, which this repo proved five separate times with provider lists.

Four decisions, each one a refusal to leak green

Weakest link, no threshold. One FAIL fails the set. A tunable threshold is a request to let a known-bad policy through.

Unevaluable OUTRANKS fail. The mixed case returns 2, not 1. “Your gate is partly blind” beats “this run is over budget”: with 1, the operator fixes the cost, re-runs, sees green, and is still blind on the dead axis.

No policy configured is 2, never 0. A gate that lost its flags in a refactor would otherwise go green forever while enforcing nothing – worse than no gate, because it is trusted. That is the “check that can report a pass without having checked” hole CLAUDE.md records.

Child exit codes are whitelisted to {0,1,2}. receipt-attest.py returns 64 on a usage error and an uncaught traceback exits 1, so a naive “rc != 1 means pass” would read the first as green and misattribute the second as a policy verdict.

--require-receipt with no receipt on disk is 1, not 2: it was checked and the answer is no. 2 is reserved for a receipt that exists and could not be attested.

A cell that looked filled in while reporting nothing

Found during verification, not assumed away: the PASS case first printed a bare { as its detail, because cost-guard sets reason: null on success and the fallback grabbed the first line of pretty-printed JSON. A cell that reports nothing while looking populated is the same class of defect as the gate itself, so the reason resolver now prefers status and refuses a raw JSON body.

Every unevaluable assertion pins != 0 as well as == 2, so folding that state into a green path under a different name still fails.

v8.91.0

Eleven tools shipped and none were discoverable

$ loki help | grep -cE 'receipt-diff|cost-guard|attest|preflight'
0

Every tool under tools/ was built, tested, and packaged – and there was no way for a user to learn it existed. That is this repo’s most-repeated defect, now on its eighth instance:

v5.0.0  -> v8.64.0   auto_detect_provider() called only by its own test
v8.73.0 -> v8.84.0   the receipt's --human explanation reached no CLI path
v8.87.0 -> v8.89.0   preflight.sh shipped to a directory not in files[]

A capability nobody can find is worth exactly what one that does not exist is worth. tools/tool-index.py lists all twelve with the descriptions their own authors wrote.

An index that guesses would be worse than no index

Two properties carry the weight, and both are mutation-proven:

A tool with no description reads (no description). A summary inferred from a filename is indistinguishable, to the reader, from one the author wrote. The reader cannot tell a guess from a fact, so the guess is strictly worse than the gap. Making the fallback synthesise "Runs <filename>" turns the suite red.

SHIPPED is derived from package.json files[], the same source npm reads – never assumed. A tool present in the checkout but absent from the tarball prints [dev] with an explicit note. That is the v8.87.0 defect exactly, and an index that listed it alongside the reachable ones would have helped that release lie. An unreadable manifest reads UNKNOWN ([ ? ]), not “not shipped”: reporting False there would be a claim rather than an absence, and that mutation is caught too.

Descriptions are extracted by ast.parse, not regex, so a # inside a string literal cannot be mistaken for a comment. A syntactically broken tool degrades to (no description) rather than crashing the index.

An empty tools/ directory exits 2 and says so. Zero tools is not a successful listing.

v8.90.0

A portable attestation that never claims more than it checked

tools/receipt-attest.py is net-new: it turns a receipt into a compact record a third party can read without the original workspace.

The design point is a THIRD state most systems do not have:

VERIFIED      checked, and it passed
FAILED        checked, and it failed
UNVERIFIABLE  could not be checked here, with the reason

Collapsing the third into either of the first two is the defect. An attestation claiming more than was checked is worse than none: it launders uncertainty into a credential. Axes are scored independently, so a receipt checked outside a git tree reports integrity VERIFIED alongside drift UNVERIFIABLE ('.' is not a git work tree) rather than one verdict smeared across everything.

Exit 0 only when every scored axis was checked AND passed; 1 on any FAILED; 2 when nothing failed but something was unverifiable. 2 is non-zero specifically so a shell && chain cannot read “unattested” as “attested”, which would re-collapse the third state at the process boundary.

A dropped axis was attesting VERIFIED on real receipts

The sharpest finding, reproduced against production shapes: tree_sha256 is written on EVERY receipt by proof-generator.py:1190 and scored by verify(), but the first attestation did not read it. A receipt that verify() REJECTS (ok=False, tree_drift=True) attested VERIFIED, exit 0 – exactly the laundering this feature exists to prevent.

A dropped axis defaults to “passed”, which makes FORGETTING one more dangerous than getting one wrong. The class invariant is now asserted directly: VERIFIED implies verify().ok, so a future dropped axis fails loudly instead of silently widening the credential.

Signature status is four states, not a boolean

_verify_gpg returns "n/a" for two different facts: no signature at all, and a signature present with gpg unavailable. Reading gpg_ok alone reports a SIGNED receipt as unsigned on any machine without gpg. Now branched on whether the receipt actually carries a signature: signed_valid, signed_invalid, signed_uncheckable, unsigned.

That probe SURVIVED at first, which proved the CLI tests could not reach the branch (gpg is installed here). It was closed with a function-level test rather than assumed covered.

One judgment call, stated

Unsigned receipts still exit 0, following verify()’s existing rollup (gpg_ok in (True, "n/a")) rather than inventing stricter semantics – signing is default-OFF, so scoring it would make exit 0 unreachable for nearly every receipt. Mitigated by an unmissable caveat on every summary line: “receipt is UNSIGNED, so origin rests on its generator”.

v8.89.0

v8.87.0 shipped a feature no npm user could reach

preflight.sh was released as a user-facing command and placed in scripts/. scripts/ is not in package.json files[], so it never entered the tarball. Verified by unpacking the real published package:

$ test -f package/tools/preflight.sh
MISSING -- scripts/ is not in files[]

Everything worked from a git checkout, so no in-repo test and no CI job could see it. That is precisely the packaging blind spot CLAUDE.md records: a check that guards the shipped artifact must run in the FAST tier, because it is the only tier that runs before every push and CI has no equivalent.

scripts/ correctly stays out of the package – it holds 23 dev-only files (local-ci, license-audit, test cleanup). So preflight moved to tools/, alongside the siblings it composes, all of which were already shipping.

The guard now names every user-facing tool individually

tests/test-runtime-libs-are-packaged.sh covered autonomy/lib/*.py but not tools/. It now asserts each of preflight.sh, receipt-diff.py, cost-guard.py, estimate-run.py, and doctor-fix.py individually – a count cannot say WHICH artifact vanished, and this repo has paid for that shortcut before (the “6 or more matches” tarball hole).

Proven against the real defect: moving preflight back to scripts/ turns the suite red twice, once on the vacuity guard (the file is not where the check looks) and once on the tarball assertion.

What actually caught it

Not a test – unpacking the published tarball and running the feature from it. The four tools/ and autonomy/lib/ features all shipped correctly; only the one placed in an unshipped directory did not. A green CI and a green local gate both reported success on a release that was, for every npm user, missing the thing it announced.

v8.88.0

Gate a merge on cost, the way you gate on tests

tools/cost-guard.py is net-new:

cost-guard.py <ws> --max-usd 5.00                        -> exit 0
cost-guard.py <ws> --max-usd 1.00                        -> exit 1
cost-guard.py <ws> --baseline b.json --max-increase-pct 10 -> exit 1
OVER BUDGET: cost $2.5000 exceeds the ceiling $1.0000 by $1.5000 (150.0% over)
OVER BUDGET: cost rose 150.0% ($1.0000 -> $2.5000, +$1.5000), over the allowed 10.0%

It names which number breached and by how much, never a bare FAILED.

Unmeasured exits 2, and that is the whole design

A gate’s exit code IS a merge decision. “We did not measure” carries zero information about budget compliance, so mapping absence to 0 makes the gate emit its most confident output – a green – at exactly the moment its instrumentation broke and it is least entitled to speak.

CANNOT EVALUATE: cost is UNMEASURED -- no efficiency record carried an observed
cost or token count. Unmeasured is not within budget: this gate cannot say
whether the run complied, so it reports no verdict rather than a green one.
exit=2

Enforced structurally: exit 0 is reachable from exactly one return at the bottom of evaluate(), downstream of a measurement. No policy, half a policy, unreadable baseline, unmeasured baseline, unmeasured run – all return

  1. That is the same class as the “6 or more matches” tarball hole: a check that can report a pass without having checked.

A genuinely measured $0.00 with real tokens still passes. Those two shapes are the most easily confused in this codebase, and they land on 0 and 2.

Two real bugs its own tests found

Float boundary. 1.00 -> 1.10 computes as 10.000000000000009%, so a run exactly at its allowance FAILED while printing “rose 10.0%, over the allowed 10.0%” – a verdict contradicting its own message. Same trap on the ceiling ($0.10 + $0.20 > $0.30 is True in binary). Both now compare at the precision they report. Because that LOOSENS a fail-closed gate, two tests pin the tolerance ($1.01 vs $1.00 must fail; +10.2% vs 10% must fail) and a whole-dollar tolerance is mutation-caught.

Half-applied policy. --max-usd 5 --baseline old.json never opened the baseline and could exit 0 having silently ignored half of what the operator asked for. Now exits 2.

An inherited limit, stated rather than silently owned

record_is_measured() is satisfied by tokens OR cost, so a record with real token counts but no cost_usd field reads as a measured $0.00 and passes. Verified empirically. NOT fixed here: the rule that cost_usd: 0.0 with tokens must pass makes those two shapes identical downstream of the sum, so the fix belongs in the pricing layer (price_from_tokens exists for exactly this), never in a second copy of the honesty predicate.

The baseline receipt’s integrity hash is not verified either – a tampered baseline could inflate the allowed ceiling. receipt-diff.py covers that.

v8.87.0

One command that answers “will this run work, and what will it cost?”

scripts/preflight.sh is net-new and READ-ONLY: it starts nothing, spends nothing, and contacts no provider.

Provider
  OK    auto-detection would select: claude
Environment
  OK    doctor: 12 checks passed, 1 warning(s)
Projected cost
  measured per-iteration cost: $0.0500 (median of 3 priced iteration(s))
  projected for 4 iteration(s): $0.20  [ESTIMATE, not a guarantee]
VERDICT: READY

With no history it reads UNKNOWN ... there is NO history to project from, so no cost is estimated (not $0.00). BLOCKED names each blocker AND its fix.

It COMPOSES existing producers rather than reimplementing them – auto_detect_provider, doctor --json, estimate-run.py – because a second implementation of a rule is how the rule drifts, which this repo has now proven five times over.

The fifth provider list, and doctor was the one lying

Building preflight surfaced it: _provider_cmds in autonomy/loki backs doctor --json’s verdict and omitted opencode. On an opencode-only machine:

ai_provider.status : fail
ai_provider.detail : No AI provider CLI. Fix: npm install -g @anthropic-ai/claude-code

The runner would have selected opencode without complaint. This is the surface a user consults FIRST when something looks wrong, so it sent them to install a provider they did not need – the same defect as v8.76.0 and v8.82.0, on a fifth list. tests/test-provider-lists-agree.sh now pins this one too.

A test that encoded the bug as expected behaviour

Preflight shipped with two assertions requiring it to REPORT the disagreement between the two provider checkers. That was correct when written and obsolete the moment the underlying list was fixed: with the checkers agreeing there is nothing to surface. Both assertions were rewritten to assert the RESOLVED state – an opencode-only machine reads READY, and a re-emerging disagreement now FAILS rather than passes.

Three defects the author found by verifying rather than assuming

One mutation probe SURVIVED and is reported as such: an if proj is None: guard is unreachable because the estimator always returns a projection when has_basis is true. It is defensive depth against a fabricated zero, not tested behaviour.

v8.86.0

doctor tells you what is broken; now something tells you how to fix it

autonomy/lib/doctor-fix.py is net-new: it turns a loki doctor --json report into an ordered, copy-pasteable remediation plan.

Remediation plan -- 2 blockers, in order (prerequisites first).
These commands are NOT run for you. Copy, review, then run them yourself.

1. Node.js is not installed
   Install Node.js 18+: brew install node    (macOS)  |  https://nodejs.org

2. No AI provider CLI installed (at least one is required)
   npm install -g @anthropic-ai/claude-code

The ordering is load-bearing rather than cosmetic: step 2’s npm does not exist until step 1 runs.

It refuses to guess

An unrecognized blocker prints, verbatim:

   no automated fix known for this blocker -- diagnose manually, then re-run: loki doctor

A wrong command is worse than no command, because the user runs it and trusts the result. It also never executes anything and says so in its own output.

An empty blocker list reads System is healthy; unparseable input is REFUSED rather than rendering as healthy, which is the same absent-vs-zero distinction this codebase has now applied to thirteen surfaces.

It parses doctor’s own remedy rather than restating it

ai_provider.detail already carries "...Fix: npm install -g @anthropic-ai/claude-code". That Fix: is parsed, so doctor stays the single source of truth; hardcoding a duplicate turns the suite red. cmd_why’s PROVIDER_AUTH map was deliberately NOT reused: it keys on a LAST_ERROR error_class, an orthogonal axis, and doctor --json has no auth blocker to map from. Manufacturing one to demonstrate an ordering dependency would be the exact fabrication the tool refuses.

Commands sourced from the repo are distinguished from conventional ones, since only brew install python3, brew install jq and the nodejs.org URL actually appear in-repo.

A stale-bytecode trap worth recording

A mutation probe left the suite red against a provably-correct source. spec_from_file_location had cached a .pyc, and the probe’s restore produced a same-size, same-mtime file, so Python reused stale bytecode: the suite ran mutated code against a restored file. That presents exactly like a genuine defect. Fixed at the root with sys.dont_write_bytecode = True before the loader; this bites any mutation probe against a hyphenated module.

Known gap, stated rather than implied

doctor --json emits no blocker list. Blockers are DERIVED from status == "fail" across checks[], ai_provider, and disk. Two text-path blockers (broken skill symlink, missing detectors) have no JSON representation today, so this tool cannot see them; it handles them if they ever appear. Nothing invokes doctor-fix.py from the CLI yet – it runs standalone via --report or stdin.

v8.85.0

Compare two runs: what actually changed, and what cannot be compared

tools/receipt-diff.py is net-new. Verification of ONE receipt existed; comparing TWO did not.

Evidence Receipt diff
  cost             12.421 -> 3.112    delta -$9.3090
  cache hit ratio  0.2000 -> 0.9000   delta +70.0%
  iterations       6 -> 2             delta -4
  duration         2333 -> 754        delta -1579s

  REGRESSED  tests: passed -> failed
  FIXED      lint: failed -> passed
  ADDED      security (passed), absent in A

A delta against an unmeasured value is UNKNOWN, never a number

Thirteen surfaces have now been through this rule, and a diff is where it would have been easiest to violate: subtracting an absent measurement produces a plausible-looking saving.

  cost             12.421 -> UNKNOWN   delta UNKNOWN

  NOT COMPARABLE (reported UNKNOWN, not zero):
    cost: cost was not measured in <path>
    cache_hit_ratio: no cached-plus-fresh input tokens recorded, so no ratio
                     was observed (this is not a 0% hit rate)

Iterations and duration still compute; only the unmeasurable axis abstains. --json emits the string "UNKNOWN", never a number. A genuinely measured zero delta stays 0.0 – is None is load-bearing, since 0 is falsy and a falsy guard would blank a real observation.

Integrity first, and comparability before arithmetic

A tampered receipt is refused before any delta is computed (verify_integrity is imported, not reimplemented), and receipts for different specs are refused with both paths named rather than producing a meaningless number. Both exit 2.

record_is_measured() reads cost_usd while receipts store cost.usd, so the key is mapped and the canonical predicate called. cost.available is deliberately NOT trusted on its own – that flag lying is precisely the v8.52.0 defect.

A red test that was kept red

The tool was asserted to classify the v8.52.0 shape (available: true over an all-zero cost block) as unmeasured. It never gets the chance: verify_integrity already scores that incoherent, so the integrity gate refuses it upstream. That became two tests – one for the coherent all-zero variant that does reach the diff, one asserting the shipped defect is refused before arithmetic – rather than weakening either.

v8.84.0

The receipt could explain itself, but nobody could ask it to

v8.73.0 taught verify_integrity() to return a reasons list naming every failed check, and shipped render_reasons() plus a --human flag on proof-verify.py. Nothing in the CLI ever passed that flag. loki proof verify printed raw JSON, so the person holding a failed receipt had to parse it by eye.

Same shape as auto_detect_provider(), which sat unwired from v5.0.0 to v8.64.0: built, tested, unreachable.

$ loki proof verify <id> --human
FAILED
  - hash mismatch: recorded deadbeef, computed 287cf091... -- proof.json was
    edited after it was written
  - cost claim is incoherent: cost.available is true but every token count and
    usd is zero or absent; the receipt claims a measurement it does not have
    (an unmeasured run is unknown, not free)
  - drift unverifiable: '.' is not a git work tree, so the recorded diff cannot
    be re-derived

This matters more here than for a typical command. The Evidence Receipt is the differentiator – of six installed CLIs, only loki-mode verifies its own output – and a verifier that answers “false” without saying why spends most of that advantage.

JSON remains the default, deliberately

Existing consumers pipe this stdout verbatim, so flipping the default would have been a silent breaking change dressed up as an improvement. --human is opt-in, and the test asserts the default is still parseable JSON. Making --human the default turns the suite red.

The flag is STRIPPED from the positional args rather than assumed at a fixed index, so it works on either side of the proof id – id is consumed positionally, and a fixed-index assumption would break one of the two orders a user will actually type.

A note on finding the right code

The verify) arm was patched by locating it structurally rather than by string match: five separate blocks in autonomy/loki contain a byte-identical Missing proof id. guard, and a first-match replace silently patched a DIFFERENT command’s arm. The symptom was an unbound-variable error at runtime, not a failed edit.

A passing receipt still renders the verdict alone, never a fabricated reason – a tool that invents a complaint to look thorough is the same dishonesty in reverse.

v8.83.0

A quality gate reported unanimous PASS without running

magic/core/debate.py runs four persona critics and computes:

consensus = len(critiques) > 0 and all(c.approves for c in critiques)

When the provider CLI is absent, _invoke_provider raises and all four round paths route to _critique_from_error, which returned approves=True. So four FAILED invocations produced unanimous consensus. Reproduced on the shipped code:

approves: [True, True, True, True]
consensus from 4 FAILED invocations: True

The verdict outlives the call: consensus feeds debate_passed into the registry and into the memory bridge. A gate that cannot run must not vote.

The old docstring justified it as avoiding a spurious block. That is the wrong instinct on a machine surface – “the check did not run” and “the check passed” are opposite facts. Failed personas now report approves=False while keeping severity: "info", so an infrastructure failure is never laundered into a dissent that blocks; parse_error remains the discriminator, and Gate 12 (which greps for severity: "block") is unchanged. Verified: 20/20 gate tests still pass.

autonomy/loki:30593 already carries a comment about this exact class biting this feature once before, when a caller’s || true made Gate 12 report PASS on total failure.

Recorded, not fixed

Two more findings in magic/, both left alone with reasons rather than quietly:

Fallback content is stamped as generated. When the provider returns empty, generator.py substitutes a hardcoded button component, and _hash_header stamps Generated by magic/core/generator.py on both paths identically with no provenance flag – so a caller asking for a DataTable gets a button, labeled generated. It is documented as deliberate graceful degradation, so changing it is a design decision across four call sites plus a return-shape change, not a surgical fix.

avg_debate_score is always 0.0. registry.py:224 averages debate_result["score"], but DebateResult.to_dict() emits no score key and nothing in the repo writes one, so the else 0.0 branch always fires and loki_magic_stats renders that as a measurement. The cleanest remaining unmeasured-reads-as-zero violation, ranked below the gate because nothing acts on the value.

v8.82.0

An agent that BUILT an auth feature killed its own run

retry_class.ts classifies a failure as permanent or transient. It is handed the agent’s entire captured output – the agent’s own prose, up to 64KB – not a provider error envelope. Several patterns were ordinary application vocabulary. Measured on the shipped code:

agent built auth: test log shows 401       -> {"klass":"non_retryable","reason":"auth"}
implemented permission denied handling     -> {"klass":"non_retryable","reason":"auth"}
added a payment required page to checkout  -> {"klass":"non_retryable","reason":"quota_exhausted"}

The consequence is not a skipped retry. autonomous.ts:873-881 persists failed and returns exit 1, terminating the whole run on the first iteration – default-on, triggered by writing about auth or payments.

This is precisely the inversion the module header forbids: an unrecognized condition taking the dangerous branch. Fixed by DELETION, in the sanctioned direction (toward TRANSIENT): bare 401/403/402/400, unauthorized, permission denied, payment required, invalid model are gone. Every provider wire-text literal stays (invalid x-api-key, insufficient quota, credit balance too low, model_not_found).

After:

agent built auth ...        -> {"klass":"transient","reason":"unrecognized"}
invalid x-api-key           -> {"klass":"non_retryable","reason":"auth"}
some unrecognized failure   -> {"klass":"transient","reason":"unrecognized"}

No proximity or context heuristics were added: that is a natural-language arms race, a bigger diff for less certainty. The module had ZERO prior test coverage; it now has 27 tests asserting all three directions, including that real provider failures still stop early – without that group, deleting the pattern list entirely would pass.

A competitor we could not run scored 0 and we won by default

The benchmark grader set success = (acceptance exit == 0) on whatever was in the workdir. When the tool never ran, that is the untouched fixture, so the trial scored success=false. Every distinct failure collapsed to the same number:

cli_not_found -> 0.0     timeout -> 0.0     adapter_error -> 0.0
error_rc_1    -> 0.0     completed -> 0.0

Published as: | aider | 0/3 | automated (verified) | – a confident false claim about a competitor that was merely not installed. It failed in our favour, which makes it worse than a gap: a missing number invites a question, “0/3 (verified)” does not.

benchmarks/bench/adapters/_base.py:129-131 already said of cli_not_found: “Honest: not an error in the run, the tool is simply absent. Report it so the report can render ‘tool absent’.” No consumer ever did.

Now:

NOT INSTALLED            success_rate=None   attempted=3  unmeasured=3
TIMED OUT                success_rate=None   attempted=3  unmeasured=3
real zero (ran, failed)  success_rate=0.0    attempted=3  unmeasured=0

Measuredness is derived at READ time from exit_status, so historical rows retro-classify rather than all becoming UNKNOWN, and an unrecognized status reads as MEASURED – the fail-safe direction, since this predicate must never silently delete real data.

opencode was missing from four more lists

loki provider list (BOTH routes), the bash and zsh completions, and the README provider matrix all named the stale four. The Bun route needed two edits, not one: ALL_PROVIDERS and a separate hardcoded rows array that actually renders. Both routes now print identical output.

Two documented env vars did nothing

skills/healing.md listed LOKI_HEAL_BASELINE_DIR and LOKI_HEAL_PRESERVE_FRICTION in its Environment Variables table. Neither is read anywhere: the baseline path is hardcoded at migration-hooks.sh:776, and friction preservation is unconditional. A phantom row is worse than an absent one – an undocumented var makes you read the code, a documented one makes you trust a lie.

v8.81.0

Every emit.sh event was attributed to “unknown”

events/emit.sh built its flat record with a comment asserting that source is “not part of the dashboard schema”, and dropped it. That claim was false against the very code it describes. dashboard/server.py:6686:

s = e.get("data", {}).get("source", "unknown")
by_source[s] = by_source.get(s, 0) + 1

exposed as signalsBySource. So every event routed through emit.sh fell into the unknown bucket and the entire by-source breakdown was meaningless.

Before:

{"timestamp":"...","type":"state","data":{"action":"phase_change"}}
dashboard attributes to: 'unknown'

After:

{"timestamp":"...","type":"state","data":{"source":"my-component","action":"phase_change"}}

The failure shape is the dangerous kind: no error, no warning, a populated chart that is simply wrong. Nobody goes looking for a bug in a number that renders.

Spliced, not appended

PAYLOAD always opens with {"action":..., so source is inserted after the brace rather than the object being rebuilt. Rebuilding is where a caller’s key=value pairs would silently vanish, so the test asserts those survive: emit.sh task runner complete task_id=task-001 keeps task_id alongside the new field.

The test pins the writer to the reader

It asserts the dashboard still reads data.source FIRST – if that stops being true the rest of the test is measuring nothing – then emits real events and parses them with the dashboard’s own expression, byte for byte. A test that merely grepped emit.sh for the string would have passed while the reader moved.

Removing the splice turns it red.

v8.80.0

Three more surfaces called an unmeasured run free

The class that took v8.51.0 through v8.54.0, then v8.69.0, v8.72.0 and v8.74.0. Six surfaces were fixed; these three were never audited.

/api/cost had no measured/unmeasured concept at all. The user-facing cost endpoint rendered estimated_cost_usd: 0.0 and total_tokens: 0 for a run where nothing was recorded.

/api/cost/timeline already had the right FIELD and set it wrong: for data in records: cost_recorded = True – true for any parseable file. The canonical docstring forbids exactly this: a present file is not a measurement.

loki_agent_metrics (MCP) returned efficiency records verbatim. This one has the widest blast radius: it is machine-consumed, so a downstream agent doing budget accounting reads cost_usd: 0, concludes the run was free, and no human is present to sanity-check it.

All three now mirror record_is_measured() rather than restating the rule, and the decision is made once per RECORD, never per field – 0 is falsy, so a per-field test would blank genuine measured zeros.

A test was asserting the lie

test_recorded_but_zero_distinct_from_not_recorded used an all-zero fixture byte-identical to the canonical UNMEASURED shape, so it locked in “present file == measured”. The regression net had a hole shaped exactly like the defect. Its fixture is now a genuine measured zero (real tokens, $0 cost), with a separate test for the all-zero case.

An emitted event could vanish with a receipt

events/emit.sh discarded the append result, and the exit code could not carry a failure anyway: both best-effort fallback paths end in || true; return 0. Probed against an unwritable events.jsonl:

ab585200
EXIT=0
--- jsonl size --- 0

An ID printed, exit 0, zero bytes written. measure-run.sh skips absent lines by design, so the loss surfaces as “0 events for a stage” – an absent measurement read as “the stage did not happen”.

It now warns on stderr only: stdout still carries just the event ID so the ID=$(bash emit.sh ...) contract holds, the check sits inside an if so a failing stat cannot trip set -e, and the exit status stays 0. An emit failure must never crash a build.

A false positive was caught during development and is pinned by its own test: defaulting an unmeasurable size probe to 0 made EVERY successful emit warn. A warning that cries wolf on the hot path is worse than none, so an unmeasurable size stays silent.

Recorded, not fixed

emit.sh claims source is “not part of the dashboard schema”, but dashboard/server.py:6678 filters on data.source and :6687 counts by_source, so emit.sh-routed events are attributed unknown. Separately, bus.py and bus.ts emit() write only the pending file and never reach events.jsonl, and their timestamp format differs from emit.sh in the same directory while get_pending_events compares timestamps as strings.

v8.79.0

Every autonomous opencode run would hang

providers/opencode.sh declared PROVIDER_AUTONOMOUS_FLAG="" and all three dispatch paths built opencode run --model <m> <prompt>. Verified against the real installed CLI (opencode 1.18.9):

--auto   auto-approve permissions that are not explicitly denied (dangerous!)
                                              [boolean] [default: false]

loader.sh justified the empty value by asserting that opencode run <prompt> is already autonomous. The CLI contradicts that outright. Without --auto, every autonomous run stalls on the first permission prompt with no TTY to answer it – so it presents as a hang, not an error: no message, no exit code, nothing to grep for.

This hit every opencode run, including the opencode-only machines v8.76.0 had just unblocked. Setting the variable alone would have been cosmetic, since nothing splices it into opencode’s argv; the flag is spliced into all three paths.

The test covered one path of three

The first version asserted only provider_invoke_argv. A mutation removing --auto from provider_invoke SURVIVED – the other two paths call the binary directly and were unguarded, so a regression in either would have shipped silently.

The test now runs each dispatch function against a stub opencode on PATH and asserts the real argv. All three mutations are now caught.

Checked and deliberately NOT changed

Codex “stale v0.98 assumptions” is a stale COMMENT, not a stale flag. Installed codex is 0.146.0, and every element of exec --sandbox workspace-write --skip-git-repo-check was verified present in codex exec --help. An unverified flag change would break dispatch for real users, which is worse than an out-of-date comment.

opencode’s missing cost/context scalars are cosmetic. review_effective_cap explicitly guards unset and returns the default, so a provider declaring none takes the unset path. Adding values would only LOWER opencode’s review cap – a behaviour change dressed as a fix.

cline’s -y flag is unverified: cline is not installed on this machine, so it was left alone rather than changed on assumption.

Retrieved memories that were never in the knowledge base

build_rag_context rendered rows matching only a category bucket – no name, no description – under a header asserting they were found in the knowledge base:

The following patterns were found in the organization knowledge base:

### Unknown Pattern (retry)

A fabricated memory injected into the agent’s prompt, burning context budget on nothing. Two falsy traps made the obvious guard useless, both verified against real code:

{"category": "retry", "description": ""}.get('name', p.get('pattern', 'Unknown Pattern'))
  -> 'Unknown Pattern'      # truthy, so `if not name` never fires

{"description": "", "correct_approach": "Use a key."}.get('description', q.get(...))
  -> ''                     # the key EXISTS, so the default never fires: real data lost

The second is the dangerous one – the naive fix drops genuine memories. or chaining fixes both, and a substance guard requires a name or a description, since a category alone is a bucket label rather than a memory.

v8.78.0

The provider docs described the product as it was before v8.64.0

skills/providers.md – the page a user reading about providers lands on – listed FOUR providers, never mentioned opencode, and presented LOKI_PROVIDER as something you must set:

export LOKI_PROVIDER=claude  # or codex, cline, aider

That is the pre-v8.64.0 world. An opencode user reading it would conclude their working machine was unsupported, which is the same conclusion the v8.76.0 gate bug forced on them in code.

The doc now leads with the fact that a provider is auto-detected (claude > cline > codex > aider > opencode), states that an explicit choice always wins and is never silently substituted, and points at loki doctor for which provider would actually run.

Docs are a surface, and this one is now pinned to the code

v8.76.0’s root cause was a SECOND list of providers that had drifted from auto_detect_provider(). skills/providers.md was a third. Rather than fix the wording and hope, a test extracts the authoritative order from providers/loader.sh and asserts:

Both drift directions are mutation-proven: adding a provider to the code turns it red, and reordering the doc turns it red.

Checked and correctly left alone

The one gemini reference under references/ is a DeepMind research citation (Gemini Robotics 1.5), not a stale provider claim. Gemini was removed as a provider upstream; nothing documents it as one.

v8.77.0

Two cost tools shipped that no user could run

$ grep -rn "cost-summary\|estimate-run" autonomy/loki bin/loki
(zero matches)

autonomy/lib/cost-summary.py (v8.69.0) and tools/estimate-run.py (v8.71.0) both existed and neither had a CLI path. Same shape as auto_detect_provider(), which sat unwired from v5.0.0 until v8.64.0: built, tested, unreachable.

loki cost --detail          per-iteration rows, cache hit ratio, cost trend,
                            and the measured/found PARTIAL flag
loki estimate --iterations N   projects forward from measured history

The premise was half wrong, and replacing the old command would have been a regression

loki cost was ALREADY reachable via cmd_report cost. What was unreachable was cost-summary.py specifically – a different renderer over the same records. And cmd_cost supports --last N and budget caps that cost-summary.py does not, so swapping its body would have been a regression dressed as wiring. --detail is additive; estimate is a genuinely new arm.

Two defects found while wiring, both of the same family

--detail --last N silently discarded --last. A flag that looks accepted and does nothing is the same dishonesty as a fabricated $0.00: the user gets an answer to a question they did not ask. It now rejects the combination and explains why.

A hardcoded . workspace ignored LOKI_DIR, which is genuinely relocatable (autonomy/serve.sh:224 exports a non-default). loki cost and loki cost --detail would have described DIFFERENT workspaces while appearing to describe one. Both wrappers now derive from LOKI_DIR.

Registered everywhere, because partial registration has bitten three times

Dispatch arm, the All commands help block, both shell completions (bash and zsh), and both --help texts. The repo’s own drift gate passes across 108 dispatch tokens, and help-discoverability passes 4/4.

An empty workspace still reads Total cost: UNKNOWN, never $0.00.

v8.76.0

An opencode-only machine was told it had no AI provider

Two lists name the supported providers, and they disagreed:

providers/loader.sh    auto_detect_provider()  claude cline codex aider opencode
autonomy/provider-offer.sh  detect_any_provider()   claude codex cline aider

detect_any_provider is the PRE-FLIGHT GATE – provider_offer_gate routes through it and cmd_start exits 2 when it fails. So a machine with opencode installed and working got:

No AI provider CLI found; cannot prompt to install in a non-interactive shell.
Run: npm install -g @anthropic-ai/claude-code
No provider available; cannot start a build.

A redundant install instruction for a provider they did not need, and no build. That is a REQUIRED step added before first value, on hardware that was already ready.

The order was wrong too, not just the membership: with codex and cline both installed, quickstart printed Found: codex while the runner would actually select cline.

Why the fix is a name, not a shared function

detect_any_provider is deliberately PATH-only. Its callers (cmd_demo, cmd_quick) stay on the bash route and genuinely need a binary present, so routing it through the richer detector would be a fail-OPEN: a green pre-flight followed by a runner that cannot invoke anything.

The bug was the missing NAME. The list is corrected, the mechanism is untouched, and a new test pins the two lists together – membership AND order, since the order IS the selection priority. Both directions are mutation-proven: dropping opencode turns it red, and so does making the gate always pass.

quickstart now names the provider that will actually run

autonomy/quickstart.sh re-derived its own provider list instead of asking auto_detect_provider. It now consults the single source of truth, so step 1 cannot name a provider the runner would not pick.

v8.75.0

The 3.12 guard reported a defect that did not exist

v8.63.0 added a guard that re-runs the dashboard liveness tests under Python 3.12, because a local 3.14 physically cannot observe the PEP 649 annotation bug that took down two releases. That guard then failed CI’s Python 3.13 job.

Not a real defect. shutil.which("python3.12") on that runner resolves to /usr/bin/python3.12, a SYSTEM python with no pytest installed, so the subprocess exited non-zero for a missing dependency. The guard read that exit code as “the liveness tests fail on 3.12”.

A tool that cannot run is an ABSENT measurement, not a failing one – the same rule the cost work established, applied to a test harness instead of a receipt. It now probes python3 -c "import pytest" first and SKIPS when the interpreter cannot run the check, rather than converting a missing dependency into a false alarm.

It still runs and still passes where pytest exists, so the guard has not been hollowed out. Both directions are asserted, and removing the availability probe turns the suite red.

A cadence hazard worth recording

The Tests workflow sets cancel-in-progress: true grouped by branch and takes about 25 minutes. Releases in this session have been landing every ~5 minutes, so each push cancelled the previous run: Tests completed on none of the last five releases.

The config is correct for normal cadence and is left alone. It is recorded here because “no red X” on those runs means cancelled, not passed, and reading it as a pass would be exactly the kind of absent-measurement-as-success this codebase keeps paying for. The local gate does run the full pytest suite before every push, which is what actually verified them.

v8.74.0

Unmeasured token counts also stopped reading as zero

v8.72.0 fixed total_cost_usd in kpis.ts and deliberately left the TOKEN fields for a follow-up. This is it: total_input_tokens and total_output_tokens reported 0 for an unmeasured run, so a reader could not tell a run that genuinely emitted no output tokens from one where nothing was ever recorded.

Python nulls five keys together on ONE condition (efficiency_cost.py:174-185): usd, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens. Cost and tokens are not separate rules there, and they are not separate rules here now: recordsMeasured() is hoisted once and gates all three totals. No second predicate was introduced.

The half of this bug that is easy to get wrong

A genuinely MEASURED zero must stay 0. The renderer uses ??, never || – 0 is falsy, so a || fallback would blank real data into UNKNOWN, which is its own dishonesty in the opposite direction.

That is what the mutation probe pins. Swapping ?? for || still renders UNKNOWN correctly for the unmeasured case and would pass on that alone; it fails only through the measured-zero assertion, which uses a cache-only fixture (input_tokens: 0, output_tokens: 0, cache_read_tokens: 797496) where the run IS measured and both token totals are honest zeros.

A type-only mutation survived, and is reported as a non-proof

Changing number | null back to number does NOT fail bun test, because TypeScript types are erased and the runner does not typecheck. That was established in v8.72.0 and held again here. It is recorded as a documented non-proof rather than presented as evidence; the three probes that bind all change runtime behaviour.

Two stale notes corrected

The operator note claimed token/duration KPIs zeroed, which this change makes false. It now reads cost UNKNOWN, tokens UNKNOWN, duration zeroed. total_duration_ms stays a real 0: it is wall clock, not provider-reported usage, and Python does not null it either.

v8.73.0

A failed receipt verification now says exactly what is wrong

The Evidence Receipt is the differentiator: of six installed CLIs (opencode, aider, codex, claude, cursor-agent, loki-mode), only loki-mode verifies its own output. A verifier that returns ok: False and nothing else spends that advantage – the user learns the receipt is bad, not why.

verify_integrity() now returns a reasons list naming every failed check:

FAILED
  - hash mismatch: recorded 94b4f487..., computed 981d39c2... -- proof.json was
    edited after it was written
  - cost claim is incoherent: cost.available is true but every token count and
    usd is zero or absent; the receipt claims a measurement it does not have
    (an unmeasured run is unknown, not free)
  - diff drift: the receipt recorded 99 files / +4242 / -1717, the repository
    now has 1 files / +2 / -0

Three failures reported at once. A pre-existing singular reason field was first-wins, and its precedence is asserted by four existing suites, so it was left exactly as-is; reasons is additive alongside it. A --human flag renders the list.

The half-fix this avoided

Building the list only inside verify_integrity() would have left verify()’s own eight failure sites – diff drift, tree drift, unresolvable base – returning ok: False with an EMPTY list. That is the same generic-failure problem relocated rather than fixed, and it would have looked complete. All fourteen sites append.

Verdicts are unchanged, and that is asserted

This explains a verdict; it never changes one. A verifier made more talkative but more permissive would be strictly worse than a quiet one. 244 proof-related tests pass unchanged, and the moat-breaking mutation – forcing cost_coherent to always be true – turns the suite red.

A passing receipt reports an empty list, never a fabricated reason.

v8.72.0

The sixth and last surface that called an unmeasured run free

  v8.51.0  codex dispatch   recorded no tokens at all
  v8.52.0  receipt          {"usd": 0.0, "available": true}
  v8.53.0  the PROMPT       "$0.00" per iteration, steering the agent
  v8.54.0  the verifier     never checked cost, so the claim passed
  v8.69.0  cost summary     the per-run reader
  v8.72.0  kpis.ts          this one

loki-ts/src/metrics/kpis.ts typed total_cost_usd as a bare number and produced it unconditionally, with the empty snapshot seeded to 0. An unmeasured run – efficiency records present, every token and cost field zero, the pre-v8.51.0 codex shape – rendered:

  Total cost USD:       0

certifying the run as free. Now number | null, rendering UNKNOWN (not measured).

The predicate reads the records, not the derived total

recordsMeasured() mirrors record_is_measured from efficiency_cost.py, summing the same five fields on the RAW records. Reading the derived total_cost_usd instead would have been wrong in a way that looks right: that value comes from calculateCostFromRecords(), which prices tokens, so it is non-zero even when no cost was ever observed. It answers “did we compute a number”, not “did we measure one”.

Measured-zero and unmeasured stay distinguishable

cost_usd: 0 with non-zero tokens is MEASURED and stays 0. All five fields zero is UNMEASURED and becomes null. The average uses an explicit === null guard rather than a falsy check, so a genuine measured zero is not blanked into UNKNOWN. Blanking real data would be its own dishonesty.

Mutation-proven at the runtime layer: seeding the empty snapshot back to 0 turns the suite red. Note the type annotation alone is NOT sufficient evidence – TypeScript types are erased, so a type-only mutation survives bun test. The probe that binds is the one that changes runtime behaviour.

Known remaining, stated rather than implied

Python also nulls the TOKEN fields when unmeasured; total_input_tokens and total_output_tokens in kpis.ts still read 0. Same class, deliberately not in scope here.

v8.71.0

loki why gave the same advice for a 401 and a timeout

cmd_why already read .loki/state/LAST_ERROR.json and already printed the error class. What it did NOT have was any mapping from that class to a remedy:

sed -n '/^cmd_why()/,/^}/p' autonomy/loki | grep -cE 'rate_limited|auth_error|build_timeout'
0

So every failure got the same status-keyed line: “Read .loki/logs/ + the handoff below for the failure, fix it, then re-run.” Identical whether the provider returned HTTP 401 or the build timed out.

Now:

  Last error  : auth_error (iteration 3)
                Provider returned HTTP 401.
  What to do  : Re-authenticate claude: claude login   (or set ANTHROPIC_API_KEY)

The honesty call inside it

The LAST_ERROR schema has no provider field. Rather than defaulting to claude and presenting a guess as the fix, auth_error uses LOKI_PROVIDER when set, and otherwise lists every provider’s command while stating plainly that the provider was not recorded.

The map is defined once and injected into both python heredocs cmd_why runs, so the human and --json paths cannot drift. --json also gains an honesty triple (last_error_present / last_error_readable / last_error_recognized) so a consumer can tell “no failure recorded” from “malformed” from “unmapped class” instead of one indistinguishable null.

A latent bug surfaced while wiring this: load() collapsed absent and unparseable into {}, so a corrupt LAST_ERROR.json printed nothing and read as “no failure”. Those are now distinguished.

What a run will cost, before you start it

tools/estimate-run.py projects forward from this workspace’s own measured history, which nothing did before. Three existing surfaces were checked first: cost-summary.py is backward-looking, show_prd_plan() estimates from PRD text rather than measurement, and effort_estimator.py reports hours.

Two findings shaped it:

record_is_measured is field-agnostic, so a record with real tokens but cost_usd: 0 (an unpriced model) returns true. Filtering on it alone would pull a 0.0 into the median and ship a fabricated-low estimate – the exact defect class this guards. The basis is measured AND non-zero cost, and the output reports three counts: found, measured, priced.

No multi-run history exists to derive a horizon from, so --iterations is required rather than invented. With no basis at all it prints NO BASIS and UNKNOWN, never a number. If the basis model differs from the model that would run now, it says the projection may not transfer.

v8.70.0

The libs carrying the receipt verifier had no packaging guard

Four releases were spent discovering that the checks guarding the SHIPPED PACKAGE were themselves unguarded. Everything works from a git checkout, so no in-repo test and no GitHub CI job can see a packaging break:

tests/test-detectors-are-packaged.sh now guards the detectors. Nothing guarded autonomy/lib/*.py – which is where the Evidence Receipt verifier and the cost-honesty rule live. They ship today only because files[] happens to contain a broad autonomy/ entry. Narrowing that entry for any reason would drop them silently, and the failure would surface as a receipt that cannot be verified rather than as an error.

The fast-tier tarball check now asserts nine artifacts individually, adding proof-verify.py, efficiency_cost.py, and cost-summary.py.

Two rules applied, both learned expensively

Assert each thing individually, never a count. A threshold cannot say WHICH artifact vanished. The check’s closing line also stopped hardcoding “all 6” and now counts from the loop, because a literal count goes stale the moment the list grows and then understates what is guarded.

Guard against vacuity. A substring search over an EMPTY listing reports nothing missing. npm pack writes its listing to STDERR, so 2>&1 >file captures build chatter instead and makes every assertion pass. The new suite fails loudly on an empty or non-listing capture, and that path is mutation-proven: forcing the listing to empty turns it red rather than green.

Narrowing files[] from autonomy/ to autonomy/loki also turns it red.

v8.69.0

What did this run cost, and how much of that is honest

autonomy/lib/cost-summary.py reads .loki/metrics/efficiency/ and reports per-run cost, the per-iteration breakdown, the cost trend, and the cache hit ratio (the metric that dominates cost at a 97.5% hit rate).

The whole point is the accounting rule this repo paid four releases to learn: an unmeasured cost reads UNKNOWN, never $0.00. Free and unmeasured are different claims and only one is honest.

A partially-measured run says so, and excludes rather than zero-fills:

Iterations:  3 found, 2 measured  <- PARTIAL
Total cost:  $0.0599
Cache ratio: 77.8%   (cache_read / (input + cache_read))
Cost trend:  CLIMBING (2 measured point(s))
  iter 3: UNKNOWN (excluded from totals)

note: PARTIAL: 2 of 3 iterations measured. The total covers only the measured
ones; unmeasured iterations are excluded, not counted as zero, so the real cost
is HIGHER than shown.

$0.0599 is exactly the sum of the two measured iterations. A fully unmeasured run reads Total cost: UNKNOWN, and --json emits null, never 0.

One definition of “measured”, not two

collect_efficiency() already had the honesty guard, but only in aggregate: it summed everything and then checked the TOTAL, so it could not say WHICH iterations were measured. Rather than write a second rule that can drift, record_is_measured() was extracted as the single definition and collect_efficiency rewired to call it. Both surfaces now run the same code; renaming that function turns both suites red.

The failure mode that survived the first implementation

A record with real tokens but cost_usd: 0 – an unpriced model, the exact shape codex tiers produce – counted as measured on the strength of its tokens and rendered Total cost: $0.0000. The headline rule, inverted.

The original fixtures could not reach it: they zero tokens AND cost, so the predicate short-circuits before the disagreement matters. The cost slot now reads UNKNOWN with an explanatory note while the token counts still render, since blanking real data would be its own dishonesty.

Known remaining surface

loki-ts/src/metrics/kpis.ts types total_cost_usd as a bare number, which is a sixth surface with the same $0.00 shape. Untouched here and recorded rather than implied.

v8.68.0

An interrupted run told you there was nothing to resume

Ctrl-C at iteration 7, then loki resume:

No session to resume.
Start a session with: loki start

Every part of the machinery was already correct. run.sh saves status: "interrupted" on Ctrl-C, load_state deliberately PRESERVES iterationCount for interrupted runs, _loki_resolve_run_status resolves interrupted, and _loki_next_action already maps it to loki resume. The state was saved, the status resolved, the action mapped – and then cmd_resume hit an early exit that only ever handled explicit PAUSE/STOP signal files, and told the user their work was gone.

Because loki next dispatches to cmd_resume, the one command whose job is “do the right next thing” ended at the same dead end. One function was the root cause; fixing it fixed loki next for free. No new command was added.

Now:

Interrupted run found. Stopped at iteration 7 (last activity: 2026-08-02T09:14:02Z).
Saved progress is intact. Resume it with:

  loki start

It picks up from iteration 7; verification re-runs, so nothing inherits a stale PASS.

It reports the real iteration, and this is where the first version was wrong

The hint read iterationCount. save_state() writes iteration (autonomy/run.sh:6707). So every REAL interrupted run would have reported “Stopped at iteration 0” – the one number that makes the message reassuring rather than alarming.

The test suite was green throughout, because its fixture used the same spelling the reader expected. A test that only ever sees its own fixture cannot catch a field-name mismatch with the production writer. It was caught by running the actual command against the shape save_state really emits, and is now pinned by a test that builds its fixture from the production key; reverting the reader turns the suite red.

It stays quiet when there is nothing to resume

Keyed on interrupted ONLY, deliberately excluding running. A SIGKILL or power loss leaves status: "running", which looks resumable, but load_state resets it to iteration 0 unless LOKI_DURABLE_STATE=1. Advertising “resume from iteration 47” there would promise progress the runner is about to discard. Widening the key to include running also turns the suite red.

v8.67.0

doctor now tells you which provider will actually run

loki doctor gained a Provider Availability section on both routes, and a provider_availability key in --json:

Provider Availability:
  PASS  claude - installed (auto-selected)
  PASS  codex - installed
  WARN  cline - not installed
  PASS  aider - installed
  PASS  opencode - installed
  Auto-selected provider: claude (override with LOKI_PROVIDER)

With auto-detection landing in v8.64.0, “which provider will run” became a question the user could no longer answer by reading their own command. This answers it before a run starts.

One renderer, in bash, on purpose

Doctor stdout is compared byte-for-byte between the bash and Bun routes. Two renderers drift; one cannot. The section is rendered by autonomy/provider-offer.sh and the Bun route shells out to the same helper, reusing the seam the install offer and the detect-sdk probe already use.

The ordering trap this pinned down

auto_detect_provider() walks claude cline codex aider opencode, but SUPPORTED_PROVIDERS is declared claude codex cline aider opencode – codex and cline are swapped. Marking “the first installed entry in SUPPORTED_PROVIDERS” would name codex on a machine where a build actually runs cline.

The selected provider therefore always comes from auto_detect_provider(); the declaration order is used only to order the rows. A test pins this with a fake PATH holding cline and codex but no claude, where the two candidate answers differ, and swapping the priority order turns the suite red.

A note on how this was integrated

The work was built on a branch that predated v8.65.0, so its dist/ did not contain the install-integrity check. Taking that dist wholesale would have silently reverted v8.65.0 and returned Bun Parity to red. Only the source changes were applied and dist/ was rebuilt, with both features asserted present afterwards.

v8.66.0

Asking for a provider you do not have now fails immediately, and never substitutes

--provider codex on a machine without Codex used to get most of the way into a run first. The existing guard sat after load_memory_context, the event emit, the cli-provider marker write, and the “Starting Loki Mode… Provider: codex” banner – so the operator watched it announce a provider it was about to reject.

The preflight now runs before any of that, and only for an EXPLICIT choice (--provider or LOKI_PROVIDER):

Error: you asked for provider 'codex', but its CLI is not installed.

Install it:
  npm install -g @openai/codex

Installed providers on this machine: claude
Re-run with one of those, e.g. loki start --provider claude

Loki will not silently run a different provider than the one you asked for.

It never falls back. Quietly running a different model than the one asked for is worse than failing, so substitution is asserted against as its own test and mutation-proven: dropping the exit 1 lets the run fall through to the start path, and the suite turns red.

Why the existing check was not enough, and why it stays

provider_offer_gate passes when any provider is on PATH, so an explicit --provider codex on a claude-only box sailed straight through it. The later check at autonomy/loki:2689 did catch the case, but only after the banner – and it fires on the effective provider, which includes the auto-detected default. Removing it would regress “default claude, only codex installed”, so both remain, with different scopes.

Known gaps, stated rather than implied

v8.65.0

The check that guards the shipped package was missing from the default route

doctor verifies four quality-gate detectors are present. They ship via package.json files[], and when they were absent from the tarball, mutation-integrity failed closed on every iteration for every npm user – first-pass completion was impossible regardless of model output (v8.38.0).

The bash route has checked this since then. The Bun route – the default runtime – did not. The users most likely to hit the packaging failure were exactly the ones whose doctor could not see it.

Bun Parity had been red since v8.59.0 reporting this, as a tally mismatch:

bash: Summary: 14 passed, 1 failed, 14 warnings
bun:  Summary: 10 passed, 1 failed, 14 warnings

A difference of exactly the four detector checks. Both routes now report the same count.

Reproducing it needed the real CI diff, not a local guess

The gap does not reproduce on a dev machine: both routes agreed locally, so a raw diff showed nothing and an early “fix confirmed” would have been wrong. Stripping the change and re-running still passed locally – proof the local signal was not measuring the failure at all.

A first reproduction attempt built a minimal PATH and appeared to reproduce it, showing bash omitting version strings. That was an artifact of the harness: the bash probes call tr, awk, and sed, and the stub PATH omitted tr. With full coreutils the difference vanished. The reproduction was broken, not the product. Only the actual CI log named the real diff.

Also in this release

loki doctor on the Bun route now reports missing detectors as a BLOCKING failure with a reinstall instruction, rather than staying silent.

v8.64.0

You no longer pick a provider. Loki picks the one you have.

loki start ./prd.md on a machine with Codex but not Claude used to fail. Not because Codex is unsupported – it is a first-class provider – but because autonomy/run.sh hardcoded:

LOKI_PROVIDER=${LOKI_PROVIDER:-claude}

so an operator who expressed no preference got a provider they may not have installed, and a failure naming a tool they never chose.

The detection has existed since v5.0.0. auto_detect_provider() sits in providers/loader.sh with the correct priority order (claude > cline > codex > aider > opencode) and its own passing test. Nothing in production ever called it: the only caller in the entire repo was its own unit test. Built, tested, and never wired.

Now, when LOKI_PROVIDER is unset, the default consults it:

[loki] provider: codex (auto-detected)

An explicit choice still wins

This fills an UNSET value only. LOKI_PROVIDER=codex and --provider codex behave exactly as before, including when a higher-priority provider is installed. Silently running a different model than the operator asked for would be worse than the bug being fixed, so that direction is asserted as its own test and mutation-proven: forcing detection to run unconditionally turns the suite red.

With nothing installed at all, the default stays claude so the existing “not installed” error names a real, actionable provider rather than an empty string.

Wiring, not the function

The test asserts the CALL SITE, because the function was never the broken part. It extracts the real selection block out of run.sh and executes it – a re-implementation would test the rule while the production default stayed hardcoded.

That extraction earned its own assertion. An earlier slice cut the block mid-conditional; the resulting shell parse error was swallowed by a 2>/dev/null and every unset case read as empty, which looks exactly like a broken feature instead of a broken harness. The slice is now bash -n checked before any behaviour is asserted.

v8.63.0

Two releases died to one blind spot: the local gate ran the wrong Python

GitHub CI runs Python 3.12. This Mac runs 3.14. .githooks/pre-push invoked bare python3, so every local green was measured on an interpreter CI does not use. Two bugs walked straight through it.

1. NameError: name 'Any' is not defined (5 dashboard liveness tests). tests/dashboard/test_status_registry_liveness.py execs a sliced function out of dashboard/server.py. The from __future__ import annotations at the top of server.py does NOT travel with a slice, and _safe_json_read(default: Any = None) puts an annotation in a default value, which evaluates at def time. Python 3.14 defers annotations unconditionally (PEP 649) and never evaluates it; 3.12 does, and raises. The slice now declares its own deferral.

This is what failed v8.61.0’s Release, which is why v8.61.0 and v8.62.0 were both built but never tagged or published. This release unblocks both.

2. The code index was empty for any checkout under .claude/. tools/index-codebase.py filtered with any(skip in str(p) for skip in SKIP_DIRS) – a substring test against the absolute path. SKIP_DIRS contains .claude, so a worktree at .../.claude/worktrees/<name>/ matched on every file and collected zero Python sources. Now matched by path COMPONENT relative to the project root: 515 -> 581 files, memory/ restored (18 files), skip dirs still excluded.

The failure mode is the dangerous kind: no error, just a code search that quietly returns nothing. It surfaced only on 3.12 – on 3.14 chromadb fails to import and the caller silently falls back to a different file list that happens to include memory/.

The fix is to the gate, not just the bugs

.githooks/pre-push (the tracked hook, wired via core.hooksPath) now prefers python3.12 when present, so a local green predicts a CI green.

scripts/local-ci.sh labels its run the CI interpreter but deliberately runs one interpreter, not two: 3.12 is the pass that predicts CI, and the fast tier is the release gate kept short for cadence. A second blanket run costs ~92s to protect a runtime CI never uses.

Mutation-proven both directions: reverting the substring filter turns the suite red, and neutering the filter entirely (so node_modules leaks in) also turns it red.

Trust-core detector: 93 invariants.

v8.62.0

Why we are slower: 10.65M tokens in, 34.7K tokens out

Measured on one real iteration of GitHub issue #24:

cache_read : 10,651,759 tokens
output     :     34,729 tokens
ratio      :        307 : 1

in a single provider call – one iteration_start, one result-cost-1.json, so cross-iteration reuse is ruled out. That call was 100% of measured stage time (744s to first code change).

The gap is ~6x on time-to-first-signal, not 100x. Replit shows a preview at ~120s against our 744s; on first working version (~600s) it is 1.2x. Stating that plainly matters more than a dramatic number.

What is NOT wrong

The provider cache is working: 97.5% hit rate, already discounting the call 10x ($31.96 -> $3.20). We are not missing a cache.

The problem is that the order being discounted is enormous. Cached reads are still 67% of a $4.74 iteration, and they are prefill the model must process serially before emitting a single character – which makes this the only measured lever touching both cost and the 744s.

Three other candidate levers were adversarially refuted and are recorded so they are not re-proposed: reviewer-prompt cache sharing (<3% of run cost, and mechanically inert – no user-turn cache flag exists on the bash route), LOKI_SDK_PROMPT_CACHE (reaches only the one-shot judge path), and wiring [CACHE_BREAKPOINT] (a documentation anchor with no provider-side effect).

This measures. It does not trim.

“The tool loop re-accumulates history” is inferred from the ratio, not observed per turn. Trimming context on an inference is how you ship an agent that forgets what it already tried and redoes the work – raising iteration count and costing more than it saves.

So per-turn usage is now recorded to .loki/metrics/context-growth-<n>.json with a growth_factor (first turn vs last turn within one call). Verified end-to-end against the real stream parser: a 50k -> 1.6M turn sequence yields growth_factor: 32.0.

The cut is a separate decision, gated on iterations-to-done, never on a token count.

Absence is not a growth factor

A run with no assistant turns writes no record, rather than a fabricated 0. Gated on turns observed, not on cost being reported – codex reports tokens and never dollars, so tying it to cost would lose the shape on every codex run.

The embedded parser is now syntax-checked

The stream filter is a python3 -u -c '...' heredoc inside run.sh. A syntax error there breaks every Claude-route run, and bash -n cannot see it. The test extracts and ast.parses it; breaking if _turn_usage: turns it red.

Trust-core detector: 93 invariants.

v8.61.0

W3: the call costing 93% of the run never reported its own input

Measured on a real run: the agent call is 1814s of 1941s wall clock. Every code reviewer logs its prompt bytes – “Reviewer security-sentinel: prompt 160074 bytes” – and the dominant call logged nothing. Searching run.sh for an agent prompt-size metric returned zero.

Why prompt size specifically. We cannot make the provider faster and we cannot train a model. The market’s own finding is that “the same model in a different harness routinely drops 10-15 points” – the harness is the lever, and prompt size is the input side of that 93%. Send less, pay less, wait less.

Without the number a regression is invisible: a prompt that grows 40% surfaces only as latency and cost with no attributable cause – the same shape as the cost arc, where four surfaces reported $0.00 because nothing measured tokens.

Now emitted with the agent stage, carrying bytes and duration_s so size and latency can be correlated, and rendered by measure-run.sh:

agent prompt      : 200 KB median (160-200 KB over 2 call(s))

Costs one wc -c on a string already in memory – no subprocess, no file read.

Absent is not zero

A run predating this emits nothing, and the report says not recorded, not 0 KB. A fabricated zero would be averaged into later analysis as a real observation. Both directions mutation-pinned: removing the emit, and rendering 0 KB for a run with no data, each turn the test red.

Verified against the real FireLater workspace, which predates the event: it correctly reports not recorded rather than inventing a figure.

A harness bug caught before it became a false verdict

The first version of the test used grep -B12 and reported three failures against correct code – the emit is a multi-line continuation, so its arguments live after the matched line while the guard lives before. A one-directional window cannot see both. Fixed to span -B26 -A6.

That is the fourth time this session a harness bug produced a verdict about code that was actually fine. Verifying the tool before believing the verdict is what caught it each time.

Trust-core detector: 91 invariants.

v8.60.0

One missing measurement must not become four different lies

A single defect – codex writing no token usage – surfaced independently on four surfaces, and every one of them turned “we did not measure” into “it was free”:

release surface the claim it made
v8.51.0 codex dispatch recorded no tokens at all
v8.52.0 Evidence Receipt {"usd": 0.0, "available": true}
v8.53.0 the prompt $0.00, steering the agent
v8.54.0 the verifier never checked cost, so the claim passed

Each was fixed in isolation and each has its own unit test. None of those tests can see a fifth surface drift, or one of the four regress in a way its neighbours mask.

This asserts the property they share:

An unmeasured cost reads as UNKNOWN on every surface. Never as $0.00.

Verified end to end in both directions on a real record shape:

measured    receipt $0.0187  |  prompt "$0.02, cache 54%"  |  verifier ok
unmeasured  receipt None     |  prompt "90s, completed"    |  verifier rejects

Why the opposite direction is tested just as hard

A guard strict enough to suppress genuine data would blank the cost surface permanently – its own dishonesty, and it would make the 20x cost-per-resolved-issue spread unmeasurable again, which is the axis a buyer compares first.

So the measured case asserts the cost still appears, the cache ratio survives, and the duration is not dropped along with it.

One test, three surfaces caught

Mutating any of the three fixable surfaces turns this single test red: reverting the receipt guard, restoring $0.00 in the prompt, or disabling the verifier check. The fourth (capture) is pinned by asserting the helper exists, is called by the runner, and that every shipped codex tier is priced.

That is the difference between four tests that each guard a symptom and one that guards the property.

Trust-core detector: 89 invariants.

v8.59.0

Pinning the escalation chain end to end

v8.58.0 wired three dead gates into the escalation writer. This verifies the signal those gates now emit actually reaches the agent, and pins the contract so it cannot silently break.

The chain, verified link by link:

write_gate_escalation_guidance          (writer)
  -> .loki/signals/GATE_ESCALATION.json (signal)
  -> build_gate_escalation_context      (reader)
  -> gate_escalation_context            (the PROMPT)

All five JSON keys match exactly – action, count, gate, threshold, latest_artifact – verified by extracting both sides from the source rather than assuming.

Why a matching-keys test earns its place

This failure mode is invisible. Rename a key on either side and the signal is still written, still read, and simply carries nothing. The agent receives an escalation with no findings attached, which looks identical to no escalation at all – no error, no log line, no failing test.

Both key sets are now derived from the code and compared, so a rename on either side turns the test red. Verified: renaming latest_artifact to artifact_path on the reader alone fails it.

A dead-branch sweep, recorded as clean

The v8.58.0 lesson – derive the expectation from the code, because a hardcoded list passes over the case you did not think of – was applied to the rest of the name-dispatch surface: _loki_surface_why_hint, _loki_build_self_heal_hint, _loki_supervised_source_hints. All three have live callers.

No fifth dead branch exists. Recorded as a finding rather than turned into a change that was not needed.

Trust-core detector: 87 invariants.

v8.58.0

Escalation guidance was dead for three of the four gates it handles

write_gate_escalation_guidance maps four gates to the findings artifact that explains their failure:

code_review        -> quality/reviews/<latest>/
mutation_integrity -> quality/mutation-findings.txt
mock_integrity     -> quality/mock-findings.txt
test_coverage      -> quality/test-results.json

Only code_review ever called it. The other three branches could never run – code that reads as a feature and executes as nothing.

Measured, not theoretical: on a real run mock_integrity failed three times (more than any other gate) and .loki/signals/GATE_ESCALATION.json was never written. The agent was told the gate failed and never handed the findings file that says WHY.

That is the 56% “did not attempt to recover from an error” failure shape (SlopCodeBench, arXiv 2603.24755) – manufactured by our own harness rather than by the model.

The test found a fourth one I had missed

I wired mock_integrity and mutation_integrity, then wrote the test to derive the handled set from the writer rather than hardcode it. It immediately failed on test_coverage – a fourth dead branch I had not noticed.

A hardcoded list would have passed and left it dead. That is the entire argument for deriving the expectation from the code under test: it covers the case you did not think of, which is the only kind that survives review.

Guards on the new call sites

Trust-core detector: 86 invariants.

v8.57.0

111 seconds of startup were completely unmeasured

Timeline from a real run, relative to the first event:

+   0s  session
+  17s  session_start
+ 128s  iteration_start      <-- 111 seconds unaccounted for
+ 872s  iteration_complete

Over two minutes in which the user sees nothing and no agent work has begun – and .loki/events.jsonl recorded not one stage for any of it. Counting startup instrumentation returned literally 0.

This is the felt-latency surface. Time-to-first-signal on the same run was 744s, against Replit’s ~2 minutes to a visible preview. You cannot shorten an interval you cannot attribute – the same gap W1 fixed for cost, on the surface a user notices first.

spec_interrogation_run calls the provider during startup, making it the prime suspect for the bulk of that window. It is now timed through the existing emit_stage_complete channel, so measure-run.sh and every other consumer pick it up with no new plumbing.

A missing start epoch emits nothing, never a zero

The same honesty rule as the cost work: a fabricated duration is worse than an absent one, because it would be averaged into later analysis as real data. Mutation-pinned in both directions – removing the timing, and making a missing epoch produce duration_s=0, each turn the test red.

Correction to v8.50.0

That release reported “first code change: not recorded – the v8.35.0 signal did not fire on its first real exercise.” That was wrong. The reading came from a pre-v8.35.0 run. On the current run the signal fires correctly:

.loki/state/first-artifact.json -> {"seconds_to_first_artifact":744,"iteration":1}

and measure-run.sh renders first code change : 744s. v8.35.0 works end to end. The 744s it measures is the real finding.

Trust-core detector: 85 invariants.

v8.56.0

The dashboard showed STOPPED while the build was running

Reported from a real run. A build launched with loki start <github issue> was progressing normally – prerequisites passed, PRD parsed, 6 tasks extracted – while the dashboard showed:

SESSION: STOPPED     PHASE: BUILDING     UPTIME: 1m 14s

STOPPED next to BUILDING and a live uptime counter is self-contradictory, and it is the first thing a user sees about their own run.

Root cause: the fix was applied to one of two duplicate code paths

server.py computed liveness twice, independently:

surface function had .loki/pids/?
WebSocket stream _push_loki_state_loop yes
REST /api/status (what the UI renders) get_status no

A prior fix added .loki/pids/ as a third liveness source for CLI-started runs and reached the WebSocket path only. /api/status still decided from loki.pid and session.json – and a CLI run writes neither (run.sh only UPDATES session.json when it already exists), so both checks missed.

Two decisive symptoms, both observed on the live run:

Now one shared _registry_run_alive() called by both, so they cannot diverge again.

The kind filter is load-bearing

.loki/pids/ also registers the dashboard itself, the status monitor and the resource monitor – none carrying a kind. Accepting any live pid would let the dashboard’s own PID prove the run alive, trading a false STOPPED for a permanent false RUNNING. That is the worse failure: a dead or stuck run would look healthy forever. Both directions are mutation-pinned.

Verified independently against the live workspace (_registry_run_alive -> True on the running build) and across four synthetic cases including the no-kind dashboard pid.

Trust-core detector: 83 invariants.

v8.55.0

A raw shell error was the first thing users saw

Reported from a real loki start run. Before any Loki output:

.../autonomy/crash.sh: line 216: .loki/config: Is a directory

loki_show_disclosure_once appends a back-compat sentinel to ${LOKI_DIR:-$HOME/.loki}/config. During a build LOKI_DIR is the PROJECT .loki, not ~/.loki – and a project’s .loki/config is legitimately a directory. Appending to a directory fails.

2>/dev/null could never have suppressed it. A redirection failure is reported by the SHELL, before the redirection that would silence it is applied. The 2>/dev/null || true already on that line was decorative.

Now guarded: a directory is a reachable, non-exceptional state, so the write is skipped. The sentinel is back-compat only, so the cost of skipping is zero – the cost of printing a line 216: error on a user’s first screen is trust.

Both directions pinned: silent on a directory, sentinel still written (once, idempotently) when config is a file.

W3: the prompt cache had no guard

The prompt splits at [CACHE_BREAKPOINT] into a cache-stable prefix and a volatile tail. The prefix is currently clean – and nothing tested it.

The stakes are money, not style. Cache reads price at 0.1x input and are ~98% of input volume on the measured shape, so leaking $iteration, $retry, $gate_failures or the PRD body into the prefix is close to a 10x input-cost increase on every iteration. And the agent call is 93% of wall clock (1814s of 1941s, measured), making this the dominant cost lever we actually control – we cannot train a faster model, but we can stop paying full price for a cache we already built.

The failure is silent: the run still succeeds, just costs multiples more, forever, with nothing in any log saying so.

tests/test-cache-breakpoint-discipline.sh asserts no per-iteration value is referenced between <loki_system> and [CACHE_BREAKPOINT], that the prefix holds real content (so the check cannot go vacuous if the split moves), and that the volatile <dynamic_context> tail still exists. Verified by leaking $iteration into the prefix – the test goes red.

Trust-core detector: 81 invariants.

v8.54.0

The moat had a blind spot: the verifier never checked cost

benchmarks/results/competitor-verify-surface.json records the measured competitive claim: of six installed coding-agent CLIs – opencode, aider, codex, claude, cursor-agent, loki-mode – only loki-mode verifies its own output. That is the differentiator.

A moat with a blind spot is not a moat. loki proof verify checked the integrity hash, the diff, the gates and the headline. It never looked at cost, so a receipt could assert any spend – $0.00 or $10,000 – and pass.

This was not hypothetical

A real FireLater receipt shipped with:

"cost": {"usd": 0.0, "input_tokens": 0, ..., "available": true}

A shareable document asserting the run was free. Its integrity hash was VALID – it passed the old verifier completely, because the collector keyed availability on a record file existing rather than carrying data (v8.52.0). Nothing downstream could see it.

Replayed against the real artifact: hash_ok=true, cost_coherent=false, ok=false. A cryptographically intact receipt asserting a false fact, now caught.

What it checks, and what it deliberately does not

Internal coherence – the receipt must not contradict itself:

It does not re-price the run. That would need token counts and a price table at verify time, and a verifier that guesses is worse than one that abstains. A receipt with no cost block abstains rather than fails – older receipts predate the block and are not forgeries.

Why this over the speed work

W3 targets the agent call at 93% of wall clock, which is real. But a receipt that can certify a false fact undermines the one claim no competitor can make. Fixing the moat outranks optimising throughput.

Trust-core detector: 80 invariants.

v8.53.0

The prompt was telling the agent its work cost $0.00

W4 in docs/WANG-PRINCIPLES-PLAN.md – the half of Wang’s principle 5 that turns telemetry into an actual feedback loop.

Good news first: the loop is wired. iteration_attribution.py --prompt-block renders an efficiency trend and run.sh:19909 injects it into the prompt. Rendered against the real FireLater run:

EFFICIENCY TREND (your own last 3 iteration(s); steer on it):
  iter 2: 226s, $0.00, failed
  iter 3: 1730s, $0.00, completed
  iter 4: 86s, $0.00, completed

Every iteration said $0.00. This text goes INTO THE PROMPT, so it was not merely misreporting a number – it was actively teaching the agent that its work costs nothing, and that belief survives into every downstream decision it makes about effort and iteration.

The honesty rule was already written at the top of that same file – “cost not recorded for any iteration (reported as null, not as $0.00)” – and only the SUMMARY path honoured it. The per-iteration line rendered any numeric cost, including 0.

Now omitted when unmeasured. A missing field reads as unknown; a printed $0.00 reads as a fact.

Three cases pinned, including the boundary

Same root defect, third surface

v8.51.0 fixed codex recording no tokens. v8.52.0 fixed the receipt claiming usd: 0.0, available: True. This is the same fabricated zero reaching the prompt. One missing measurement, three surfaces – exactly the half-shipped pattern W5 exists to stop.

Trust-core detector: 78 invariants.

v8.52.0

The receipt was claiming runs cost $0.00

W2 in docs/WANG-PRINCIPLES-PLAN.md – and a receipt-integrity defect on its own terms.

collect_efficiency feeds the cost block of the Evidence Receipt. Its guard keyed on whether a record FILE could be parsed, not on whether the record carried any data. So a run whose efficiency records were all zeros produced:

{'usd': 0.0, 'input_tokens': 0, ..., 'available': True}

The receipt asserting the run cost nothing. The Evidence Receipt exists to prevent exactly that kind of fabricated fact.

Measured, not hypothetical: the real FireLater run wrote four efficiency records with every token field 0 (codex emitted no usage before v8.51.0), and this function reported usd=0.0, available=True.

A run that did work necessarily consumed tokens. All-zero therefore means we failed to measure, not it was free – different claims, and only one is honest. available now requires an OBSERVED non-zero value.

Both directions guarded

A guard so strict it suppressed genuine data would blank the cost block permanently, which is its own dishonesty. Three cases pinned:

W2’s real blocker, recorded honestly

benchmarks/datasets/swebench-lite.json is a placeholder ("status": "PLACEHOLDER", "problems": 300 is a count, not data), swebench is not installed, and a real run is ~300 problems x ~2 iterations x ~900s = ~150 hours of wall clock plus hundreds of dollars at the measured ~$14/pass.

That is a founder spending decision, not an engineering one, so it was not launched autonomously. What WAS blocking it in code – a cost aggregate that could not distinguish free from unmeasured – is fixed here, so cost-per-resolved-issue becomes computable the moment a real eval runs.

Trust-core detector: 77 invariants.

v8.51.0

W1: codex runs recorded NO tokens and NO cost

First item of docs/WANG-PRINCIPLES-PLAN.md. Wang states the thesis conditionally – “the right agentic loop AND the right eval or metric for the agents to optimize”. We had the loop and not the metric.

Measured on the real FireLater run: every efficiency record showed input_tokens=0, output_tokens=0, cost_usd=0. Not just cost – nothing at all. _read_iteration_cost looks for .loki/metrics/result-cost-<n>.json or .loki/context/tracking.json, and codex writes neither, so the whole chain silently resolved to zero.

A zero is a claim that the iteration was free.

That made cost-per-resolved-issue unmeasurable – the axis with a measured 20x industry spread (~$14/pass on Sonnet 4.5; an open harness lands tasks at ~1/20th Devin’s cost) and the number a buyer compares first.

Recovered from the session rollout, not from --json

codex reports usage on turn.completed, but only under codex exec --json. The dispatch pipes stdout through tee into the logs the runner parses for completion signals, so switching to JSONL would change the format every one of those readers depends on.

codex also persists ~/.codex/sessions/**/rollout-*.jsonl carrying total_token_usage. Reading that is a side channel with zero risk to the pipeline. Verified against a live call: 20420 input / 11008 cached / 23 output.

End to end, cost now resolves to 0.018719 where it read 0.

unknown != free

The property the feature exists for. A model we cannot price records no cost field at all, never 0. Both directions are mutation-pinned: emitting 0.000000 for an unpriced model turns the test red.

Also fixed: codex reports input_tokens inclusive of cached, and billing splits them. Reporting the inclusive figure overstates cost, so the helper emits the uncached remainder – also mutation-pinned.

Pricing added for the shipped codex tiers (gpt-5.6-sol / -terra / -luna); only gpt-5.3-codex was priced before, so current runs could not be costed at all.

Trust-core detector: 76 invariants.

v8.50.0

A real run found two defects my tests could not

Re-ran FireLater issue #24 on v8.49.0 against the v8.8.0 baseline. Measured:

  baseline v8.8.0 v8.49.0
iterations 3 2
mutation_integrity 3 of 3 fail, 0-1s each 2 fail, 3s each
code_review 281s median 1s (skip fired)
agent share of wall clock – 93% (1814s of 1941s)

The detector fix is confirmed on a real run: mutation_integrity went from failing in 0-1 seconds every iteration (detector missing, gate could never pass) to executing properly. Iterations dropped 3 to 2.

Then the run exposed two defects in the F0 work itself.

THE HEADER TRAP – F0 was comparing a banner, not a cause

mutation-findings.txt and mock-findings.txt both open with a static banner:

# Test mutation findings (HIGH blocks this iteration)

byte-identical on every run. F0 took head -1, so gate-stuck-mutation_integrity.last had recorded the banner. Two completely different findings therefore compared EQUAL, and the valve would abort a run that was making real progress through different findings each iteration – the one direction this valve must never fail in.

Now takes the first non-comment, non-blank line and strips ANSI colour (the detectors emit it, so the same finding rendered with and without colour would otherwise compare unequal). Verified against the real artifacts: extracted cause is now an actual [HIGH] frontend/src/... finding.

mock_integrity was the top failing gate and was not wired

It failed 3 times in this run – more than any other gate – and was not connected to the stuck check, so an unfixable mock problem could grind indefinitely. Now wired, making three gates covered.

What this says about the plan

agent is 93% of wall clock (1814s of 1941s). Every speed knob shipped in v8.33.0-v8.35.0 targets the remaining 7%. docs/WANG-PRINCIPLES-PLAN.md says this plainly and blocks further gate-latency work until the agent call is measured.

Also unfixed and recorded: first code change: not recorded – the v8.35.0 first-artifact signal did not fire on its first real exercise.

Trust-core detector: 74 invariants.

v8.49.0

F4: the 1000-iteration default was an 8.3-day runaway ceiling

Last item of docs/FIRST-PASS-COMPLETION-PLAN.md. I had parked this as “needs a founder decision”. Measuring instead of asking resolved it.

Measured, not chosen. Real per-iteration wall clock across every recorded run: median 718s, max 1746s. So LOKI_MAX_ITERATIONS=1000 was an 8.3-day ceiling – and the only backstop, because the other two valves ship disabled:

LOKI_BUDGET_LIMIT  defaults to ""  -> check_budget_limit returns immediately
LOKI_MAX_DURATION  defaults to 0   -> check_max_duration "never stop"

It also contradicted our own documentation: SETUP.md tells users to RAISE the budget for large work with LOKI_MAX_ITERATIONS=40, and the demo uses 10. The shipped default was 25x the documented “large” value.

What real runs actually use: 1, 1, 3, 4 iterations. Every one terminated completed via council approval or a completion promise; none hit a cap. Those evidence-driven terminals are untouched – the cap is a backstop, not the mechanism.

New default 25: 6x the observed maximum, deliberately generous because the research is explicit that a too-small cap fails runs whose approach was sound (1-2 fails even when on track; 5-10 recommended). Retune with LOKI_MAX_ITERATIONS_DEFAULT.

Two guards, both mutation-pinned

Hitting the cap still records max_iterations_reached with exit 20. A bounded cap is only safe because reaching it fails honestly; if it exited 0 this would convert long runs into fake successes – worse than the runaway it replaces.

The test’s own copy hid a real defect

The explicit-setting mutation SURVIVED at first. The test drove a hand-copied resolution chain, so mutating run.sh changed nothing it executed – while the real defect was severe: an explicit LOKI_MAX_ITERATIONS=200 silently became

  1. The probe is now sliced out of run.sh itself.

Fourth time this session a synthetic harness hid a real defect. The rule holds: a re-implementation tests the RULE; only the source tests the CODE.

Plan status: all items closed. F0, F2, F3, F4 shipped; F1 was already built.

v8.48.0

F0 covered one of the three gates that actually cause extra iterations

v8.47.0 fixed F0’s off-by-one by replaying a preserved artifact. Applying the same method to the OTHER preserved runs exposed a scope gap: F0 was wired only to mutation_integrity.

The preserved gate-failure-count.json files say which gates really cost iterations:

FireLater   mutation_integrity 3   code_review 2   static_analysis 1
anonima     code_review 1                          (4 iterations)

So F0 covered one of three. static_analysis is now wired too.

The JSON artifact would have made the valve silently dead

static-analysis.json records its cause in a summary field – and also carries a timestamp that differs on every run. A whole-file compare would therefore never match, and the valve would have been dead on this gate while passing every test written against the plain-text shape.

The helper now extracts a stable cause per artifact shape: first line for text, summary for JSON. Verified against the real FireLater static-analysis.json – extracted cause "Syntax error: docker-stop.sh. shellcheck (error severity): docker-stop.sh.", aborting at iteration 3 with the timestamp correctly ignored, and a genuinely changed cause still iterating.

code_review is deliberately NOT wired

It caused extra iterations in both runs (FireLater x2, anonima x1), so it looks like the obvious next target. It persists no single comparable cause – reviews land as per-reviewer artifacts under .loki/quality/reviews/<id>/, and a council’s verdict legitimately shifts between iterations even when one finding repeats.

Inventing a synthetic key would risk aborting a run that was making progress, which is the one direction this valve must never fail in. Recorded as a known limitation rather than guessed at.

The repo’s own checker caught my heredoc footgun

The first version used python3 -c "..." with a multi-line body. The repo has a checker for exactly this (check-heredoc-dollar-digit.sh) and it failed the push – flagging local gate_name="$1" in the NEXT function, because an unterminated double-quoted -c body makes everything after it look like it is still inside the program.

Now a QUOTED heredoc, which also guarantees bash performs no expansion inside the Python at all. The gate caught a real footgun before it shipped.

Trust-core detector: 73 invariants.

v8.47.0

F0 would have missed the exact run it was built for, by one iteration

v8.45.0 shipped the stuck-gate abort with 11 passing tests and three caught mutations. Then I replayed it against the preserved FireLater artifact – the real .loki/quality/mutation-findings.txt from the run that motivated the feature – and it did not fire:

iteration 1 -> continue
iteration 2 -> continue
iteration 3 -> continue      <- FireLater ended here
iteration 4 -> ABORT         <- one too late

Cause: the failure reason was recorded only after the threshold check, so the first comparison could not happen until count == threshold + 1. With a threshold of 3, the abort landed on iteration 4. FireLater ended at 3.

The valve was correct in every unit case and useless on the one real case.

Fixed by recording the reason on every failure and comparing only at threshold. Re-replayed against the same artifact: aborts at iteration 3.

Why the tests did not catch it

The unit cases drove synthetic counts out of order (probe 3, then probe 3 again) which happened to prime the prior. Real iterations arrive 1, 2, 3 – an ordering the tests never exercised.

A regression case now replays that exact sequence and asserts the abort lands on the threshold iteration, not past it. Restoring the off-by-one turns it red.

The generalisable rule, and the third time this session a synthetic harness hid a real defect:

Unit cases prove the RULE. Only the recorded artifact proves the CASE. If a preserved artifact from the motivating failure exists, replay it.

No behaviour changed for healthy runs: below-threshold, first-sighting, changed-reason, missing-file, empty-file and opt-out all still continue, and a stuck gate still exits 20 rather than reporting success.

v8.46.0

F3: the agent was never told which gates would judge it

The first-pass directive already said “self-verify by running, not by reading” – the right instruction, and it named zero of the gates that actually block.

That matters because of what our own telemetry shows. Across every recorded run, every multi-iteration run had a failing gate and every single-iteration run had none. Iterations are gates rejecting work, not the model failing to finish. An agent cannot aim at a gate nobody named.

Iteration 1 now names the four, with the concrete failure mode for each rather than a label:

Those are not an arbitrary list: mutation_integrity, code_review and static_analysis are the only gates that have ever failed here.

Iteration-1 only, and byte-mirrored

Repeating it every iteration would waste tokens and risk drifting into the cache-stable half of the prompt (split at [CACHE_BREAKPOINT]), busting the prompt cache every pass. Asserted: present at iteration 1, absent at 3, with the autonomy preamble still shipping on both.

The directive is byte-mirrored into loki-ts/src/providers/claude_flags.ts. Editing one route alone silently diverges the two runtimes – deleting the mutation-integrity line from the Bun copy turns the test red.

A mutation that survived, and what it taught

Rewording the header to “5. Do your best.” left all four gate names in place, so every name assertion still passed while the list stopped being an instruction. The substance and the framing are separate properties, and a test that checks only the list cannot see the difference. Both are now pinned.

Trust-core detector: 71 invariants.

v8.45.0

F0: stop grinding against a gate that can never pass

The item that would have saved the founder’s FireLater run.

That run spent 3 iterations against mutation_integrity, which failed in 0-1 SECONDS each time with the identical line:

[HIGH] mutation detector unavailable: .../tests/detect-test-mutations.sh

The detector was never packaged (fixed v8.38.0), so the gate could never pass. The run was doomed at iteration 1 and nothing noticed – it just kept going, burning paid provider calls to re-derive the same verdict.

A gate that fails for a byte-identical reason now stops the run with a named terminal status instead of iterating.

The distinction this turns on

“Failed 3 times” is not the signal. A gate failing three times for three DIFFERENT reasons is the loop working – the agent fixes one thing and finds the next. Only an unchanging reason means no progress is possible.

Both directions are tested, and the protective case (changed reason keeps iterating) is mutation-pinned: making the check ignore whether the reason changed turns the test red.

Fail-safe direction, deliberately

On any doubt – missing reason file, unreadable, empty, first sighting, below threshold – the helper returns “not stuck” and the run continues exactly as before. It can only ever SHORTEN a doomed run, never stop a healthy one.

And it never reports success: a stuck gate maps to exit 20 (terminal failure) with gate_stuck_mutation_integrity recorded. A stuck gate that exited 0 would report a doomed run as done – strictly worse than grinding, and that is mutation-pinned too.

Threshold is 3, not 2: a reason can legitimately repeat once while the agent is mid-fix. LOKI_GATE_STUCK_THRESHOLD / LOKI_GATE_STUCK_ABORT=0 to tune or disable.

Trust-core detector: 69 invariants.

v8.44.0

F2: the feedback loop could stop feeding back, silently

First item shipped from docs/FIRST-PASS-COMPLETION-PLAN.md.

Findings injection is what tells the next iteration what to fix. It defaults ON – but the call was gated on command -v bun, so on a machine without bun it degraded with no signal at all. The agent was told only that it failed.

That is the single largest failure shape in the research: “the agent did not attempt to recover from an error” accounts for 56% of agent failures (SlopCodeBench, arXiv 2603.24755). A silently-dead feedback loop manufactures exactly that – and misattributes it, because the next iteration then looks like the model failing when it was never told what went wrong.

Now it warns, names the consequence rather than just the missing binary, and records a capability_degraded event so the receipt can show it. Opting out (LOKI_INJECT_FINDINGS=0) stays silent – a warning for a deliberate choice is noise, and noise trains users to ignore the channel carrying the real warning.

Why this is the first F-item shipped

Our own telemetry, across every recorded run:

project iterations failing gates
FireLater 3 static_analysis 2, mutation_integrity 3, code_review 2
anonima 4 code_review 1
autonomi-engine-runs 1 none
loki-mode 1 none

Perfect correlation: every multi-iteration run had a failing gate; every single-iteration run had none. Iterations are not the model failing to finish; they are gates rejecting work. So the quality of what the agent is told after a rejection is the first-pass lever.

One assertion was reading the payload, not the call

The wiring check first matched the literal string capability_degraded. A mutation replacing emit_event_json with false left that argument in place, so the assertion passed while nothing was emitted. Now matches emit_event_json "capability_degraded" – the call, not its payload.

Trust-core detector: 66 invariants.

v8.43.0

doctor could not see the worst breakage this package has shipped

The quality gates shell out to detector scripts under tests/. Before v8.38.0 those were never packaged, so every npm install had zero detectors and four gates fail-closed on every iteration – correct behaviour, but it means the build can never pass and first-pass completion is impossible regardless of model output.

loki doctor reported a healthy system the entire time. It only ever checked for external COMMANDS (git, node, bun) – never for the files this package is supposed to contain. A broken install and a healthy one were indistinguishable from the one command meant to tell them apart.

doctor now has an Install integrity section:

Install integrity:
  OK    Quality-gate detectors present (4/4)

and on a broken install:

  FAIL  Quality-gate detectors MISSING: detect-test-mutations.sh ...
        These gates fail-closed, so every iteration will be blocked.

with Incomplete install: ... Reinstall: bun install -g loki-mode in the blocker list – the same funnel-fix contract doctor already follows for every other hard failure: name it, say what it costs, give the exact command.

Verified in both directions

Tested against the real CLI on the exact shape 8.8.0 shipped – a tests/ directory that exists but is empty. A check that cannot fail is decoration, so the negative case is the one that matters, and both halves are mutation-pinned: removing the blocker registration, and short-circuiting the file check to always-present, each turn the test red.

Trust-core detector: 64 invariants.

v8.42.0

A stale install was invisible on the command people actually run

The update hint existed and worked – wired into loki doctor and loki version. Neither is run before a build, so a stale install stayed invisible until someone went looking.

This machine was 4 releases behind (8.8.0 vs 8.41.0) while running builds, and that gap was not cosmetic: v8.38.0 fixed four quality-gate detectors that had never been packaged, so on 8.8.0 mutation-integrity fail-closed on every iteration and first-pass completion was impossible regardless of model output. The tool gave the user no way to know.

loki start now prints the one-line nudge. Cheap and fail-silent by construction: the result is cached 24h, the registry call has a 1.5s hard timeout, and CI / non-TTY / LOKI_NO_UPDATE_CHECK=1 all return without output.

The first draft was silently dead

I wrote it as:

maybe_print_update_hint 2>/dev/null || true

The hint prints on stderr. Redirecting stderr discards the only thing the call produces – a defensive habit that turned a feature into a no-op which reads as correct in any source review, and which no existing test could see because a suppressed message and an absent message are identical.

Both are now pinned by mutation: re-adding 2>/dev/null, and removing the call outright, each turn tests/test-start-update-hint.sh red.

Two test-harness bugs found on the way

Trust-core detector: 62 invariants.

v8.41.0

Two more packaging checks were deferred, and one of them was broken

v8.40.0 found the dist-freshness gate deferred by the tier that justifies it. The obvious follow-up: are the OTHER local-only packaging checks skipped too?

Yes – npm pack tarball contents and the Agent SDK dependency check. Both validate the PACKAGED ARTIFACT, which GitHub CI never inspects and no in-repo test can see, because everything works fine from a git checkout.

npm pack tarball contents is exactly the check that would have caught v8.38.0 – four quality-gate detectors that never shipped, making first-pass completion impossible for every npm user. It was being skipped at push time. The SDK check’s own comment says it “would have caught the whole-arc council’s packaging finding”. Also skipped.

Both promoted. Measured cost: npm pack 1.6s, SDK check 21ms; the whole fast tier runs in 35s.

The check itself tolerated losing a third of its artifacts

Promoting it exposed a defect in it. The old form was:

... | grep -E "<six patterns>" | wc -l | grep -qE "[6-9]|[1-9][0-9]"

That means “6 or more, or any 2-digit number”. With everything present the count is 8 (some patterns match twice), so the guard tolerated losing TWO required artifacts. Verified: deleting the autonomy/ entry from files[] drops provider-offer.sh and quickstart.sh from the tarball – the count falls to 6 and the check still passed.

A count threshold also cannot say WHICH artifact vanished.

Now each of the six is asserted individually, the failure names the missing paths, and the listing is vacuity-guarded first (npm writes it to STDERR, so a reversed redirect captures build chatter and makes every assertion vacuous – the same false-alarm mechanism recorded in v8.39.0).

Verified: the same break now exits 1 with MISSING FROM TARBALL: autonomy/provider-offer.sh autonomy/quickstart.sh.

v8.40.0

The dist-freshness check was deferred by the tier it justifies

CLAUDE.md names dist freshness the sharpest reason the fast tier exists:

CI never validates that the committed loki-ts/dist/loki.js matches src, and when that slipped we shipped THREE releases reporting the wrong version.

The check was in local-ci.sh – and _FAST_KEEP did not list it, so the fast tier deferred the very check it is justified by. Every pre-push run printed SKIP: dist/loki.js is a fresh build of src.

The cost was not theoretical. It slipped twice more before anyone noticed:

Both are exactly the failure the check was written to stop, both slipped past a green pre-push gate, and the second happened while actively fixing the first.

Now in _FAST_KEEP. Measured cost: one bun run build, ~40ms.

Verified by staling the dist to 8.11.0 and re-running: the fast tier exits 1 with DIST STALE. Restored afterwards.

The second mechanism: tracked-but-ignored

loki-ts/dist/ is gitignored while dist/loki.js is tracked. A plain git add loki-ts/dist/loki.js is therefore refused with a hint that is easy to skim past, and the stale bundle ships silently. It needs git add -f.

That is why v8.39.0’s dist commit landed empty of its intended change. The gate above is the durable fix – it fails the push rather than relying on anyone reading a git hint.

v8.39.0

Auditing the rest of the packaging, and a false alarm worth recording

v8.38.0 fixed four detectors that never shipped. That gap was found by accident, from telemetry – so this audits the general case: every file the runtime shells out to, derived from all four entry points (loki, run.sh, completion-council.sh, app-runner.sh).

Result: no other gaps. The detectors were the only ones. That is a real finding, recorded rather than turned into a fix that was not needed.

The 55-file false alarm

The first audit reported 55 missing files – including autonomy/run.sh itself, which is obviously shipped. Cause:

npm pack --dry-run 2>&1 > file     # WRONG: listing goes to the terminal
npm pack --dry-run > file 2>&1     # right

npm writes the file listing to stderr. With the redirects in the wrong order, the capture held 10 lines of build chatter instead of 730, and every “is X in the listing” check failed against an empty haystack.

Same shape as the grep-absence errors that appeared three times earlier in this session: an empty result is not evidence, it is an absent measurement.

Both directions now guarded

Trust-core detector: 60 invariants.

v8.38.0

Four quality gates were broken for every npm user, on every iteration

The measurement harness shipped one release ago paid for itself immediately. Running it against a real FireLater run showed mutation_integrity failing 3 of 3 iterations in 0-1 SECONDS each. Real mutation analysis cannot run in zero seconds – that was a fail-closed branch firing instantly, forever.

The recorded finding:

[HIGH] mutation detector unavailable:
  /opt/homebrew/.../node_modules/loki-mode/autonomy/../tests/detect-test-mutations.sh

package.json’s files[] had no tests/ entry, so the published package shipped ZERO files in tests/. Four gates shell out to detectors there:

enforce_mock_integrity        tests/detect-mock-problems.sh
enforce_mutation_integrity    tests/detect-test-mutations.sh
(semantic)                    tests/detect-semantic-test-problems.sh
(invariants)                  tests/detect-invariant-violations.sh

Mutation-integrity is fail-CLOSED by design, which is correct – a missing detector must never read as “nothing found”. The consequence is that it could never pass for an installed user, forcing another iteration every single time.

That is precisely the founder’s complaint. First-pass completion was impossible for any npm user regardless of how good the model’s output was, and no amount of model quality could have fixed it.

The four detectors are now in files[], verified present in the real npm pack tarball.

Why nothing caught this

The detectors exist in the repo and run fine from a git checkout, so every in-repo test passed. Only the PACKAGED artifact was broken – invisible to unit tests by construction, and invisible to the trust-core detector, which also runs from the checkout.

tests/test-detectors-are-packaged.sh derives the required list FROM run.sh rather than hardcoding it, so a fifth gate added later is covered automatically, and asserts against the real tarball rather than only files[].

Two corrections made while writing that test

v8.37.0

Make the unproven measurement one command

v8.33.0-v8.36.0 shipped three speed knobs and verified they BIND on production code. The end-to-end win stayed unproven, because proving it needs a real build – and reading the result meant hand-parsing .loki/events.jsonl, which is how the original numbers were derived. That is not something to re-derive under time pressure with a paid build already spent.

scripts/measure-run.sh <workspace> reports where a run’s wall clock went. It reads only what a run already wrote: it starts nothing, costs nothing, and never contacts a provider.

Verified against the real FireLater run that produced the original analysis – it reproduces those numbers exactly:

stage                    n   median    total   share
code_review              2     504s     785s     96%
static_analysis          3       7s      26s      3%
...
TOTAL                                   817s

CODE REVIEW, per council
  seconds  reviewers  pass  fail
      502          7     0     7
      280          7     1     6

It also correctly reports first code change: not recorded for that pre-v8.35.0 run rather than inventing a value – the same silence-over-fabrication rule the signal writers follow.

Degrades safely: a truncated or corrupt JSONL tail is skipped rather than aborting the report, a missing workspace exits 66, and --json emits a parseable object. The “Nx” scaling line prints only when the data contains more than one council size, because a single sample cannot support a claim about scaling.

The comparison it exists to make

baseline:  (knobs unset)
capped:    LOKI_REVIEW_MAX_REVIEWERS=3 LOKI_REVIEW_SKIP_ON_GATE_FAIL=true

Run the same issue twice, diff the code_review row and the total. That is the number that answers the 21-minute complaint, and it still requires a real build – this release removes the friction, not the requirement.

v8.36.0

Verifying the shipped knobs against production code, not a copy of it

v8.33.0-v8.35.0 shipped three speed knobs, all default-OFF. This release verifies the council cap works on run.sh’s actual selector – extracting the real python3 << 'SPECIALIST_SELECT' heredoc and running it end to end through the same environment the runner sets:

cap=0 -> 6: architecture-strategist, maintainer-mergeability, security-sentinel,
            test-coverage-auditor, performance-oracle, dependency-analyst
cap=4 -> 4: architecture-strategist, maintainer-mergeability, security-sentinel,
            test-coverage-auditor
cap=3 -> 3: architecture-strategist, maintainer-mergeability, security-sentinel

The safety property holds in production code: the mandatory pair survives at every level, and only the appended tail is trimmed.

The mutation that survived, and the real defect behind it

Breaking the trim loop’s mandatory-exclusion did NOT fail the existing test. It turned out to produce a genuinely broken council:

cap=3 -> architecture-strategist, maintainer-mergeability, architecture-strategist

Every mandatory reviewer is still present, so a presence-only assertion stays green – while a capped slot is wasted on a DUPLICATE and the council pays full latency for one fewer perspective.

Presence is not distinctness. The test now asserts both, and the mutation goes red.

Why a second cap test exists

test-review-council-cap.sh drives a faithful re-implementation of the trimming rule. That is useful and insufficient: three mutations across v8.28.0, v8.34.0 and v8.35.0 survived because a test exercised its own copy while the original was broken. This one runs the source.

A re-implementation tests the RULE. Only the source tests the CODE.

Honest status of the speed work

Verified: the knobs reach the real code path and bind correctly; gate_failures (run.sh:22697) and the skip decision (run.sh:22962) share the same function scope, so the skip observes real failures.

NOT yet verified: an end-to-end scoped-issue run with the knobs enabled. That needs a real paid build. Until it is measured, these are selector behaviour plus historical stage timings – not a demonstrated end-to-end improvement, and the plan says so.

Trust-core detector: 57 invariants.

v8.35.0

A run with no preview told the user nothing until it ended

seconds_to_first_preview only fires for a previewable app. The shape the founder measured at 21 minutes – a scoped GitHub issue fix – produces no running app, so that number is empty and nothing marked the moment the run first touched the codebase. The user watched an idle terminal until the whole iteration finished.

That is why felt time is worse than measured time even when measured time is competitive: Replit shows something at roughly 2 minutes, and we showed nothing at all.

.loki/state/first-artifact.json now records seconds-to-first-code-change, and the summary renders it as First change: beside First preview:.

Costs nothing on the common path: start_time and end_time already exist at the write site, so this adds no subprocess to the iteration.

Deliberately silent rather than fabricated

Two probes SURVIVED, for the same reason as last release

The write-once and no-fabrication guards live in run.sh’s real writer, while the test drove a faithful copy of it – so mutating the original left every assertion green.

This is the third time this exact shape has appeared (v8.28.0 extraction tests, v8.34.0 skip condition, here). The rule, now applied on sight:

A re-implementation tests the RULE. Only reading the source tests the CODE. Any test that reproduces logic must also assert against the original.

Both now turn red under mutation, alongside a third proving the value is rendered and not merely recorded – the half-shipped pattern that cost six releases earlier in this session.

Trust-core detector: 54 invariants.

v8.34.0

Spending 280-502s to describe a rejection that was already decided

The deterministic gates run BEFORE code review and cost ~6s combined (static_analysis 5s, security_scan 1s, lsp_diagnostics 1s, test_suite <1s). The council costs 31s at 3 reviewers and 280-502s at 6-7.

When one of those cheap gates has already failed, gate_failures is non-empty and the iteration cannot be accepted no matter what the council says – the same variable feeds the completion decision. So the review was spending up to 502 seconds producing advice on code that was already going back for another pass.

LOKI_REVIEW_SKIP_ON_GATE_FAIL=true skips the council in exactly that case.

What this deliberately is NOT

It does not weaken any gate. A skipped review is recorded as SKIPPED, never as a PASS, the failing gate still blocks unchanged, and the skip branch touches neither clear_gate_failure nor gate_failures. It cannot turn a rejection into an approval – it only declines to spend five minutes narrating one.

Default OFF. Review findings are also next-iteration steering (LOKI_INJECT_FINDINGS), so skipping trades some guidance for a large latency win. That is a per-route call, not a silent global one.

Three mutations pin it, covering both directions of error:

A re-implementation tests the rule; only the source tests the code

The third mutation initially SURVIVED. The test drove a faithful copy of the skip condition, so weakening run.sh’s actual condition left every assertion green. Fixed by asserting against the source text, not a reproduction of it.

Same lesson as v8.31.0’s council finding and v8.28.0’s blind extraction tests, arriving from a third direction.

Also corrected: P1 in the speed plan was already half-done

The plan proposed reordering cheap gates before the council. They already run first (run.sh:22653, 22714 precede 22883). The actual gap was that nothing CONSULTED the result – which is what this release fixes.

Trust-core detector: 50 invariants.

v8.33.0

The council was never sequential. It was oversized.

Measured from real code_review_start/code_review_complete pairs:

reviewers seconds
7 502
7 280
6 177
3 31

Roughly 2x the council for 9-16x the wall clock. Since reviewers are already dispatched concurrently (run.sh:14803 forks with ) &), that superlinearity is the max-of-N tail plus contention on one provider.

And the 7-member council was partly redundant – security-sentinel AND review-security reviewing the same diff:

architecture-strategist, maintainer-mergeability,
security-sentinel, review-security,        <- two security reviewers
performance-oracle, eng-qa, dependency-analyst

Root cause: the tier map sizes only the SPECIALIST slots ({simple:2, standard:2, complex:4}). Installed agents and dependency-analyst append AFTER that sizing, so nothing bounded the total – which is how a 4-reviewer battery becomes 7 on a scoped issue.

LOKI_REVIEW_MAX_REVIEWERS caps the total. Default 0 = uncapped, so this release changes no existing behaviour; it is a knob to turn on per route.

Trimming order is a safety property

Shrinking a council must never be able to manufacture an approval. So:

Both halves are mutation-proven: removing the below-mandate guard, and letting the trim loop drop mandatory reviewers, each turn tests/test-review-council-cap.sh red.

Correction to the plan committed one release earlier

docs/SPEED-AND-FIRST-PASS-PLAN.md originally claimed reviewers ran sequentially and made “parallelize the council” its headline P0. That was wrong – the council has always been parallel. The grep behind that claim searched the wrong line range and returned nothing, and I read the silence as evidence.

The plan now records the error rather than hiding the edit, and carries the rule that produced this release instead:

No optimization ships without a before-number, and no claim about how the code behaves ships without reading the code that does it. An absent grep match is not evidence of absence.

Trust-core detector: 47 invariants.

v8.32.0

Aider’s fallback named a Claude model

autonomy/run.sh hardcoded claude-opus-4-7 in seven places as the aider default. That id appears NOWHERE in providers/model_catalog.json, and providers/aider.sh’s own comment explains why it is worse than merely stale: aider takes litellm full model strings while Claude aliases are namespace-incompatible, so the fallback would have dispatched a Claude model name to aider. Now openrouter/deepseek/deepseek-v3.2, matching the catalog.

Reachable only when the provider config was never sourced – aider.sh already derives its default from the catalog – so this hardens a fallback rather than changing normal operation.

On Claude, nothing needed changing

That route already dispatches BARE aliases (sonnet, haiku), which always resolve to the provider’s latest model with no catalog maintenance at all. Verified against the live CLI: claude --model opus reports claude-opus-5.

Codex tier pinning was investigated and deliberately NOT changed

providers/codex.sh resolves all three tiers to the empty string on the real loader path, so selecting high does not select gpt-5.6-sol. That looks like a bug and a fix was written – then reverted, because tests/test-codex-model-trusted.sh caught it and records why:

codex-cli rejects gpt-5.3-codex with “not supported when using Codex with a ChatGPT account”, and Codex ships free with every ChatGPT plan. A pinned default breaks the only zero-cost on-ramp in the category outright. Resolving tiers to catalog models re-introduces exactly that failure.

The empty default is therefore correct: when a model cannot be known to be valid for the account tier, sending no --model and letting codex choose beats guessing. Making codex tiers work needs account-tier awareness, not a catalog lookup – recorded here so the fix is not re-attempted from scratch.

v8.31.0

The planning tier was pointing at a superseded flagship

The founder’s standing instruction is to re-check model tiers against provider docs after every release. Doing that found real drift: providers/model_catalog.json resolved the planning tier – the most expensive tier, used for architecture decisions – to claude-opus-4-8 while claude-opus-5 was already shipping.

Verified against the live CLI before changing anything: claude-opus-5 dispatches successfully, and so does claude-opus-4-8. This was staleness, not breakage – runs were working, just on a previous-generation flagship.

The promotion moves all three references together (models[], the derived latest_planning mirror, and the opus CLI alias), because the catalog’s own _source_of_truth note requires it and tests/test-model-catalog-single-source.sh fails when they disagree.

Cost tracking is unaffected: loki-ts/data/model-pricing.json is keyed by ALIAS (opus), not by model id, so promoting the alias target does not orphan the pricing entry. Checked rather than assumed.

Catalog AGE could not have caught this

loki doctor already reports catalog age and warns past 90 days. That check was useless here: the catalog was 3 days old. A provider shipping mid-window makes age a poor proxy for correctness.

tests/test-model-catalog-current-flagship.sh closes that gap offline, with no network and no hardcoded knowledge of what the newest model is (which would rot exactly like the catalog). It fails when the catalog contradicts ITSELF:

Both real drift shapes are pinned by mutation: restoring the pre-fix latest_planning and the pre-fix opus alias each turn the test red.

Also ships tests/test-model-catalog-staleness.sh (13 assertions), which was written earlier in this session, passes, and had never been committed.

Trust-core detector: 47 invariants.

v8.30.0

The baseline guard found two probes running zero tests

v8.29.0 added a baseline check to mutation-probe.sh: run the test on UNMODIFIED code first, and refuse (rc 67) if it is already red. Its first full run across all 45 trust-core invariants immediately found three cases whose prior PASS verdicts meant nothing.

Two probes had been executing zero tests. They invoked bun test test/budget_cache_pricing.test.ts from the repo root, but that file lives under loki-ts/. Bun searched 536 files, matched nothing, and exited 0 – so the probe saw success, reported the invariant proven, and never ran a line of it. Both now run via cd loki-ts and genuinely detect their mutations.

This is the exit-code false-green shape: a command that never did the work returns the same 0 as one that did.

A rate-limit signal that could not match, and two stale expectations

tests/test-rate-limiting.sh was genuinely red on unmodified code (24 passed, 3 failed) and had been for some time. The three failures split two ways:

One was a real defect. is_rate_limited requires a rate-limit token and an error token to co-occur, but “quota exceeded” CONTAINS its own error word: the alternation consumed exceeded as the rate-limit half and found no error half left. API quota exceeded for project – the canonical quota rate-limit line – did not match at all, so the runner would treat a quota limit as a hard failure instead of backing off. Self-sufficient phrases now match on their own.

Two were stale expectations. The cases asserted that a bare X-RateLimit-Remaining: 0 and a bare Retry-After: 60 must trigger a wait. That contradicts a deliberate design decision: those strings appear in the agent’s OWN generated source, and treating them as a live provider limit caused multi-minute false waits mid-build. The expectations predate the co-occurrence rule and were never updated when it landed. They now assert the correct behaviour, with companion cases proving the same headers DO trigger inside a real error frame.

Deliberately NOT widened: a bare 429, a bare retry-after, or an X-RateLimit-* header still must not qualify alone.

v8.29.0

The same blind spot on the Python side, on an input-validation boundary

The extraction-test sweep (v8.27.0, v8.28.0) covered the shell. The Python tests have the same isolation shape – exec a function out of server.py and drive it directly – and had never been checked. Of the two, one was blind:

_normalize_start_model could be bypassed at its call site with every assertion still green. Two consequences, and the second is the serious one:

Both call sites are now pinned separately. A presence-grep passes while one of the two is removed, and losing only the advisor path leaves the primary path correct – the asymmetric shape that is hardest to notice. The allowlist membership test is pinned as well, since dropping it makes the function a pass-through that still looks wired everywhere it is called.

A test that always fails proves nothing either

The first version of these assertions had a bug (_SERVER is a path string in this module, not a Path), so they raised AttributeError unconditionally. All three mutation probes reported OK – the tests did “fail” under mutation, but they failed identically without it.

mutation-probe.sh distinguishes three outcomes, and this is a fourth it cannot see: a test that fails for its own reasons is as useless as one that always passes, and it reads as success. The fix was verified by confirming a GREEN baseline first, then re-running all three probes.

Trust-core detector: 45 invariants.

v8.28.0

The extraction-test sweep is finished: 6 of 8 were blind

v8.27.0 probed four of the eight extraction-style tests in this repository and found three blind. This finishes the remaining four. One was already sound (_loki_app_sandbox_dir); the other three were not:

Final tally across v8.27.0 and this release: 6 of 8 extraction tests were blind. Every one is now pinned by a mutation that goes red.

An assertion loose enough to never fail is the same bug, one level up

The state-file fix was wrong on the first attempt and the probe caught it. The first version asserted a call-site COUNT of at least three – but the resolver has SEVEN call sites, so deleting one still cleared the bar and the mutation survived a second time.

A count threshold that the defect cannot trip is exactly the failure mode this detector exists to find, just relocated from the test into the assertion. Now pinned to three specific call sites by exact text: the baseline reader, the terminal-status writer, and the final-status writer.

Trust-core detector: 42 invariants.

v8.27.0

Four more gates could be disconnected with every test green

This release stops chasing the pattern one gate at a time and sweeps for it.

An extraction test – one that pulls a shell function out of run.sh and drives it directly – proves the FUNCTION is correct and says nothing about whether anything still CALLS it. Eight tests in this repository have that shape. Four were probed by mutation; three were blind, and each blind one guards something a user relies on:

All three now carry wiring assertions, and all three previously-surviving mutations go red. Four cases added to the trust-core detector (now 39).

The two probed paths that were already sound are recorded as such rather than “fixed”: the sentrux security gate and the evidence-gate detail surface both end in || true, but each captures its verdict separately and returns it intact – the same shape v8.26.0’s fix converged on.

Nine subsystems have now been found with this caller-vs-callee gap. An isolation test without a wiring assertion is half a test.

v8.26.0

Gate 12 (Magic Modules debate) has never blocked anything

It was broken in three independent ways, any one of which alone was fatal:

  1. The CLI called run_debate(react_path=..., wc_path=...). Those are not parameters of that function – it takes component_path – so every invocation raised TypeError before a debate ever started.
  2. run_debate RETURNS a dict and prints nothing. The CLI discarded the return value, so even a successful debate produced no output.
  3. The gate grepped stdout for "severity": "block", a string that only ever existed inside the discarded dict.

The failure was invisible because the gate captured output with || true and then looked for a blocking severity. A crash produces no match, and no match read as “nothing to block on”, so a totally broken gate reported PASS on every iteration since v6.77.0 – contributing a green result to the completion decision while judging nothing.

Reproduced end to end before fixing: TypeError -> “Debate failed” -> gate returns PASS.

Enforcement is opt-in, because the fix was measured before being trusted

Fixing the plumbing made the blocking path reachable for the first time. That path had never run against a real project, so it was measured rather than assumed – and the result is why it ships advisory:

On a deliberately thorough spec (explicit KB budgets, a named device class, a zero-JS server component, a stated contrast ratio), three of four personas still returned “block”. Two independent specs, two blocks. A single “block” from any one persona fails the gate, so it approves only when four strict reviewers are simultaneously satisfied – a threshold almost nothing clears.

Turning that on would have replaced a gate that never blocked with one that blocks nearly every build: a worse regression than the silent fail-open being fixed. The finding is surfaced and recorded; enforcement waits on tuning against real projects. Set LOKI_GATE_MAGIC_DEBATE_BLOCKING=true to enforce.

A gate that cannot run no longer reports PASS

“Could not run” splits in two, and the halves need opposite handling:

Unrecognised failures degrade rather than block, so a new provider error shape can never wedge every build.

tests/test-magic-debate-gate.sh (20 assertions) and five cases in the trust-core detector (now 35) pin all of it, including that enforcement stays opt-in: flipping the default to true turns the test red.

v8.25.0

The startup preflight had no tests at all

validate_api_keys runs four environment checks before a build begins. Nothing tested any of them. Four checks deciding whether a run may start, with no evidence any still fired – the same shape as the three safety valves in v8.24.0, which all turned out to be disconnectable while their own tests stayed green.

What a silent regression costs here is specific. Without the preflight, a user with no git, or a working directory they cannot write to, does not get a one-line fix at second zero. They enter the loop, spend a paid provider call, and fail deep inside the build with an error about something else. Turning that into an instant actionable message is the preflight’s entire value.

tests/test-preflight-checks.sh (14 assertions) now covers it, and asserts BOTH directions, because the two failure modes are opposite:

Inverting either direction is a real defect: a fail-closed network probe locks working users out on a transient blip, and a fail-open workspace check lets a build run with nowhere to write its receipt.

Three cases added to tests/test-trust-core-tests-detect.sh (now 30) pin this with real mutations: making the git check fail-open, disconnecting the workspace check from its caller, and making the node advisory block all turn the test red.

The wiring assertions pin the call sites by count rather than presence. A presence-grep passes while one of four calls is deleted – the exact gap found in eight subsystems in this codebase.

v8.24.0

The three safety valves could all be disconnected

The spend cap, the iteration cap and the wall-clock cap are the only things that stop a run on its own. All three could be disconnected from the loop with every test still passing, including the one written specifically for the duration cap.

Without them a build spends and runs until something external kills it.

All three connections are now asserted. The iteration cap needed more than a presence check: it has two call sites, one before the loop and one inside it, and a test that merely found the name passed while the in-loop check was removed. The assertion pins the exact number of call sites, so losing either one fails.

Each valve is also required to record its own terminal status, so a run that stopped on time is not reported as one that stopped on cost.

The detection audit covers twenty-six invariants.

The vote itself could be skipped

Finishing the enumeration found the last and largest instance. The four hard gates were guarded a release ago, but the call that runs the actual evaluation was not. A true return from it is what prints project approved and writes the completion marker, so bypassing it approves every run with no vote taken.

That is a stronger failure than any single gate: the gates each check one thing, while the evaluation is the decision.

The safety valve beside it was probed at the same time and was already guarded. Both are now pinned, along with the completion marker still being written from the approval path rather than anywhere else.

The council enumeration is complete: seven guards, all now proven to fail when disconnected. The detection audit covers twenty-three invariants.

The other three completion gates had the same hole

Fixing the evidence gate one release ago and stopping there was the wrong instinct. It sits in a run of four hard gates that each block completion for a different reason: an unmet checklist, failing held-out evaluations, missing evidence, and unresolved assumptions. Only one had been checked. All four were disconnectable with every test still passing.

Bypassing any one lets a run complete while the thing that gate exists to check was never checked, and the receipt still reads verified.

All four connections are now asserted as a group, so a future gate added beside them without a call-site assertion is visible rather than silently unguarded.

The detection audit covers twenty-one invariants.

Two more gates could be bypassed with every test still green

The code-review gate and the evidence gate were both disconnectable without any test failing. Both fail in the dangerous direction.

Bypassing the review gate makes every iteration pass review without the review running. Bypassing the evidence gate lets the completion council approve a run with no diff and no green tests, which is the exact claim the product exists to make honestly.

Both were invisible for the same reason as the four before them: the suites test each gate’s own logic thoroughly and never assert that anything still calls it.

Both connections are now asserted, along with the phase toggle still being consulted so the review gate cannot be silently hardcoded on or off. The detection audit covers eighteen invariants.

The completion gates could be disconnected with every test still green

The completion council decides when a run is done. The supervised path has its own gate that blocks completion when an iteration reported failures. Both live in the same conditional in the runner, and neither connection was tested.

Disconnecting the council means no run can ever be approved, which then looks like a model problem rather than a wiring bug. Bypassing the supervised gate means every supervised run reports complete with its gates unchecked. Both failure modes were silent: the council suites test the decision logic directly and never asked whether the runner still consults it.

Both connections are now asserted, along with the helpers still being defined under the names their callers use, since a rename touching one side leaves the guard permanently false with no error anywhere.

The detection audit covers sixteen invariants, each proven to fail when broken.

The last cost route was checked and is clean

Four cost routes had the same defect: a correct calculator receiving records that had already lost their cache tokens. The TypeScript path was the remaining one that had never been checked end to end, only through hand-built records.

Driving the real file-to-cost flow shows it preserves the fields and produces the same total the other routes do, so there is no fifth instance. The test now exercises that flow rather than assuming it, and it is pinned in the detection audit, which covers fourteen invariants.

Recording a clean result matters as much as recording a fix. Without it the next person has to re-derive whether this route was ever verified.

The budget breaker under-counted the tokens that dominate a bill

Applying the isolation-versus-wiring check to the Python tests found a second caller of the cost calculator that never passed cache tokens. It feeds the budget snapshot, which decides when to warn at eighty percent and when to stop at the cap.

On a real record it saw $0.12 where the true spend was $0.66, so a run could sail past its limit unwarned. The same bug, in its fourth location: the earlier three were fixed while this one stayed hidden because every test exercised the calculator directly and none checked what the callers pass it.

All call sites now pass cache tokens, and a test asserts that for each of them rather than for the calculator alone.

A security guard could be bypassed with every test still green

Several tests extract a helper from the engine and verify it in isolation. That proves the helper works and proves nothing about whether anything still calls it. Sweeping for that specific shape found three blind tests, one of them on a security boundary.

The app-runner validates a user-supplied command against a whitelist before executing it. Replacing that guard so an unvalidated command runs left the entire injection test suite passing, because every case tested the validator directly and none checked that execution still went through it.

Two more had the same shape: the interactive handoff every launch shows could be disabled silently, and the language-gate timeout wiring fixed in the previous release. Rate-limit detection was checked and turned out to be genuinely guarded.

All three now assert their call sites, and each is pinned in the detection audit, which grew from ten invariants to thirteen. Every one is proven to fail when broken.

A gate could lose its timeout and every test would stay green

The language-gate timeout tests extract the helper into a scratch harness and verify it there. That proves the helper works and says nothing about whether the gates call it. The connection was untested, so a gate reverting to unbounded execution, which can hang an entire run, would not have failed anything.

The tests now assert the call sites too, and that assertion catches the real regression: removing the timeout from the go gate turns them red.

The detection audit grew from seven invariants to ten, adding the TypeScript cost path, codex tier resolution, and the gate timeout wiring. All ten are proven to fail when broken.

One note for whoever extends this: probing a function’s DEFINITION is not the same as probing its use. Renaming a helper leaves its call sites pointing at the old name, which no real change would do, and the resulting report reads as a blind test. Target the call site.

Proof that the trust-core tests would fail if the trust core broke

Every test here passes. That was never evidence any of them would fail if the code broke, and for the trust core that gap is the product: a receipt claiming verification, guarded by a test that cannot tell when verification stopped happening.

Seven load-bearing invariants are now proven detectable by breaking each one and requiring its test to go red. All seven pass, and the audit is a test itself, so it re-runs whenever anything changes rather than being a one-off exercise.

The audit found a real coverage hole while being built. The verifier keeps its post-headline filter in two places, and every existing fixture exercised only the legacy arm, so the arm that real receipts actually take was unguarded. Two cases now pin the arms apart.

It also found a trap in the method itself: probing the wrong copy of duplicated code reports that the mutation survived, which is indistinguishable from a blind test. The probe now takes a marker to pin which copy is under test, and the audit records why that matters where the next reader will hit it.

A mutation test that does not mutate looks exactly like one that passes

Verifying a test by breaking the code and watching it go red depends entirely on the break landing. When the search string does not match, nothing breaks, the test passes, and the result is indistinguishable from a working test. A checker then ships with no evidence it detects anything.

That happened twice here in consecutive releases. Both times the only clue was a debug line that existed by luck.

There is now a probe that makes the distinction structural, with three separate exit codes: the mutation applied and the test caught it, the mutation did not apply, or the mutation applied and the test missed it. The middle case used to look like success and now fails loudly. The target file is restored in every outcome, including errors.

Building it surfaced two of its own bugs, both found by pointing it at itself: a restore that ran twice and printed an error on a healthy run, and a self-test that recursed without bound and left the probe on disk with its own guard deleted. The second is recorded in the test file so the idea is not retried from scratch.

The release checklist pointed at a file that never existed

skills/sdlc-fleet.md has a section headed “Per-release checklist (always)”. It sent the reader to a roadmap file for the binding fourteen steps. That file has never been tracked in this repository, and the only mention of the checklist anywhere is the pointer itself, so an agent following it for the binding procedure found nothing and had to guess.

It now points at the Release Workflow section of CLAUDE.md, which is the real procedure and stays current because every release edits it.

The pointer checker now covers the skills modules as well as the two entry documents, ninety-nine paths in total. Fenced code blocks are excluded, because one module shows an example learning note whose file names are illustrative, and two runtime paths are excluded by name for the same reason: a checker that flags correct things gets ignored.

The project instructions pointed at a file nobody else has

CLAUDE.md is loaded into the context of every agent that works on this repository. It ended its architecture section by pointing at a codebase knowledge graph under a path that is machine-local Claude memory, under a previous repository name, neither tracked in git nor shipped in the package.

It resolved on exactly one machine. Every other reader, human or agent, followed it to nothing.

The reference now says where that file actually lives and states plainly that it is not in the repository, so a reader stops looking instead of assuming they have the wrong checkout. The in-repo tables are named as the authority for everyone else.

A test now resolves every repository path cited in CLAUDE.md and SKILL.md. Scoped narrowly on purpose: runtime state paths and bare prose filenames are skipped, because a checker that flags those is noise, and a noisy checker gets muted.

A month-old decision list was reading as current

The founder decision list from 2026-07-01 presents eight decisions awaiting an answer. Re-checked against the repository a month later, four of them concern a package that does not exist here at all: it lives in a separate repository, so those decisions cannot be acted on where they are being read. One more cites a benchmark progress figure that cannot be confirmed or refuted from this repo, because the results directory is not tracked.

The document now opens with what was re-verified and what was not, item by item, and points at the current decision list. Nothing was deleted: the decisions were real when written, and a reader deserves to see both the original and what has since become unverifiable.

The accuracy test now covers this document too, including the claim that the package really is absent. If it is ever vendored in, the note becomes wrong and the test says so.

The open question was narrower than it looked

Sweeping every fact the proof records showed that keeping an unrun check out of the honesty ledger is a deliberate, consistent pattern rather than a gap in one place. Three facts behave that way, and two of them said so in their own documentation while the third did not.

The security collector now carries the same explicit note the other two have, so the behaviour is documented where a reader will find it instead of inferred from its absence. No verdict changes and no question is answered.

The decision document was corrected: answering the security question alone would leave the receipt internally inconsistent, since a reader who sees an unrun scan flagged will ask why an unrun healthcheck is not. It is one decision about a tier, not three about separate facts. The recommendation that said otherwise was written before the sweep and has been replaced.

The two open trust questions are written down

Two questions surfaced during this work that change what “Verified” means on a receipt, so they were deliberately left unanswered. They were recorded only in commit messages, which is where decisions go to be forgotten.

They are now a document, each with the measurement that raised it, both sides of the argument, and the exact function where the change would go. The sharper of the two: a receipt can read VERIFIED with an empty gaps list while the security scan never ran, because an absent scan is deliberately not counted as a gap and there is a test asserting that.

A test keeps the document honest. It re-checks every quotation against the source and re-measures the behaviour the document describes, so answering either question turns it red and forces the record to be updated rather than left stale.

The forgery-accusation fix was one status string from breaking again

v8.19.2 stopped the verifier accusing honest receipts of tampering, by skipping the gap entries the generator appends after computing the headline. That filter matched a single status string, which works only until a second kind of post-headline entry exists.

Entries now carry an explicit marker saying they were appended after the headline, and the verifier filters on that intent rather than on a status it has to keep guessing. Any future entry is covered by construction. Receipts written by earlier versions, which predate the marker, are still recognised.

An honest receipt was being accused of forgery

The verifier re-derives a receipt’s headline from its recorded facts and reports any mismatch as evidence the headline was edited to misrepresent them. That is the strongest claim the tool makes.

v8.19.0 added a gap entry for each disabled trust gate, appended after the headline is computed so the verdict does not move. The verifier re-derived using those entries, and the derivation only cares whether the gap list is empty, so a run with security switched off recorded one headline and re-derived another.

The result was a user being told their honest proof had been edited to misrepresent the facts. For a product whose claim is verifiable trust, a false accusation of tampering is worse than the gap it was meant to surface.

The re-derivation now uses exactly the list the generator used. Excluding those entries does not blind it: a genuinely edited proof is still caught, and a real gap recorded before the headline still counts, both asserted.

The owner read the same problem three times

Rendering a real generated proof through the owner-facing page showed a gate named three times in four lines: once by a summary line added in v8.18.1, then twice more by the per-gate entries the honesty ledger gained in v8.19.0.

Repetition in a section about what went wrong reads as separate problems, which is the opposite of what a plain-English summary is for. The ledger is the canonical source now, so the redundant summary line is gone and each skipped check is named exactly once.

The verdict is unchanged: a run with correctness gates disabled still does not show a green badge, and the unhelpful fallback that says no gaps were itemized still stays suppressed when the ledger explains the gap.

A switched-off gate now appears in the honesty ledger

The ledger exists so a reader sees exactly what was not verified rather than inferring it from silence. A gate the operator switched off is exactly that, and it was missing.

Measured through the real generator rather than a hand-built proof: a run with code review and security disabled listed only “build” as its gap, while two correctness checks had not run at all. The receipt was accurate about what it mentioned and silent about what mattered most.

The entries are appended after the headline is computed, deliberately. Feeding them into the headline would change what “Verified” means, and this project already treats that as a decision for the council and founder rather than something to infer. The same run produces the same verdict as before; only the gap list is now complete.

Phases that do not bear on correctness, such as performance testing and competitor research, are not listed. Padding the ledger with entries that do not mean the work is less verified is how a ledger gets skimmed.

The owner was told the badge was not green, but not why

v8.18.0 stopped a run with disabled trust gates from showing a green “ready” badge. The section explaining what went wrong lists the recorded gaps, and a run blocked only by disabled gates has no recorded gaps at all: its verdict is VERIFIED and its degraded list is empty.

So the owner read “no specific gaps were itemized” when the reason was precise and already recorded, next to a verdict line saying VERIFIED and a badge refusing to call it ready. Three statements on one page that could not be reconciled.

The skipped gates are now named first among the gaps, which makes the three statements consistent: verified as far as it went, not ready, and here is exactly which checks did not run.

The honest fallback stays for runs where nothing explains the gap, and a run that is genuinely unverified for other reasons keeps its own list.

A run with its trust gates switched off showed a green “ready” badge

The plain-English summary restates a run for a non-technical owner. Its green verdict checked the headline, the tests and the build, but not whether the trust gates had actually run, so a build with code review and security disabled reported “ready” to the reader least equipped to notice what was missing.

The function’s own documentation already said this is the line that must never overclaim. It now also requires that no correctness gate was skipped.

Every other surface can show a caveat beside a green badge and trust the reader to weigh it. On this page the badge is the message, so the caveat has to change the badge.

Only gates bearing on correctness count: code review, security, unit tests and end-to-end. Turning off performance testing or competitor research still allows green, because a badge that turns amber for uninteresting reasons teaches owners to ignore the colour, which costs more than the overclaim it was meant to prevent.

Proofs written before the field existed are unaffected, and a malformed value is treated as bad data rather than as evidence of a skipped gate.

The shareable receipt now shows disabled gates too

The HTML receipt is the artifact that gets linked and read by people who did not run the build. It rendered “3 of 3 gates passed” identically whether every gate ran or code review and security were switched off and three lesser gates ran instead.

This is the third renderer of the same fact, after the proof record and the markdown receipt, and the most public of the three. The notice appears above the pass counts, so a reader who stops after the first line does not stop having read only the good news, and it states plainly that the counts do not cover the skipped checks.

Phase names are escaped. This is a shared document rendering values from a JSON file, and interpolating them raw would be an injection vector in the one artifact whose entire purpose is to be trusted.

Both of the template’s render paths carry it, since wiring only the newer one would leave older receipts silent. An ordinary run, a proof predating the field, and a malformed value all render nothing at all.

The receipt now shows the disabled gates it already recorded

v8.17.0 taught the proof to record which phases were switched off. Nothing rendered it, so the Evidence Receipt – the artifact that lands on a pull request and is the entire point of the proof pipeline – still could not distinguish a fully verified run from one with code review and security disabled.

A recorded fact nobody can see does not make a receipt more honest. The receipt now names the disabled gates in its facts table.

Only when something was actually disabled. A permanent “all gates ran” row on every receipt is noise, and noise in a trust artifact trains readers to skim past exactly the line that matters on the one run where it appears.

Proofs written before the field existed render unchanged, and a malformed value degrades to omitting the row rather than failing to render: a receipt that does not render at all is worse than one missing a line.

A receipt can now say what was not checked

The proof recorded which gates ran and how many passed. A gate that never ran was simply absent from the list, so a receipt reading “3 of 3 gates passed” was identical whether every gate executed or code review and security were switched off and three lesser gates ran instead.

Nothing in the artifact was false. The information was missing, which for a product whose central claim is verification is the more dangerous shape: recording only successes is how a green badge stops meaning anything.

Proofs now carry the phases that were disabled for that run, plus an explicit flag for the ordinary case. Phases remain configurable; the receipt simply reflects the run that actually happened.

An ordinary run records an empty list rather than a misleading one. A field that reported phantom disabled phases on every normal run would be noise, and noise gets ignored, which is worse than not having the field at all.

A terminal failure recorded itself as a success

When a spec is internally contradictory the engine stops without building anything, and re-running cannot help. The process exits 20 to say so, and both routes classify the status as a deterministic failure. The state file recorded zero.

Every consumer of that file reads the recorded code as what happened, so a run that was never buildable left a record claiming a clean stop. This is the hardest kind of inconsistency to notice, because every other signal was already correct: the exit code, the ENT-3 classification, and the TypeScript mapping all said failure. Only the persisted field disagreed.

The accompanying test sweeps the whole contract rather than that one status. Every terminal treated as a failure must record a non-zero code, and a clean stop must still record zero, so a future terminal added with the wrong code fails here rather than in someone’s pipeline.

A 54x token undercount in a published benchmark number

Trust metrics compute cost-per-verified-task from each proof’s cost block. When a proof records its token counts as separate components rather than a precomputed total, the fallback summed input and output and ignored the cache tiers, which the proof generator does record and which are the overwhelming majority of real traffic.

A proof whose own record totals 893,701 tokens was reported as 16,436.

This is the fourth site of the same defect, after the bash budget breaker, the TypeScript one, and the dashboard cost endpoint. Every one had the data available and dropped it.

An explicit total still wins, since that figure is authoritative. A proof with no token data reports nothing rather than zero, because zero would read as free rather than as unmeasured.

The dashboard’s cost endpoint was the third route pricing cache at zero

/api/cost, the surface a user watches while a build runs, summed input and output tokens only, and its cost fallback took no cache arguments at all. Cache reads are typically about 98% of input volume, so the token count described roughly one percent of the run, and when an iteration record carried no recorded cost the estimate under-counted by about five times.

That degraded path is exactly where a runaway is most likely, and it is the same bug already fixed in the bash budget breaker and the TypeScript one. This was the third route carrying it.

The rates now match the other two exactly, so a single run cannot report three different spends depending on which surface you look at. The endpoint also returns the cache token counts and the hit ratio it now computes.

With nothing read in the ratio is null rather than zero. A zero is a claim about a cold cache, which is a real and expensive condition, so reporting it for a run with no data would send someone hunting a problem that does not exist.

A test that catches a surface drifting from its siblings

Four surfaces report on a build: the completion summary, loki why, loki why --json, and the efficiency trend injected into the agent’s next prompt. Each was improved separately over the preceding releases, and three times a fix landed on one surface while a sibling kept the old behaviour, so the feature looked complete and was not.

The per-surface tests could not catch that. Every one of them passed while the surfaces disagreed with each other, because each verifies its own surface in isolation.

This adds the integration counterpart: one run, four surfaces, all four must reach the same verdict. Both directions are asserted, since a test that only checks the thrashing case would pass against a surface hardcoded to say thrashing. A run with no records must leave every surface silent rather than any of them inventing a verdict.

Verified against a simulated drift: removing the rework field from one surface alone turns this test red, including the assertion that all four agree.

Six control knobs shipped without documentation

The engine gained six operator-facing switches across the last several releases: the efficiency trend, reviewer-findings injection, the iteration grace, the scoped-change profile, and both provider timeouts. All are live, all default to on, and none of them appeared in the environment-variable reference.

A switch nobody can find is not a switch. The operator is stuck with the default and cannot tell there is a default, which matters most for the findings injection: turning it off makes every iteration a blind retry, and that is a decision someone should be able to make deliberately rather than discover.

All six are now documented with their defaults and their consequences, including the two timeouts, where setting either to zero restores the older behaviour of having no hang guard at all.

The existing documentation test only checked one direction, that nothing documented is fiction. It now also checks the reverse, that a named operator knob is documented, which is the direction that actually failed.

loki cost –json said what a run cost, never why

v8.13.1 taught the economics report to count cache tokens and report the hit ratio, after finding the old report described about one percent of a run. loki cost --json, the surface CI and dashboards consume, still emitted a bare dollar figure with no token breakdown at all.

So an automated consumer could see that a run cost $3.60 and had no way to see that 240,000 of its tokens were cache reads at a 98.9% hit ratio. Cache is typically the overwhelming majority of input volume and the single biggest lever on the bill, which makes it the first thing anyone investigating cost needs.

The data was never missing. The shared efficiency collector, also used by the proof generator, already sums the full breakdown; this surface took the dollar figure and discarded the rest. It now passes the whole thing through.

A run with no tokens reports a null hit ratio rather than 0.0. A zero is a claim about a cold cache, which is a real and expensive condition, and reporting it for a run with no data would send someone hunting a caching problem that does not exist.

The machine-readable surface knew less than the printed one

v8.14.1 taught loki why to tell a converging run from a thrashing one. It taught only the human output. loki why --json, the surface CI and scripts consume, is a separate code path that returns before the human report is built, so it still emitted a bare status.

An automated caller deciding whether to retry a capped build therefore had strictly less information than a person reading the same terminal. It now carries the same split, including a verdict field so callers branch on one shared judgement instead of each re-deriving the threshold:

“rework”: { “progress_iterations”: 0, “rework_iterations”: 3, “total_iterations”: 3, “rework_cost_usd”: 3.6, “verdict”: “thrashing” }

With no efficiency records the field is null rather than a zeroed object. A 0-of-0 split reads as “no rework happened”, which the data does not support, and a caller branching on it would retry a run that should not be retried.

loki why contradicted the summary that sent you to it

v8.14.0 made the completion summary’s cap advice rework-aware, then told the user to run loki why for detail. That command still gave the old line: raise LOKI_MAX_ITERATIONS, with no awareness of whether the run was converging or thrashing.

So the two surfaces disagreed, and the one the summary pointed at was the wrong one. On a thrashing run it recommended exactly the action that buys more thrashing at full price.

Both now read the same attribution. A thrashing run is told what its redos cost and to read the reviewer findings first; a converging run is told to raise the cap. With no efficiency records the original static advice stands, because guessing which case applies with no data is worse than generic guidance.

A run that stopped on a bound now names the lever

“Max iterations” told a user what happened and nothing about what to do, and the obvious guess is expensive in a specific way: raising the cap on a run that was THRASHING buys more thrashing at full price, while raising it on a run that was CONVERGING is exactly right. Same message, opposite correct actions.

The rework split added in v8.13.0 already separates those two cases in the same summary, so the advice points at it rather than listing every knob:

Next: this run hit its iteration ceiling, not a verdict. Converging (low rework above)? Raise it: LOKI_MAX_ITERATIONS= Thrashing (high rework above)? Raising the cap buys more of the same. Read the last reviewer findings first: loki why

A budget stop points at the cache hit ratio, since a cold cache is the usual cause of a surprising bill. A council force-stop states plainly that the work is not verified complete.

A successful run prints none of this. Advice after a clean finish is noise, and noise in a summary is how users learn to stop reading summaries.

The token report described about one percent of the run

loki memory economics summed input and output tokens only. Cache tokens dominate real traffic: a measured iteration carried 797,496 cache-read tokens against 10,272 of plain input. On that shape the report called 16,436 the total when the run was billed for 893,701.

The cache hit ratio was not reported at all, so a run with a cold cache paying the full input rate on every token looked identical to one running almost entirely on cache, while costing very different amounts. That ratio is the single biggest lever a user has on spend.

Both are now reported. The engine has recorded these fields per iteration since v6.82.0; the report was discarding them. Records written before those fields existed still total correctly.

The user can now see how much of a run was rework

The agent has been steering on its own progress-versus-rework split since the eval trend shipped. The user’s completion summary never showed it, reporting only task counts, which cannot distinguish real work from a gate false positive that forced redos.

That distinction is the difference between “the model was slow” and “our own gate was wrong”. A measured run had the agent claim done on every iteration while a false positive blocked all six: it looked like six iterations of work and was one iteration plus five of harness bug. The summary now says, for example, “Rework: 2 of 5 iteration(s) redid earlier work ($0.80)”, so the cost of our own mistakes is visible rather than buried in a total.

When the split is unknown the line renders nothing at all. A zero-of-zero line would read as a clean bill of health that has not been earned, and pointing a user at the wrong culprit is worse than saying nothing. That is the same rule the timing table follows.

The line and the agent’s trend come from one shared attribution source, so the number the user reads and the number the agent steers on cannot drift apart.

A documented feature that was only ever a delete

The memory-system docs listed a rewards store as a shipped capability. The only reference to it anywhere in the codebase was the line that emptied it every run: nothing wrote it and nothing read it. A directory that exists solely to be deleted is not a feature, and documenting it as one told the agent something false about its own runtime.

Both the dead code and the false claim are gone. The efficiency records, which are genuinely written and now feed the agent its own cost and outcome trend, are untouched.

The scoped-change profile was dead on every real issue run

v8.10.0 added a profile that skips greenfield-only phases for a scoped issue fix. It never armed. Two independent reasons, both found by running the real code path rather than a fixture.

The spec arrives as a POSITIONAL argument: loki start owner/repo#123 writes .loki/prd-issue-N.md and hands run.sh that path directly. The check read only an environment variable that is never set on that path. It also ran at source time, before argument parsing, so the spec was empty even when supplied.

And it tested for a .git DIRECTORY. In a git worktree or a submodule, .git is a FILE, so every worktree run was classified as greenfield. The check now asks git whether it is inside a work tree.

Test gates could hang a run forever

Language test gates ran with no wall-clock bound, so a hung test suite hung the whole run with no terminal and no signal. They are now bounded by a shared timeout helper.

The helper matters more than the cap: stock macOS ships neither timeout nor gtimeout, so a bare timeout prefix resolves to command-not-found and turns every gate into a false failure. The probe falls back across the available binaries and degrades to running unbounded rather than failing closed on a missing tool.

Skill documentation that lied to the agent

The skill modules are loaded into the agent’s context and acted upon, so an inaccurate claim is worse than no claim. Claims in the model-selection and index modules were verified against source and corrected, and a test now asserts they stay accurate.

A new module documents how to extend the engine at the orchestration layer without editing engine files.

The iteration cap now looks at evidence before cutting a run off

The cap was a bare counter. It consulted no gate, no council, and no evidence, so a run one step from finishing was cut off identically to a run thrashing in circles, and both reported the same terminal failure.

An iteration count is a proxy for “is this converging”. Where real evidence already sits on disk, the cap now prefers it: when the agent has requested completion and no gate is failing, the run gets one extra iteration to land the work. A claim of done on top of a real gate failure is still stopped, which is exactly the case the cap exists for.

It remains a cap. The grace is granted at most once per run, requires positive evidence rather than the absence of a failure signal, and extends by exactly one iteration. Published measurements put automated-verifier false-negative rates near 24 percent, so an unbounded verifier-driven terminal would burn real money re-doing work that was already correct. This is a bounded nudge, not a new termination rule. LOKI_ITERATION_GRACE=0 restores the pure counter.

The engine now runs on a closed feedback loop. Five principles from Alexandr Wang’s talk on agentic systems, mapped onto what loki actually does.

Evals and metrics: the agent can finally see its own numbers

The engine has written a rich per-iteration record for its entire life – duration, cost, tokens, cache hit rate, status – and read it back only in a stop-only budget breaker and an offline report. The agent producing the cost never saw it.

That trend is now injected into the next iteration’s prompt: how long each iteration took, what it cost, whether the cache is working, how many iterations made progress versus reworked earlier output. An agent that can see it is getting slower and more expensive can steer on it. An unmeasured run adds nothing, and the block sits in the volatile part of the prompt so it never invalidates the cache-stable prefix.

Both routes render it through one Python entry point, so they are byte-identical by construction rather than by two renderers kept in sync forever.

Speed and reliability: where the time actually went

A run that took 25 minutes could not tell you which phase consumed them. Per-stage wall-clock is now recorded and surfaced in the completion summary, so “where did the time go” is answerable from the run’s own artifacts.

Extensibility at the orchestration layer

A user-supplied reviewer can now take part in a run without editing engine files, with the installation path tested end to end. Manifest data is treated as data: a postinstall field is never stored and never runs.

Cost correctness: the two routes disagreed 4.2x about the same run

Found by executing both cost paths on one real record rather than reading them. The bash route under-counted 5.4x by pricing cache tokens at zero, on a record where cache reads were 98.7% of input volume. The TypeScript route over-charged 10x on the same term, because the pricing loader copied only input and output and silently dropped the cache tiers the JSON defined.

Both directions matter for a circuit breaker: under-counting lets a runaway pass the cap, over-charging stops a healthy run early and bills for a limit never reached. Both routes now agree.

A run that never verified its work reported success

A council force-stop happens when a run stagnates or floods done-signals. It means the run gave up WITHOUT verifying anything, and the engine already said so in three places: the header reads STOPPED WITHOUT APPROVAL, the log warns that work is not verified-complete, and it deliberately refuses to open a pull request.

It then exited 0. The exit code is the only signal an automated caller reads, so a CI job, a Kubernetes Job, or a shell && chain saw a stagnation force-stop as a pass. Meanwhile max_iterations_reached, which is the same class of outcome, exited 20. Two terminals with identical meaning reported opposite results.

Force-stop now exits 20 on both routes, alongside max-iterations and budget-exceeded. Verified completions and human-controlled pauses are unchanged at 0.

The model was told to fix issues without being told which

The strongest accuracy lever in the loop was disabled by a nesting bug.

Structured reviewer findings were injected only when a gate had written gate-failures.txt. A reviewer council can BLOCK without that file, and in that case the prompt said “FIX THESE ISSUES BEFORE PROCEEDING” with no issues attached. The model re-derived what was wrong from scratch on every iteration, which is the slowest and least accurate way to converge and the direct cause of long, expensive runs.

Proven by execution rather than inspection. With real reviewer findings present and no gate-failures.txt, the baseline prompt contained neither the offending file nor the defect text; with the fix it contains both, with severity, so iteration two is a targeted fix instead of a guess.

Runs stop being chopped into pieces a frontier model does not need

The iteration budget was written when a model could not hold a whole feature in one pass. On a frontier tier that is now harmful: every iteration boundary is a context reset, and re-planning mid-feature is where a run loses the thread.

The budget now scales with the capability tier actually dispatched. On a real 322-word issue: high went from 18 estimated iterations to 3, medium to 9, small unchanged since smaller models genuinely benefit from more, smaller steps. This scales the estimate and the ceiling only. Nothing forces an early stop.

A bug fix no longer pays for greenfield orchestration

Competitor web research, load and performance testing, regression simulation and UAT all defaulted on, so a scoped fix against an existing repository paid for all of them. An issue-sourced spec on a repository with real history now skips them, auto-detected rather than requiring another flag.

Every trust gate stays on: code review, security, unit tests, end-to-end. Speed comes from not running phases that are irrelevant to the change, never from skipping verification. Greenfield builds and whole-repository refactors keep the full suite.

Complexity scoring stops inflating itself

One PostgreSQL project counted as two data stores, because postgresql and postgres were separate keywords. ORMs counted as stores, as did the bare words database and migration, so a line about adding a migration scored two. Aliases now collapse to one name and access layers no longer count as storage.

v8.8.1

A running build displayed as STOPPED

Reported from a real run: loki start <issue-url> was working normally – the process alive, STATUS.txt reading BUILDING, iterations advancing – while the dashboard showed STOPPED, iteration –, 0 agents.

The liveness check had two sources and a background CLI run writes neither. It does not create .loki/loki.pid, and the engine only UPDATES .loki/session.json when that file already exists rather than creating it. Both checks failed, so every CLI-started build fell through to “stopped” the entire time it ran.

Such a run does register itself in .loki/pids/. That is now a third source. Liveness is still proven by signalling the process, never by a file existing, so a stale entry from a crashed run still reads as stopped.

Capability tiers did nothing on codex

Asking for small, medium or high on codex selected nothing: all three tiers fell back to an empty default, so no model was sent and the tier vocabulary was decorative. Users had to name a model by hand, which is exactly what the tiers exist to avoid.

Each tier now resolves from the catalog to its own model:

small -> gpt-5.6-luna budget, high volume medium -> gpt-5.6-terra the default high -> gpt-5.6-sol frontier work

Model IDs verified against OpenAI’s published model documentation. Naming a model explicitly still overrides the tier verbatim, and a Claude alias arriving through the generic chain still cannot reach codex.

Displayed per-token rates for the new line are placeholders scaled from the previous model and are labelled as unverified in the source. They drive a display estimate only and never a gate.

v8.8.0

The model picker offered models it could not dispatch

Starting a run with a non-Claude provider showed haiku/sonnet/opus in the model picker. On codex those are undispatchable, so the control offered choices that could never be honoured. The picker now derives its options from the provider actually executing, read from run-time state rather than from an environment variable the dashboard process never sees. A codex run offers that provider’s model and the generic capability tiers, with no Claude alias anywhere in the payload. Mid-run switching is reported as unsupported where the engine does not support it, instead of accepting the request and ignoring it.

The write half is fixed with it. The spawn endpoint validated the picked model against a Claude-only allowlist, so every value a codex user could choose was silently discarded and the run started on the provider default with no error. Generic tiers now pin through the provider-agnostic lever rather than the Claude-only environment triple, which was inert everywhere except Claude.

A runner that finished too quickly was reported as a launch failure

The lineage guard proves a spawned runner is the child we launched. On Linux it reads /proc//environ, which disappears the moment the process is reaped, so a fast-exiting runner was indistinguishable from one that never carried the marker. The supervisor reported it as a launch failure. A vanished process is now treated as unknown rather than absent, resolved against the process handle. A live process genuinely missing its marker still fails closed.

loki help reached no command

loki help proof printed the same generic front page as bare loki help, while loki proof --help printed real help. Two spellings of one request, one of them useless. They now agree. Because the help path re-enters dispatch, and dispatch reaches commands that start and stop things, asking for help is now structurally incapable of executing anything.

Speed and housekeeping

v8.7.0

Five items aimed at one goal: making the harness work with a model that is not Claude. Two were bugs that would have made a cheap or local model look worse than it is.

Fixed

Added

Fixed (scale)

v8.6.1

Post-release fixes found by validating the PUBLISHED v8.6.0 artifact rather than the source tree, plus the first measured evidence that the harness carries output quality independently of the model.

Fixed

Measured

Added

v8.6.0

Enterprise deployment, CLI operability, and the first adoption instrumentation. Twenty-five items. Fourteen were real defects, and the ones that mattered shared a property: every one was invisible to a green CI.

Fixed – things that reported success while failing

Added

Changed

v8.5.2

Patch. The timeout-safe provider seam was the broken one.

For Codex, provider_get_tier_param returns an EFFORT level (xhigh, high, low), not a model name. The code says so in its own BUG-PROV-012 note. provider_invoke_argv passed it to --model anyway, so every argv-based invocation sent --model high and the API rejected it:

400 invalid_request_error: The 'high' model is not supported
    when using Codex with a ChatGPT account.

This mattered more than an ordinary bug because provider_invoke was fine, so the broken path was the argv seam, which exists precisely so a call can be wrapped in timeout (a shell function cannot be exec’d by timeout). A hung provider could not be bounded without also breaking the invocation. And it failed silently: five trials produced no artifact while looking like slow runs.

Effort now travels in CODEX_MODEL_REASONING_EFFORT, exported so it survives the timeout wrapper, and an empty model name omits the flag rather than sending --model "". Verified end to end: 51 seconds, artifact correct.

Measuring codex properly, and what it showed

Five trials of an identical artifact-verified task through the provider layer, free tier, no API spend:

300s (timeout)  48s  80s  71s  67s
4 of 5 succeeded, median 69s, range 48 to 80s

The timeout is reported rather than discarded. Dropping it would turn a 4-of-5 success rate into an implied 5-of-5 and overstate reliability.

The spread is the actual finding, and it is why a single sample was refused earlier: one shot had timed out and would have been published as “codex: timeout” when the same command completed in 51 seconds minutes later. That would have been a fabricated competitive claim.

This is still not a cross-CLI comparison, and the artifact says so. Only codex has been measured to five trials; opencode, aider, Claude Code and cursor-agent have not been run on this task, so no ranking is asserted.

v8.5.1

Patch. The free on-ramp is proven by a build, not by a tier label.

Codex is the only free on-ramp left since Gemini’s free tier ended on 2026-06-18, and this project had it pinned at “Tier 3” on v0.98 assumptions while the installed CLI had moved to 0.146.0. The label was the stale part, not the code.

Measured on 2026-07-30, one complete invocation through the Loki provider layer rather than as a direct CLI call, because a raw CLI call proves nothing about whether Loki can drive it:

provider    codex-cli 0.146.0, ChatGPT auth, no API key
task        create hello.txt containing exactly "hi"
wall clock  72s
tokens      55,889
API cost    $0.00
artifact    hello.txt present with exactly the expected content

A keyless path that demonstrably completes a build is the strongest answer to “why would I install this”, and it is the model-agnostic claim made concrete instead of architectural.

benchmarks/results/free-onramp.json records it with an explicit scope limit: one task, one invocation, not a quality or speed comparison against a flagship and not a multi-iteration build.

The accompanying test deliberately does not invoke the provider. CI has no ChatGPT auth, and a test requiring a login would be skipped forever and rot. It asserts what breaks silently instead: that provider_invoke is defined after loading, that the autonomous flag is non-empty (an empty one makes codex run interactively and hang a build), and that --sandbox and --model exist on the installed binary rather than in documentation.

A comparison we chose not to publish

A cross-CLI speed table was attempted and deliberately withheld. The same codex command that completed in 72 seconds later timed out at both 200 and 300 seconds on the identical task and machine, while a trivial prompt answered in 9 seconds, so the CLI is healthy and the variance is task-level.

A one-sample-per-cell table would have recorded “codex: timeout” as a fact about a competitor when that exact command had already succeeded. That is a fabricated competitive claim, and a comparison that makes a competitor look bad on an unstable sample is worse than no comparison: it gets caught, and it takes the credibility of every other number with it. Recorded as INCONCLUSIVE in benchmarks/results/cross-cli-headtohead.json with the contradiction and what would make it publishable.

v8.5.0

Minor release. Know what you paid for, and what the wait was.

The receipt now separates progress from rework

Iterations multiply both wall clock and cost, and the Evidence Receipt reported only a count. A user seeing “6 iterations” could not tell real work from a gate false positive forcing five redos, and neither could we, which is worse: it hid whether an expensive run was a slow model or our own harness being wrong.

That is not hypothetical. A measured run here had the agent claim done on every iteration while a mock-integrity false positive blocked all six, and a start-sha bug made the council see a permanently empty diff so it could never vote done. Both were harness defects billed to the user as model cost.

The receipt now carries the split, with the share and the basis:

progress 2 ($0.24) | rework 1 ($0.12) | 33% of the run

“Paying for your own errors” is a named churn driver in this category and no competitor surfaces it. It composes with the receipt rather than bolting on: rework cost is a deterministic fact derived from records already written, so it sits with the facts, not the AI assessments.

Two limits are stated in the artifact itself. Rework counts failed iterations only, so an iteration that completed but was forced to repeat by a gate counts as progress, which makes the number a floor rather than a ceiling. And with no efficiency records the attribution is omitted entirely rather than reporting zero rework, because claiming 0% for a run nobody measured would be fabricated reassurance.

The longest silence, not the total time

A ten-minute build reporting something every twenty seconds feels fast. A four-minute build silent for three of them feels broken. The loudest complaints about agents in this category are not that they were wrong, but that they went quiet: “always stuck at Preparing write” (76 comments), “not making changes to code”, “agent does not execute functions”.

autonomy/lib/silence_report.py reports the worst gaps and names the step each one followed, because a duration alone cannot be fixed:

214.0s  after 'code_review_start' before 'code_review_complete'

Idle time between sessions is excluded and said so explicitly. Measured against a real 4,240 event stream, 4,234 gaps were correctly ignored as a human being away from the keyboard, leaving three genuine in-build gaps. Without that filter, lunch breaks swamp the signal.

Both tools refuse to invent data: malformed records are skipped rather than defaulted, and an absent file reports “nothing to measure” instead of a zero-silence claim.

v8.4.0

Minor release. Time to first preview is no longer invisible.

The number that describes whether the product felt fast

app-runner.sh has recorded seconds_to_first_preview since v8: write-once, atomic, and it already refused absurd values. Nothing ever read it. A grep across the whole repository found exactly one reference, the line that writes it. Not the run summary, not the Evidence Receipt, not the dashboard.

Research on this category puts the delight peak at a working preview around four minutes and the churn peak at a long silent wait, so this is the single number that best describes whether a build felt fast. It was invisible to the person who waited for it and to us.

The run summary now shows it beside the live app URL:

Files:         1 (+1 / -0)
First preview: 247s

A run where no app ever came up prints nothing rather than “0s”. A fabricated zero would claim instant preview for a run that never previewed, which is the same false-green the Evidence Receipt exists to prevent. Negative and malformed values are refused too, and a corrupt file does not break the summary.

The efficiency baseline is collectable, proven without spending

.loki/metrics/efficiency/ was empty in this checkout, which meant every claim about speed or cost was intuition with no “before” to compare against. The obvious move is to run a real build and see what lands, but that costs money and tens of minutes, and a broken recorder means spending both and having nothing.

So the pipeline was proven first with no provider, no key and no spend: track_iteration_complete writes a complete per-iteration record with the DISPATCHED model (not a provider default, a mislabel that once made a model-equivalence benchmark unfalsifiable), and collect_efficiency folds records while keeping the property the whole baseline rests on: an absent record reports usd=None, a genuine zero reports 0.0. If those collapsed, every published cost figure would be unfalsifiable.

Also

Four suspected gaps were investigated and found already implemented, so no code changed: the simple-tier fast path is fully wired (seven consumers, including a doc-suite skip worth roughly 270 seconds per simple build and a council floor that drops from three iterations to one), reviewer count already scales with complexity, and all three behaviours already had registered passing tests.

v8.3.3

Patch. The first-run path is now covered on the shell most users actually have.

v8.3.2 fixed one bash 4 construct that made loki quickstart return zero templates on macOS. Fixing one call site does not close the class: any future bash-4-only construct anywhere in the first-run path reintroduces the same failure, and our test harness runs homebrew bash 5, so the suite structurally could not see the shell most users have.

The first-run path is audited clean of declare -A, ${var^^}, ${var,,}, readarray and mapfile, and tests/test-first-run-bash32.sh now walks the commands a brand-new user actually runs, in order, under /bin/bash explicitly. Running them under $SHELL would reproduce the exact blind spot that let the v8.3.2 bug ship.

Verified under bash 3.2.57: the welcome banner and all three next steps render, loki tour produces an Evidence Receipt with its honest “VERIFIED WITH GAPS” headline, and the quickstart scorer returns three ranked templates where it previously returned none.

Nothing requiring a provider, key, network or spend is asserted. The test covers what works before a user commits anything, which is also the honest boundary of what we advertise as keyless.

v8.3.2

Patch. loki quickstart was returning nothing on every Mac.

The guided first build was broken on macOS stock bash

_qs_score_templates used declare -A, a bash 4 associative array. macOS ships bash 3.2.57 as /bin/bash, frozen since 2007 for licensing reasons and never updated by Apple. On the stock shell of the most common developer platform the scorer printed declare: -A: invalid option and returned ZERO templates, so the guided first build we point new users at from both the welcome screen and loki doctor showed an empty picker.

Three things hid it. The function BODY parses fine under 3.2, so bash -n and sourcing both looked healthy and only CALLING it failed. It degraded to empty rather than crashing. And the test harness runs homebrew bash 5, where it works perfectly.

Rewritten with a flat name/score list folded by awk. Verified as a pure portability change, not a behaviour change: output is identical between bash 3.2 and bash 5 across five briefs, and identical to the pre-fix bash 5 output across six. Ranking stays correct, with “chrome extension” ranking chrome-extension first and “saas billing” ranking saas-starter first.

tests/test-quickstart-bash32.sh runs the scorer under /bin/bash explicitly, because testing it under $SHELL would reproduce exactly the blind spot that let this ship.

Two tests existed but the gate never ran them

test-shard-coverage.sh and test-quickstart-bash32.sh were both written, both passing, and both silently unregistered: the edits that were supposed to add them to the runner failed to match their anchor text and reported it in output that went unread. The gate stayed at 289 suites while two new tests sat outside it.

Caught by noticing the suite count did not move, which is the only reliable tell. Both are registered now and the count is 291.

v8.3.1

Patch. The gate stops lying about why it failed.

CI is 2.33x faster, measured

Shell tests was 13m01s of a ~15m pipeline, six times the next slowest job, because 289 suites ran serially on one runner. A 4-way shard cut it to 5m35s.

Both figures are observed, not projected. An earlier projection in this repo said ~3m15s and was wrong: it assumed the suites cost roughly the same, so four shards would be a clean quarter. They do not. Shard timings were 5m35s, 2m33s, 3m50s and 2m26s, and the wall clock follows the slowest shard rather than the mean. The projection has been replaced with the measurement rather than quietly removed.

Two latent bugs the sharding exposed

Both passed serially and failed sharded, which is what made them worth finding.

test-embeddings.sh called skip() and then exit 1, so an unavailable optional dependency was reported to the runner as a failing suite. It hid behind suite ORDER: run serially, an earlier suite left the module importable and the branch never executed. It now exits 0 while still printing the reason and a visible SKIP line, so the missing dependency stays reported rather than swallowed.

test-memory-speed-privacy.sh piped the benchmark through 2>&1, folding any stderr line the interpreter emits into the same stream as the JSON document. The parse then died with “Expecting value: line 1 column 1” and named the parser instead of the warning that caused it. Stderr now stays on stderr, and an empty document is reported as “bench produced NO output” rather than as a malformed one, because those are different failures and conflating them costs an investigation.

A gate that cannot tell “optional thing not installed” from “the code is broken” is one people learn to ignore, and that is how three red releases shipped over a real bug earlier the same day.

v8.3.0

Minor release. The first minute, the shop window, and the enterprise promise.

doctor names what blocks you instead of counting it

On a bare machine loki doctor printed 15 WARNINGS – sentrux, GPG receipt signing, bash >= 4, Bun, python3.12, truecolor, the inline-image probe, every one of them OPTIONAL – surrounding exactly 2 real blockers, then ended with “Some required prerequisites are missing” without naming them. A first-time user cannot separate the 2 that matter from the 15 that do not, and the honest reading of that screen is “this needs a lot of setup”. That is where someone closes the terminal.

It now names each blocker with its exact fix command, states plainly that everything else is optional, and points at loki tour, which works with no provider, no key and no network – so a blocked user still reaches a real result in their first minute.

An evaluation page a buyer can falsify

Our competitive material was a ten-column feature grid written January 2026 against v2.36.9, unchanged while the repo reached v8.2.0. Six months and thirty releases stale, and the unverifiable KIND of comparison: every cell a subjective grade assigned by the vendor being graded.

docs/EVALUATING.md replaces it with five claims that each carry a runnable command, all executed before publishing. It also carries a “What we do not have” section – no enterprise case studies, no independent benchmark placement, no audit of the closed-source products, and generation is not air-gapped. The old grid is now labelled STALE and points there.

The brownfield read-only promise is now enforced, not asserted

The README sends someone with a ten-year-old revenue-critical codebase to loki modernize heal --assess on the promise it changes nothing. Verified behaviourally – zero file content changed, HEAD did not move, working tree clean afterwards – and locked by a content-addressed test that hashes every file before and after. One stray write turns the safest thing we offer into the scariest, and there is no second evaluation.

The MCP registry manifest was 30+ releases stale

server.json was pinned at 7.34.1 while the repo shipped 8.2.0. Nothing caught it because nothing checked it; the cost would have landed at submission time, advertising a weeks-old build to exactly the audience deciding whether this project is maintained. Now current, added to the release checklist in CLAUDE.md, and enforced by a test that also pre-checks the two things which would reject a submission after the interactive OAuth: the 100-char description cap and the namespace-ownership marker.

Failures that explain themselves

The supervisor test surfaced only AssertionError: 127 != 0 – an exit code with none of the cause – which cost two full investigations that both ended without a root cause. It now prints the child log and the resolved child PATH, and doing so immediately produced the lead those investigations missed: the log is empty, so the child never started and the failure is on the spawn side.

Also fixed three tests that encoded assumptions about their environment rather than about the behaviour under test: one wedged only the mkdir locking path (a Linux runner has flock), one built a git fixture that failed silently without a configured identity, and one assumed its host had no blockers.

v8.2.0

Minor release. Run any model, and find the feature that was already here.

Telemetry can no longer outlive the run that emitted it

v8.1.0 fixed a specific unbounded wait in events/emit.sh. It was a real fix and it was not sufficient. Measured on a development machine on 2026-07-30, after that release shipped: 63 orphaned emit.sh processes alive, the oldest 10 hours 51 minutes, each burning roughly 5% CPU, contributing to a load average of 63 on a 14-core machine. Every one of them started after the fix landed, so whatever wedges emit.sh is not the path that was repaired.

Patching each individual hang is an arms race, because a hang anywhere in the script has the same cost to the user: a hot laptop and a process that never ends. emit.sh is fire-and-forget telemetry – nothing waits on its result, and a dropped event is strictly cheaper than a wedged process. So rather than proving no path can block, this release caps the lifetime of every path.

A watchdog now SIGKILLs the emit process after LOKI_EMIT_MAX_SECONDS (default 10; set 0 to disable). It is deliberately blunt – no cleanup hook, no graceful drain – because the failure mode being defended against is exactly “the graceful paths did not run”.

Being straight about the limits of this fix: the root cause of those 63 orphans is still unknown. The watchdog bounds the damage to 10 seconds; it does not explain the hang. An earlier diagnosis blaming the lock-retry loop’s attempt counter was tested and disproven – a deterministic wedge terminates in 2s on the pre-fix code too, so that loop was already correctly bounded.

tests/test-emit-self-reaper.sh asserts a genuinely blocked emit is reaped at its deadline (with a non-vacuity check that it actually blocked first), that a healthy emit is untouched and still writes its event, and that the watchdog leaves no stray processes of its own.

Verification in milliseconds

Verification that takes minutes cannot be embedded in an IDE, a CI step, an MCP tool, or another vendor’s agent. autonomy/lib/fast_verify.py makes the deterministic checks fast enough to be a dependency.

Measured on this repo, 1,932 tracked source files:

scope before after
full repo, cold 11,040 ms 298 ms
full repo, warm 11,040 ms 87 ms
diff-scoped 11,040 ms 19 ms

The work was never slow; the architecture was. One detector performed four separate full-tree find walks (110 ms each) and spawned ~25 subprocesses at ~24 ms of interpreter startup apiece. git ls-files returns the same set in 37 ms, already deduplicated and gitignore-aware.

Five rules: walk once and classify in that pass; run detectors as pure functions in one process; cache findings by content hash, so a moved file keeps its result, a changed file gets a new key, and there is no staleness window; scope to the diff; and admit no model call and no network on this path.

That last rule is the point, not a limitation. A verdict you can re-derive is a fact; a verdict a model produced is an opinion. A test greps for anthropic|openai|requests|urllib|httpx|provider_invoke and fails if any appear, so the constraint cannot erode.

Three surfaces, same engine: loki verify --fast, the loki_verify_fast MCP tool (20 ms end-to-end), and the Python API.

The receipt separates evidence from opinion

Only four of the eight quality gates are exogenous – deterministic and impossible for the agent to author: static analysis, mock integrity, test mutation, documentation coverage. The other four are model-coupled: the agent writes both the test and the fix, and the council, devil’s advocate, and magic debate are LLM judgments.

Published research measured a strict three-judge ensemble still accepting 55% of errors, and our “blind” council is blind only to itself – every reviewer reads the same candidate diff, which scores plausibility rather than correctness.

The Evidence Receipt now reports the two groups separately, and the headline verdict is computed from the exogenous four only. Advisory results are shown but can never lift a verdict. Unknown or renamed gates default to exogenous, so a gate can never silently lose its power to block.

proof-verify.py mirrors the same provenance rules. It is the independent re-derivation users are told to trust, and a generator it cannot reproduce would report drift on a receipt nobody tampered with – a false alarm from our own trust artifact. A drift guard asserts both sides agree on every gate-name shape.

opencode: the model-agnostic route

providers/opencode.sh makes opencode a first-class provider. It is a registry, not a fixed table: 75+ providers through the AI SDK and Models.dev, any OpenAI-compatible endpoint, plus local models via Ollama, LM Studio, and llama.cpp. Users can add providers that are not in its built-in list, which means we stop maintaining a per-vendor model list.

loki provider set opencode
export OPENROUTER_API_KEY=sk-or-...
loki start ./prd.md              # defaults to deepseek-v3.2, open weights

Why opencode and not aider: capability was never the question, maintenance was. Verified 2026-07-29 – anomalyco/opencode at 190,871 stars with v1.18.9 released 2026-07-28, against Aider-AI/aider whose last release was v0.86.0 on 2025-08-09, with Roo Code archived and Continue read-only. Integrating a dormant project would have been a slow-motion outage.

The CLI surface was verified against the installed binary rather than assumed: opencode run [message..] takes its prompt positionally, with -m/--model.

Loader fix found on the way in. loader.sh required PROVIDER_AUTONOMOUS_FLAG to be non-empty, so a CLI that needs no autonomy flag was rejected as “incomplete”. It now sits alongside PROVIDER_PROMPT_FLAG in the allow-empty set. The variable must still be defined, so an author who forgets it entirely is still caught, and there is a test asserting exactly that.

The judges work on any provider, and keep their timeout

Eight auxiliary judge sites shell out to claude -p directly instead of going through the provider abstraction. The reason is written into the source: “timeout needs a real command, not a shell function.” That is true, and it is why the obvious fix is wrong – routing them through provider_invoke would mean dropping their timeout, reintroducing the hang class that left 59 orphaned processes alive for 21 hours in v8.1.0.

New seam: provider_invoke_argv <tier> <prompt> populates _LOKI_INVOKE_ARGV with a real command line without executing it, so a caller can write

provider_invoke_argv development "$prompt"
timeout 120 "${_LOKI_INVOKE_ARGV[@]}"

and get provider-agnostic dispatch and a preserved timeout. Measured against a stub CLI that sleeps 300s: the argv form returns 124 after 3 seconds, the shell-function form returns 127 immediately because timeout cannot exec a function. That asymmetry is asserted in the test suite.

_loki_done_recog_provider_ok and _loki_prd_enrich_provider_ok now ask a capability question rather than an identity one. They required LOKI_PROVIDER=claude, so users on codex, opencode, cline, or aider silently lost done-recognition and PRD enrichment. Any provider exposing the seam now qualifies, with the legacy claude-binary check retained as a fallback.

Weak models can be judged without being punished for formatting

A strict JSON carve is the most model-sensitive contract in the engine: schema adherence varies most across models, while every coding model can state a verdict in prose. When the carve yields nothing, the council now scans for a standalone verdict word and accepts it only if exactly one of APPROVE/REJECT appears. Output mentioning both, or neither, stays INCONCLUSIVE.

Recovering a verdict the model genuinely stated is legitimate; inventing one is not. Recovered verdicts carry "recovered": true so they stay auditable, and the twelve pre-existing no-fabrication assertions still pass unchanged.

Brownfield is no longer hidden

loki modernize heal has shipped since v6.67.0 and the README mentioned it once. Greenfield (“build me an app”) is the crowded half of this market; the ten-year-old repo that pays the bills is the valuable half, and we were hiding our answer to it.

The README now opens with the read-only path:

loki modernize heal ./your-repo --assess     # changes nothing

which reports language mix, a four-level maturity rating, technical-debt signals, and a ranked list of where to start, ordered by blast radius. Every claim in that section was verified by running it against a legacy fixture, including --assess --json and the --compliance healthcare|fintech|government presets.

v8.1.0

Minor release. The telemetry layer could hang forever and burn the CPU. For a product whose claim is that it proves work actually happened, that is the worst possible place for a defect.

Telemetry no longer outlives the run (P0)

Found on a real machine at 0% idle with the fans at full: 59 orphaned events/emit.sh processes, the oldest alive 21 hours, orphaned to launchd. emit.sh spawns on every CLI invocation and was never exiting.

Both of its lock paths could hang:

Fixed: flock -w 5 with an unlocked-append fallback; staleness checked on every iteration; attempts cut 500 to 100; read -t replaces the forked sleep. Measured after: 10s to 2s, failure to success, no fork storm.

Observability must never block the thing it observes. A rare interleaved line is strictly better than a hung CLI, so the append now degrades instead of waiting.

The council no longer fabricates reviewer verdicts

Every provider arm in autonomy/council-v2.sh substituted a literal {"verdict":"REJECT","reasoning":"review execution failed"} on any miss – CLI absent, timeout, transient error, unparseable output – and the tally counted “anything not APPROVE” as a rejection. The engine therefore recorded a reviewer vote the model never gave, and blocked the run on it. Twelve such sites existed across five providers.

The devil’s advocate had the same default, so a single transient parse failure could silently overturn a unanimous approval.

This is the same defect class as an Evidence Receipt attesting to a diff stat it never measured: the artifact asserts a fact nobody established.

It matters most on a cheap or weak model, where low format compliance became a permanent BLOCK that reads as “Loki is broken” rather than “your model could not produce a parseable verdict.”

Now: an unobtained verdict is INCONCLUSIVE. It is counted separately, never as a rejection, and reported to the user. A genuine REJECT still counts as one, and an inconclusive devil’s advocate leaves a unanimous approval unchanged.

The receipt names the gate that stopped the run

A blocked run’s Evidence Receipt gave only a bare outcome – intervention. That tells a reader something stopped them, not what, and not how close it came to its threshold: the single most actionable fact about a blocked run.

The engine already had it. .loki/signals/GATE_ESCALATION.json is written on escalation, and both COMPLETION.txt and PAUSED.md already surfaced the gate, count, and threshold. The signed receipt was the one surface that stayed silent.

It now reports Blocked by: code_review (failed 3 times, threshold 3). These are deterministic facts read verbatim from a file the engine wrote, so they live in the facts block rather than the AI-assessment block. A missing or malformed signal leaves the fields empty – which reads as “no escalation recorded”, never as a false claim – and never breaks receipt generation.

Cheap models are now reachable, and the docs stopped lying about how

Three defects that together made the “run any model” story unusable:

Both surfaces now present two clearly separated routes: OpenAI-shaped endpoints via aider/cline (which speak that API natively), and Anthropic-protocol gateways via ANTHROPIC_BASE_URL with an explicit note that OpenRouter does not qualify.

Status surfaces stop contradicting each other

Tests

v8.0.4

Patch release. The two DURABLE status surfaces disagreed, and --json hid the one field that would have let a script notice.

STATUS.txt vs COMPLETION.txt

Measured on a real paused run: STATUS.txt reported Failed: 0 while COMPLETION.txt reported failed=1. Both read the same .loki/queue/failed.json, so they cannot legitimately differ.

loki why –json

Tests

v8.0.3

Patch release. Two live status surfaces contradicted each other about whether a build was running.

Dashboard status

Measured on a preserved real run that was genuinely paused on a gate:

The pre-existing liveness check is preserved: a dead PID with no control file still reports stopped, and a corrupt or missing state file no longer breaks the stream.

Evidence Receipt

v8.0.2

Patch release. loki why – the command v8.0.1 tells users to run after a failure – was misreporting the run it exists to explain.

loki why

Measured against a preserved real run that produced 9 files and 1300 insertions and paused on a gate:

No verdict, threshold, or gate logic changed. Every gate decision examined in this work was a true positive; the defects were in how correct decisions were reported.

v8.0.1

Patch release. Every change is a bug fix; no behavior a working v8.0.0 install depends on was altered.

Trust core

Reporting

Every gate decision examined during this work was a TRUE positive. The defects were in how those correct decisions were reported.

CI and portability

The Tests workflow had not started for four days: a secrets reference in a step-level if: is rejected at parse time, so ZERO jobs were created and the only symptom was “workflow file issue”. Fixing that surfaced a backlog of platform bugs, each of which reported a product failure for an environment difference:

New guards prevent each class from returning: bash 3.2 parse compatibility, stat ordering, hardcoded home and toolchain paths, greenfield diff-stat, per-outcome guidance, and the receipt’s run-level baseline. Every guard was verified non-vacuous by reintroducing the bug it exists to catch.

v8.0.0

Loki’s first 8.x. No 8.x has ever been published (npm latest was 7.129.5), so everything below ships together as ONE major release.

Fixed before release: regressions found by auditing v8 against v7.129.5

The legacy (non-SDK) engine changed substantially in this release, so it was audited line-by-line against v7.129.5 – the version users are running today – specifically for things that WORKED on v7 and would BREAK on v8. Everything below was reproduced first, then fixed, then guarded by a registered test.

Test surface

Registered test suites went from 106 to 259. Roughly two thirds of the suite existed but was wired into no runner, and therefore never ran – which is how several of the defects above reached this branch unnoticed. A coverage gate now fails CI if a new test file is added without being registered. The sections previously drafted under a separate “v8.1.0” heading are folded in here: they were never released independently, and shipping a first-ever 8.x as 8.1.0 with a phantom 8.0.0 beneath it would describe a release history that does not exist.

Default-off posture (the whole release). With no LOKI_* flag set, the engine behaves byte-identically to v7: the bash claude path runs and none of the SDK code loads. Every new capability below is opt-in or additive, and the SDK RARV loop specifically remains DEFAULT-OFF pending its acceptance tests.

v8 harness intelligence (jcode-informed)

Four measured-harness disciplines, adopted onto the existing trust core rather than bolted beside it. None of them can weaken a gate.

Operational observability

One-switch SDK activation + rollback + packaging gate (default-off)

The SDK route is reachable through eight per-site LOKI_SDK_* flags. This makes it operable at scale without learning all eight, adds the rollback that must exist before the loop can ever default-on, and proves the SDK judge ships in the packed artifact. Everything remains OPT-IN and DEFAULT-OFF: with nothing set, the engine is byte-identical to v7.

One-switch mode (LOKI_SDK_MODE). A single operator switch sets the default for all eight per-site flags at once via a write-once resolver, mirrored byte-for-byte in bash (autonomy/lib/sdk-mode.sh) and TypeScript (loki-ts/src/runner/sdk_mode.ts):

Symmetric rollback (LOKI_LEGACY_BASH). Already honored by the bin/loki shim; now also honored inside the Bun provider resolver (selectClaudeInvokerKind), so it forces the legacy claude spawn even if the Bun route is reached another way. It wins over LOKI_SDK_LOOP. This lands and is CI-tested now, before the loop ever flips default-on, so the escape hatch exists before it is needed. Today (loop default-off) it is a no-op.

Packaging gate (CI). A new sdk-tarball-no-binary job packs the real npm tarball, installs it into a clean prefix with NO claude binary on PATH, and proves the bundled @anthropic-ai/sdk judge SHIPS: keyless, loki internal sdk-judge fail-closes with exit exactly 1 (no module-not-found). A billable real-verdict probe is gated on the ANTHROPIC_API_KEY secret. This catches the “.gitignore-excluded dist” class of bug for the SDK route and is the gate that later lets LOKI_SDK_LOOP flip default-on safely.

Sandbox deprecation notice (annotation-only). The Docker Desktop microVM path now emits a one-shot notice that its bundled-claude-binary template is legacy under the SDK route, while stating explicitly that code-execution ISOLATION is RETAINED (in-process SDK runs at host privilege, so isolation matters more, not less). No behavior change, no removal; opt out LOKI_SANDBOX_DEPRECATION_QUIET=1.

Not in v8 (deliberate, YAGNI). The Message Batches API judge fan-out was scoped out after a codebase-wide audit: every judge fan-out is either a single inlined call or on the RARV critical path, where the Batch API’s minutes-of-async polling is a latency regression, not a win. No batch-appropriate site exists today; revisit if a latency-tolerant bulk-judge site is introduced.

The Anthropic SDK transformation (MAJOR)

v8 replaces Loki’s bash claude -p wrapper with the Anthropic SDKs. Every model call the engine makes can now run through a library call instead of spawning the claude CLI: the one-shot JUDGE/TEXT sites on the raw @anthropic-ai/sdk (pure HTTPS, bundled into the shipped bundle, zero extra install), and the agentic RARV MAIN LOOP on the @anthropic-ai/claude-agent-sdk query(). This is the enterprise/SaaS deploy win: a container or SaaS worker can run the SDK route with just an API key, no interactive claude login and no user-managed CLI version drift.

Everything below is OPT-IN and DEFAULT-OFF. With no LOKI_SDK_* flag set, the engine behaves byte-identically to v7: the bash claude path runs, and none of the SDK code loads. Every SDK path is fail-closed: any miss (flag off, no key, transport error, refusal, malformed output, timeout, empty) falls through to the existing claude/deterministic path. A quality gate or completion council can never fake-PASS or fake-APPROVE via an SDK path, and a failed iteration is never counted as success.

Judge / text call-sites on the raw SDK (each behind its own flag):

RARV main loop on the Agent SDK (LOKI_SDK_LOOP=1):

Packaging / distribution:

Env knobs: LOKI_SDK_DONE_RECOG, LOKI_SDK_COUNCIL_V2, LOKI_SDK_CODE_REVIEW, LOKI_SDK_COUNCIL_VOTE, LOKI_SDK_VOTER_AGENTS, LOKI_SDK_GRILL, LOKI_SDK_PRD_ENRICH, LOKI_SDK_LOOP (all default off); LOKI_SDK_COUNCIL_MODEL, LOKI_SDK_REVIEW_MODEL, LOKI_SDK_JUDGE_MODEL, LOKI_SDK_REVIEW_TIMEOUT, LOKI_SDK_GRILL_MODEL, LOKI_SDK_PRD_ENRICH_MODEL.

Validation: every SDK path was council-reviewed to unanimous approval (per cluster and a final whole-arc pass), the SDK loop was E2E-verified on the live API building real apps (a one-file utility, a multi-file CLI, and a full-stack Flask REST API whose 6 tests pass), and the full test suites stay green on both routes.

v7.129.5

Fixed: corrupt Evidence Receipt when a test runner emits colored output

enforce_test_coverage embedded a test runner’s raw output into the receipt’s test-results.json after stripping only quotes and newlines. jest, vitest and other runners colorize output by default, so the details/summary field carried ANSI escape sequences and other C0 control characters. Those are invalid inside a JSON string, so json.load on the receipt raised “Invalid control character” – and every downstream reader that swallows the parse error saw the field (and often the whole receipt) as empty. A silently corrupt Evidence Receipt is worse than none: it is the honesty core reading as blank.

The sanitizer now also deletes every remaining control character (\000-\037, including ESC) after converting newlines to spaces, so the receipt is always valid JSON regardless of runner coloring. Affects any jest/vitest/colored-output project; present since the receipt was introduced.

v7.129.4

Fixed: auto-doc-gen re-firing every iteration after a timeout

A timed-out loki docs generate call never writes docs-manifest.json (the provider call is killed mid-write), so needs_gen stayed true forever and the full ~300s generation re-fired on every iteration with zero progress. Observed live: ~20 minutes burned across 4 iterations on one real build, with no gate benefit (doc_coverage already passes on the partial docs a timed-out attempt leaves behind).

auto_generate_docs_if_needed (autonomy/run.sh) now writes a separate .last-attempt-timed-out marker so a killed attempt is remembered and not retried every iteration. A genuine completed manifest still drives the existing staleness-based regeneration path unchanged, and clears any stale marker.

v7.129.3

Fixed: code_review gate on oversized / tracked-but-gitignored diffs

A client hit the code_review quality gate returning NO_OUTPUT from all reviewers. Root cause: a directory (916 files / 11MB) was git-TRACKED before being added to .gitignore. Git keeps tracking files committed before the ignore rule, so the gate’s throwaway-index git add -A staged the dir and it bloated every reviewer prompt past the model’s limit, forcing empty output. The gate correctly BLOCKED (all-NO_OUTPUT is treated as INCONCLUSIVE, never a fake PASS), but the block was opaque – no size, no cause. Three fixes (all in run_code_review):

Immediate unblock on older versions: git rm -r --cached <stale-tracked-dir>.

v7.129.2

Fixed: parallel multi-issue follow-ups (detached –pr reliability)

Follow-up fixes found while validating v7.129.1 end to end.

v7.129.1

Fixed: reliable parallel work across multiple GitHub issues

Addresses a client report of running several issues concurrently from one checkout.

Docs: skills/github-integration.md documents the reliable N-issues-at-once pattern (isolated git worktree + LOKI_DIR per issue) and in-session skill usage.

v7.129.0

Added: structured --json-schema output across the review + completion trust paths

Adopted Claude Code’s native --json-schema structured output (verified against CLI v2.1.207) for the verdict paths that previously parsed the model’s free text or prose-wrapped JSON. Each reviewer now emits schema-forced JSON that an adapter re-materializes into the exact legacy text the downstream consumers already parse, so every gate decision, mergeability score, and existing test stays byte-identical. This removes a class of fragility where a markdown-wrapped or malformed verdict could be mis-read.

All adoptions are claude-provider-only (codex/cline/aider keep their text calls), flag-gated on loki_claude_flag_supported "--json-schema" (old CLIs fall through to the prior text path), and opt-out with LOKI_REVIEW_JSON_SCHEMA=off. Every failure mode (rc!=0, empty, malformed JSON, missing/invalid verdict, wrong stop_reason) falls closed to the pre-existing safe direction (blocking / REJECT / inconclusive), never a default PASS. A cross-field guard forces a FAIL verdict whenever any finding is Critical/High, so a self-contradictory PASS+Critical can never be waved through non-blocking.

Note: --json-schema takes the schema as INLINE content, not a file path (a path is rejected by the CLI).

Fixed: council structured dispatch was silently non-functional

The council voter dispatch passed a file PATH to claude --json-schema on both routes, which the CLI rejects (Unrecognized token '/'), so every structured council dispatch failed and fell back to the heuristic text path. Now passes the schema content inline on both routes, with a regression guard that asserts content-not-path.

Fixed: node --test detection on Node 20+/26

Node 20+ made the human “spec” reporter the default for node --test (Node 26 emits it even under output capture), whose i pass N / check-mark lines the tests-gate count parser and zero-tests detector do not match (they expect TAP # pass N / ok N -). Both copies of the config-less fallback (autonomy/run.sh, autonomy/verify.sh) now force --test-reporter=tap for a deterministic, version-independent format. TAP has been a stable built-in reporter since Node 18; no behavior change on older Node.

Fixed: verify tests-gate false BLOCKED when timeout is absent, and unittest zero-discovery

The loki verify tests gate wrapped every test-runner invocation in a bare timeout, which exits 127 (“command not found”) on a system without GNU timeout/gtimeout (bare macOS), and that was misread as a test failure -> spurious BLOCKED verdict. It now resolves a timeout/gtimeout prefix once and degrades to running without a wall-clock cap when neither is present. Separately, python3 -m unittest on a pytest-style suite of bare def test_* functions discovers zero TestCase tests and exits 5; that is now routed through the zero-tests guard as inconclusive (applicable-but-ran-no-tests) rather than fail.

Fixed: PRD-reuse mis-flagged by Loki’s own session artifacts

compute_codebase_signature counted HANDOFF.md / USAGE.md (written and committed to the repo root by the session-end completion path) as a codebase change, so a subsequent PRD-reuse check saw a spurious “codebase changed” and flipped reuse to update, suppressing the reuse disclosure. Those self-generated artifacts are now excluded from the signature.

v7.128.2

Fixed: cockpit fills the terminal + honest fleet (no stale-run zombies)

Three fixes that make loki cockpit genuinely useful instead of a tiny, noisy-looking frame:

Tests: registry reconcile (3 new, tests/dashboard/test_registry_prune.py), encoder width scaling + fleet header (cockpit.test.ts, now 26). Full bun suite 1190/0.

v7.128.1

Fixed: duplicate banner on loki start (new handoff + legacy ASCII banner)

The v7.126.0 loki start handoff renders the Autonomi brand banner, but run.sh still printed its legacy ASCII “LOKI MODE” banner right after it, so an interactive loki start showed TWO banners back to back. cmd_start now exports LOKI_BRAND_SHOWN=1 when the handoff runs, and run.sh skips the legacy banner in that case. The legacy banner is unchanged on the non-handoff path (CI / –bg / –yes / non-interactive / direct run.sh). New test tests/test-start-banner-dedup.sh.

v7.128.0

Added: loki cockpit renders the inline image out of the box (bundled wasm)

The cockpit now draws its full inline image on graphics-capable terminals (iTerm2 / Kitty / WezTerm / Ghostty) with NOTHING to install. The SVG-to-PNG rasterizer is a bundled, platform-independent WebAssembly build of resvg (@resvg/resvg-wasm, shipped as loki-ts/data/resvg.wasm), so there is no native compile step and no per-arch binary. Native @resvg/resvg-js is still used automatically if a user happens to have it (fastest path); otherwise the bundled wasm renders the frame. Only if both are somehow unavailable does the cockpit fall back honestly to the text summary + browser dashboard.

Font fix: the wasm rasterizer runs in a fontless sandbox, so it now loads a real system font buffer (cross-platform search of common font locations) with a sensible default family; without this every text label rendered blank. The SVG’s Fraunces / Inter / JetBrains Mono families gracefully fall back to it.

Robustness: buildSvg normalizes a malformed or partial state (missing verdict, budget, or arrays) to safe defaults so a truncated state file degrades gracefully and always renders a frame instead of crashing the render.

loki doctor’s cockpit section now reports “SVG rasterizer: ready (bundled wasm)” and “Render path: inline image” accordingly. Tests: cockpit.test.ts grew to 23 (real wasm image render, font-bearing raster >30KB, and malformed-state graceful degrade, all fail-when-broken). Full bun suite 1187/0. The honest fallback (protocol none / –no-image / rasterizer unavailable) is unchanged, and never claims an image it did not produce.

v7.127.0

Enhanced: the Loki Cockpit (5 enhancement loops)

Five enhancements to the v7.126.0 cockpit, all dependency-free and honest:

Tests: test-cockpit-follow.sh (E1), cockpit.test.ts grew to 21 (E2 RARV + height now fail-when-broken, mutation-verified), test-cockpit-keys.sh (E3), test-start-handoff.sh grew to 33 (E4), test-cockpit-cmd.sh 16 (E5). Full bun suite 1185/0. loki watch and non-interactive loki start unchanged.

v7.126.0

Added: the Loki Cockpit (rec #6) - stare-worthy start, loki cockpit, dashboard identity

A complete observability layer, built with zero new user-facing tooling (no chafa, no headless Chrome, nothing to install):

The cockpit CLI ships as loki-ts/dist/cockpit.js (bundled) so it works on every npm/Docker/brew install, not just from source. Tests: tests/test-start-handoff.sh (27, incl. the truecolor banner + degradation), tests/test-cockpit-cmd.sh (8), tests/test-dashboard-identity.sh (9), loki-ts/tests/cockpit.test.ts (14, encoder byte-shape + capability matrix + honest fallback, all mutation-verified). No existing command changed behavior.

v7.125.0

Added: unified per-run isolation dial --isolation none|worktree|docker (rec #4)

One canonical knob for how a run is isolated, replacing the fragmented flags with a single clear dial (zeroshot’s none/worktree/docker model, the Loki way):

Every existing flag keeps working as an alias (--parallel = worktree, --sandbox = docker), so nothing breaks and there is no forced migration – this is CLI consolidation, not replacement. Fail-closed on an unknown level (errors with the valid set, exit 2) so a typo never silently downgrades to weaker isolation than the operator asked for. Documented in both loki --help and loki start --help. New test tests/test-isolation-dial.sh (7 cases incl. a real-CLI fail-closed check). Bash-route only (loki start); no behavior change to any other command.

v7.124.0

Added: complexity-proportional verification (rec #3)

The code-review battery now scales its specialist count to the detected task tier (detect_complexity() -> simple | standard | complex), with a HARD FLOOR so nothing shippable ever gets less scrutiny:

This is the zeroshot-style “scale verification to difficulty” lever done the Loki way: only ADD validators for hard work, never subtract below the floor for anything shippable – a cost/speed win on simple tasks with no trust regression. The no-keyword-match path also honors the tier count (padding the historical two defaults with the next-ranked specialists, de-duplicated).

Wired on BOTH routes with parity: bash SPECIALIST_SELECT (autonomy/run.sh) and the Bun selectReviewers() (loki-ts/src/runner/quality_gates.ts), each reading DETECTED_COMPLEXITY. New tests: tests/test-complexity-proportional-review.sh (8 cases, extracts the real selector) and 2 Bun cases in quality_gates.test.ts. No change to the block/no-block verdict logic or to any other gate.

v7.123.1

Added: scope-honesty in completion-council evidence (rec #5)

When the completion council gathers evidence and no PRD checklist exists, the “PRD Checklist Verification Results” section was passive (“No PRD checklist has been created yet”) – a reviewer could read that silence as a pass. Now it is scope-honest and distinguishes two cases:

Never fabricates coverage that does not exist. This completes the trust-legibility arc with v7.122.0 --explain (what was verified) and v7.123.0 --check-fresh (is it current): now the council is honest about what it could NOT assess. The hard-gate block/no-block decision is unchanged; this only enriches the evidence text reviewers read. New extraction-harness test tests/test-council-scope-honesty.sh (3 cases: spec-present, spec-via-.loki, no-spec).

v7.123.0

Added: loki verify --check-fresh (evidence staleness, fail-closed)

Evidence is only trustworthy while it still describes the current code. New --check-fresh is a consumer-side freshness gate: it does NOT re-run gates, it reads a prior <out>/evidence.json and decides whether that verdict still applies to the live repo. FRESH (exit 0) when the graded head_sha matches HEAD and the worktree is clean (ignoring .loki/, verify’s own output); STALE (exit 2) when HEAD drifted or the tree changed since grading; ERROR (exit 3) when evidence is missing, unparseable, or records no graded head. It is fail-closed by construction: a stored VERIFIED that no longer matches its code never reports FRESH – trusting a cached green after the code moved on is exactly the fake-green the moat forbids. This is for CI and proof consumers that gate on a stored verdict.

evidence.json now records subject.worktree_clean_at_grade (additive), and --explain shows the graded HEAD + tree state and points at --check-fresh for a later re-check. Together with v7.122.0’s --explain, this completes the competitive-research “prove trust to a skeptic in 60 seconds” pair: --explain shows what the evidence is; --check-fresh proves it is current.

Default loki verify behavior and output are unchanged. Six new fail-closed tests in tests/test-verify.sh (fresh / dirty / drift / missing / corrupt / metadata present).

v7.122.1

Fixed: doc-vs-code default for the auto-learnings loop (it is default-ON)

Three in-code comments and the loki internal help text wrongly described the failure-learning loop as “default off, only fires when LOKI_AUTO_LEARNINGS=1”. The runtime is the opposite: both call sites gate on LOKI_AUTO_LEARNINGS !== "0" (quality_gates.ts, internal_phase1.ts), so .loki/state/relevant-learnings.json is written by default and fed into the next iteration’s prompt – the loop that makes Loki avoid repeating past mistakes is ON out of the box. Only the misleading docs were wrong; no behavior change. Corrected learnings_writer.ts, episode_bridge.ts, and the CLI help so operators see the true default (set LOKI_AUTO_LEARNINGS=0 to disable). Clarified that the episodic-memory mirror is a separate, intentionally opt-in enrichment (LOKI_AUTO_LEARNINGS_EPISODE=1) so a missing python3/chromadb dep never disrupts a run. Verified default-on with the exact runtime predicate; learnings + phase1 tests green (11 pass). dist bundle rebuilt.

v7.122.0

Added: loki verify --explain (trust legibility)

New --explain flag on loki verify renders a “Why you can trust this” panel after the run: the exact diff graded (merge-base..head, file/line counts, base ref), evidence freshness (this run’s start/end, gates re-executed now), and a per-gate table (GATE / STATUS / RUNNER / REPRO / EVIDENCE) showing which real runner or scanner produced each row. The panel makes explicit that the verdict is computed ONLY from those independent gate rows – a pass is a real runner exit, never a self-assessment. This is the top recommendation from the ralph-tui / zeroshot competitive analysis: competitors surface a verdict; Loki now shows the receipts behind it.

CLI-invariant and additive: --explain never changes the verdict or exit code. Proven by a byte-identity test (verdict + exit identical to a plain run) plus a render assertion in tests/test-verify.sh. Default loki verify output is unchanged.

v7.121.7

Fixed: Docker image build (Node 24 LTS + native Claude Code installer)

The v7.121.6 Docker publish failed on two independent ecosystem shifts, not a Loki change: (1) npm install -g npm@latest now pulls npm@12, which requires Node >=22.22.2/24.15.0/26 – but the base image was on Node 20 (EOL 2026-03), giving EBADENGINE. Bumped the NodeSource repo to Node 24.x (current Active LTS) in both Dockerfile and Dockerfile.sandbox so latest npm works. (2) @anthropic-ai/ claude-code now ships its runtime via a per-platform OPTIONAL npm dependency + postinstall link, which fails in the hardened build (“claude native binary not installed”). Switched to the documented recommended native installer (curl https://claude.ai/install.sh | bash -s stable), which places the same binary directly. Verified with a full local multi-stage build (Node 24.18.0, Claude Code 2.1.197 installed, image built clean). No runtime/CLI behavior change.

v7.121.6

Fixed: REST API provider set now matches actual support (claude/codex/cline/aider)

The HTTP/SSE REST API was the last surface still exposing the removed gemini provider (runtime deprecated v7.5.18) and omitting the real cline (Tier 2) and aider (Tier 3) providers. The server-side validator actively rejected valid cline/aider build requests and accepted a dead gemini one. Fixed across all six surfaces (server validator, session validation + preference signal, TS types

v7.121.5

Chore: keep non-public scratch out of the CLI repo

Added .gitignore rules so internal/local-only material never ships to users or npm: the Codex provider scratch (.codex/, AGENTS.md), harness scratch (.loki-verify/, .claude/launch.json, tmp/), generated per-build output docs (HANDOFF.md), internal strategy / roadmap / MOAT specs, and build/test/e2e output plus screenshots under artifacts/. Curated artifacts can still be force-added when genuinely meant to ship. No runtime, CLI, or API change.

v7.121.4

Fixed: static_analysis falsely gapped on React/JSX apps (two bugs)

The static_analysis axis read as a gap on real React builds for two independent reasons, both fixed with corpus-first extraction proof (no engine build from the worktree). This closes the static_analysis half of a “Working, with gaps” card; it does NOT green a card whose Tests axis is still not_run (an app with no test command remains an honest gap – unchanged, founder-gated).

Tests: tests/test-static-analysis-languages.sh (+ JSX / oxlint / additive / missing-linter cases), tests/test_proof_generator.py (+ marker->gate-status chain). Both suites green.

v7.121.3

Fix (app-runner: static sites get a live preview + health check)

v7.121.2

Docs (non-functional)

v7.121.1

Trust / accuracy (patch on v7.121.0 – close a tests_pass fake-green)

v7.121.0

Trust / accuracy (#142 – the real #124 non-convergence driver)

v7.120.0

Trust / accuracy (#139)

v7.119.0

Trust / Evidence Receipt honesty (#47)

v7.118.0

Build speed (measured before/after on a real SaaS build)

Trust / Evidence Receipt honesty

Accuracy instruments (Rank 12 / Rank 4)

v7.117.2

Build speed + trust (measured; RED/GREEN gated)

[7.117.1] - 2026-07-02

Fixed

[7.117.0] - 2026-07-02

Trust + adoption batch (zero-test hardening + rank 13; RED/GREEN, council-gated)

Both tests wired into run-all-tests.sh + local-ci.sh. The zero-test verdict-path change was confirmed by a live calibration of the shipped artifact: a real repo with a *.test.js containing zero test() calls recorded runner:node-test, status:inconclusive (“runner ran but executed zero tests”), verdict CONCERNS – no fake-green. Reviewed by a 3-reviewer council; a third item (grill-report wiring) was deferred because it widened the surface of a separate, pre-existing no-HITL hang on spec-internal contradictions.

[7.116.0] - 2026-07-02

Trust-core fixes surfaced by a live calibration build (RED/GREEN, council-gated)

A real bounded engine build (the shipped v7.115 artifact against a 3-item PRD) produced a genuinely-working, fully-tested deliverable, and surfaced two real defects that no fixture caught. Both are fixed here with RED/GREEN tests.

Both tests are wired into run-all-tests.sh + local-ci.sh. No behavior change to any shipping command surface beyond correctly recording a Node test run and writing the marker to the right path.

[7.115.0] - 2026-07-02

Adoption-friction + accuracy batch (ranks 6, 7, 10; RED/GREEN measured)

Driven from docs/research-2026-07/gap-analysis-backlog.json. Built in isolated worktrees, each proven with a fixture RED/GREEN test, wired into run-all-tests.sh AND local-ci.sh. All three are bash/python-only (no loki-ts mirror exists for loki verify or the proof generator), so no Bun-parity surface is touched.

[7.114.0] - 2026-07-02

Accuracy + speed moat batch (4 research-grounded items, RED/GREEN measured)

Driven from docs/research-2026-07/gap-analysis-backlog.json (ranks 2, 8, 9, 15). Each built in an isolated worktree, proven with a fixture RED/GREEN test, parity-locked with the loki-ts runner where a mirror exists, and gated by a full 3-reviewer council. No trust-core gate weakened; the deterministic evidence gate and anti-sycophancy devil’s-advocate remain authoritative.

All four tests are wired into tests/run-all-tests.sh AND scripts/local-ci.sh. No behavior change to any shipping loki command surface.

[7.113.0] - 2026-07-02

Wave-4 bug-hunt fixes (4 real, reproduced, RED/GREEN-tested)

All four independently reproduced and RED/GREEN-verified; council 3-of-3 APPROVE (2 Opus + 1 Sonnet, reviewers ran the tests). No behavior change to any shipping loki command; no trust core / evidence gate touched.

[7.112.0] - 2026-07-02

HLW-5: shareable “Verified by Loki” Evidence Receipt badge (now honest)

[7.111.0] - 2026-07-02

Trust-core fixes (bug-hunt wave 3) + honesty relabel of the proof non-forgeability claim

Wave-3 adversarial bug-hunt found two HIGH trust-core bugs and a CRITICAL honesty defect. Each fix ships with a RED/GREEN regression test.

[7.110.0] - 2026-07-01

Bug-hunt wave 2: dashboard 500-hardening, MCP lsp-proxy HTTP fix, provider flag fixes

An adversarial bug-hunt across dashboard, MCP, memory, CLI, and providers found reproduced bugs; each fix ships with a regression test that fails on old code and passes on the fix. The memory subsystem was audited and found sound (no reproducible bug).

Tests: tests/test-dashboard-json-guards.sh, tests/test-lsp-proxy-http.sh, tests/test-provider-flags.sh (all new, RED/GREEN); existing test-verify (17) and test-provider-loader green.

Deferred (honest): a hardening of the v7.109.0 runtime-gate boot-log port scrape was designed this cycle but a council review found the “probe-first” variant introduced a false-green on a contended default port (a foreign listener on the guessed port could green a broken app). That change was dropped rather than shipped; the v7.109.0 scrape-first behavior remains, and the correct scrape-with-strict-bind-anchor redesign is tracked as a follow-up.

[7.109.0] - 2026-07-01

Bug-hunt-and-fix wave 1: 5 reproduced bugs fixed (false-RED elimination + rate-limit correctness)

An adversarial bug-hunt across the completion council, verify gates, and RARV loop found 5 real, reproduced bugs. All were verified with a concrete repro before fixing, and each fix ships with a regression test. Measurable adoption/UX impact (these were friction, not safety holes - none was a false-VERIFIED / fake-green; the trust core was confirmed sound):

[7.108.0] - 2026-07-01

Accuracy: two additive verification gates (runtime boot smoke + annotate-before-act ledger)

Two independent, fail-open additions to the verification layer. Both are strictly additive: on the common path (no app to boot, no expectation ledger present) verify output is byte-identical to before, so neither can regress an existing run.

Runtime boot smoke gate (loki verify) – “it actually runs”:

Annotate-before-act expectation ledger (loki verify):

[7.107.0] - 2026-07-01

Hardening + latent crash fix: loki mcp –transport http (loopback bind + optional bearer auth)

The HTTP transport path for loki mcp --transport http was fixed and hardened.

[7.106.0] - 2026-07-01

Accuracy: reverse-classical test provenance (a green test suite must actually prove the change)

The strongest completion signal is “tests pass” – but a test written to pass proves nothing. Adopting the FrontierCode insight (grade like a tech lead, not a CI): a NEW test only counts as affirmative evidence if it FAILS on the pre-change base and PASSES on HEAD. Tests that pass on both are tautological.

[7.105.0] - 2026-07-01

Faster convergence: the completion council evaluates the moment work is claimed done

Measured from real build telemetry (docs/SPEED-DIAGNOSIS-2026-07-01.md): a build could be verifiably done at iteration 1 (code reviews non-blocking, tests green, checklist complete) yet keep running needless “next improvement” iterations, because the completion council was only allowed to evaluate every COUNCIL_CHECK_INTERVAL (default 5) iterations. One observed build ran 14 iterations for a job finished in ~1, growing from 19 to 26 tests it never needed.

Honest scope note: this fixes convergence CORRECTNESS (the loop stops at verified-done), proven by unit test. End-to-end wall-clock before/after on a fixed spec is tracked separately (benchmarks/speed-benchmark.sh) and depends on a surviving long-build run; the fat-iteration cost (a couple of disproportionately long iterations) is a distinct, still-open investigation.

[7.104.5] - 2026-07-01

Fix: memory panel is correct and self-consistent on JSON-backed projects

Builds on the fix contributed in PR #178 by @iizotov (memory summary reporting 0 on a JSON-backed project) and closes the two gaps its review surfaced, plus the same class of bug in the sibling endpoints.

[7.104.4] - 2026-07-01

Fix: /api/tasks no longer 500s on a malformed dashboard-state.json

[7.104.3] - 2026-07-01

Task list accuracy: honest, stable done column (no empty cards, no duplicates)

[7.104.2] - 2026-07-01

Fix: native/Keychain Claude login no longer misread as “not logged in”

[7.104.1] - 2026-07-01

Bun doctor checks Claude login (bash/Bun parity)

[7.104.0] - 2026-06-30

Default execution model is now Claude Sonnet 5

[7.103.0] - 2026-06-30

Bun-runner gate parity + verify.sh hardening

[7.102.0] - 2026-06-30

Trust: unknown quality-gate status can no longer read green

[7.101.0] - 2026-06-30

CRITICAL trust fix + adoption empty-state + cleanup

[7.100.1] - 2026-06-30

Zero-friction: surface the no-key path at every first touch

[7.100.0] - 2026-06-30

Zero-friction adoption: offline taste path + shareable receipt badge

[7.99.1] - 2026-06-30

[7.99.0] - 2026-06-30

Memory integrity + checklist/cost trust fixes + dashboard reuse (wave-7 hunt)

[7.98.0] - 2026-06-30

Council trust-config fixes + zero-friction adoption

[7.97.0] - 2026-06-29

Wave-5 hardening: memory, events, healing, provider parity + FIX A rework

[7.96.0] - 2026-06-29

Trust moat: council force-review path fails closed

Trust moat: completion gates now fail CLOSED

[7.94.0] - 2026-06-29

Reuse done-recognition: a no-PRD reuse run no longer rebuilds finished work

[7.93.0] - 2026-06-29

Dashboard fixes: sidebar rebalance, honest App Runner status, stable version

[7.92.0] - 2026-06-27

Zero-friction adoption: fail-open preflight, honest failure diagnosis, CLI polish

First-run and error-diagnosis improvements so a new user gets unblocked fast and a failed build explains itself, with every check fail-open and honest.

Added (first-run preflight, advisory):

Added (failure diagnosis):

Added / changed (CLI + doctor):

[7.91.1] - 2026-06-27

Fix: green the CI shell suite (caveman moat-audit false positive)

The v7.91.0 fail-fast auth preflight added an error-message line that prints claude login as a fix hint. The caveman moat-audit (test-caveman-flags.sh) does a tree-wide scan for unsuppressed claude subcalls and matched that literal string, failing the CI “Shell tests” job. Whitelisted claude login in the audit (it is a user-instruction string, not a spawned subcall - same class as the already-whitelisted claude auth status / command -v claude). No runtime behavior change; all other v7.91.0 features intact.

[7.91.0] - 2026-06-26

Zero-friction adoption: per-build workspace + fail-fast auth + honest phase state

[7.90.2] - 2026-06-20

Cleanup: remove dead keyboard-shortcut and theme-toggle markup

Follow-up to v7.90.1. The keyboard-shortcut help overlay markup + CSS and the theme-toggle CSS rule were left behind when those features were removed; they were never shown and never referenced. Deleted them so the dashboard carries no dead shortcut/theme code. No user-visible change.

[7.90.1] - 2026-06-20

Dashboard polish: a calmer, more readable live view

Fixes from watching the live dashboard during a real build, all aimed at a cleaner experience for whoever is watching:

[7.90.0] - 2026-06-20

Proven PR: every PR Loki opens carries its own re-verifiable proof

When Loki opens a pull request, the PR body now includes an Evidence Receipt summary so a reviewer does not have to take the agent on faith. The block shows the honest verdict (VERIFIED / VERIFIED WITH GAPS / NOT VERIFIED), the key facts (diff hash, tests, secure-gate result, cost), and a “verify this yourself” line: loki proof verify <id> against the recorded base SHA. The receipt is rendered by a single shared helper so every PR path Loki uses carries it consistently.

The dashboard proofs panel now links each proof to its PR and headline.

[7.89.1] - 2026-06-20

Plainer language in the live dashboard

A copy pass so a non-technical user watching a build sees plain language instead of internal jargon. Empty and loading states across the activity, cost, memory, provider-health, quality-gates, timeline, and escalations panels now describe what is happening in product terms (e.g. “Each step fills in as your build progresses” instead of “RARV phases will populate”). No behavior change.

[7.89.0] - 2026-06-20

Live dashboard + wiki: a polished experience while you watch a build

A pass over what a user actually sees while Loki works, found by watching real builds:

Telemetry: on by default for individuals, off for enterprise/CI/air-gapped

Anonymous diagnostics now default ON for an ordinary individual install (to find and fix bugs), and AUTO-OFF in enterprise, CI, air-gapped, and non-interactive contexts, so those deployments stay silent out of the box with no action required. Only anonymous diagnostics are ever sent (os, arch, version, error type, sanitized stack signatures) – never code, prompts, paths, keys, or repo names. Disclosure is shown once (welcome screen + a one-line notice before the first send) and documented in docs/PRIVACY.md, so it is never covert. Opt out anytime with loki telemetry off / DO_NOT_TRACK=1 (always wins); force on with loki telemetry on. The verbose per-run disclosure block was removed.

Fixes found by a real end-to-end run

A real loki start build (haiku, not fixtures) surfaced a batch of issues across the trust + first-run surfaces. All are fixed here:

Telemetry: on by default for individuals, off for enterprise/CI/air-gapped

Anonymous diagnostics now default ON for an ordinary individual install (to actually find and fix bugs), and AUTO-OFF in enterprise, CI, and air-gapped or non-interactive contexts, so those deployments stay silent out of the box with no action required. Only anonymous diagnostics are ever sent (os, arch, version, error type, sanitized stack signatures) – never code, prompts, paths, keys, or repo names. Disclosure is shown once on the welcome screen (never covert) and documented in docs/PRIVACY.md. Opt out anytime with loki telemetry off / DO_NOT_TRACK=1 (always wins); force on with loki telemetry on. The verbose per-run disclosure block was removed to keep logs clean.

[7.88.0] - 2026-06-20

Finish and own: a plain-English handoff for whoever owns the result

Loki has always produced a developer-oriented handoff. This adds a plain-English ownership handoff for a non-technical owner – a founder or PM who needs to understand what was built and how to run it without reading the code.

About 13 tests lock the honesty guarantees (a not-verified build can never render a green “it works” claim; the doc introduces no feature beyond your brief).

[7.87.0] - 2026-06-20

Secure-by-default gate (advisory by default)

A new gate scans the built code for a small set of high-precision, high-severity security mistakes – the ones that sink AI-built apps – and records the result in the Evidence Receipt. It is advisory by default (it never blocks an existing build); set LOKI_SECURE_GATE=block to make un-waived HIGH findings stop completion.

Honest scope: the gate proves specific known-bad patterns are ABSENT; it does not claim the app is “secure”. Deeper checks (auth/route analysis) are roadmap. About 24 new tests lock the bad/safe precision matrix and the receipt behavior.

[7.86.0] - 2026-06-20

The trust story, made visible (honestly)

The Evidence Receipt shipped in v7.85.0; this release surfaces it so a reader can see the trust differentiator without digging, and re-verify any of it themselves.

No new product capability beyond v7.85.0 – this makes the existing one legible.

[7.85.0] - 2026-06-20

Evidence Receipt: verify it yourself, Loki never lies about done

Loki already generated a proof-of-run at the end of every build. This release hardens it into a non-forgeable Evidence Receipt: the deterministic facts are separated from AI judgments, gaps are surfaced honestly, and you can re-verify the receipt against your repo yourself.

Both routes share one generator (no drift). About 24 new tests lock the honesty guarantees, including that an untampered receipt verifies clean and a post-build commit is detected as drift.

[7.84.0] - 2026-06-20

Enterprise-grade dashboard overhaul

A ground-up restructure + polish of the dashboard so it looks and feels like a tool teams already trust. The previous layout had two redundant project dropdowns, several blank views (Quality, Analytics, Trust, Wiki), an over-long unpruned project list, a leftover developer API-URL control, and a task board that lost its column headers on scroll. All fixed, plus a full information-architecture pass.

Verified with a Playwright screenshot pass across every view in light and dark. A standalone, offline-browseable build ships in artifacts/dashboard-preview/.

[7.83.1] - 2026-06-20

Fix: loki ship now reviews the work being shipped

A post-release adversarial hunt found that loki ship (new in v7.83.0) ran its quality gate on the uncommitted diff, but a Loki build auto-commits the session’s work to the loki/session-* branch – so by ship time the working tree is clean, the review saw an empty diff, and ship reported “clean” and advised a PR without gating the actual branch work. ship was always print-only (it never pushed), so nothing unsafe shipped, but the gate it advertises was a no-op exactly when it mattered. Fix: on a clean loki/* branch with no explicit scope, ship now reviews the branch-vs-base range (resolved from base-branch.txt), so the shippable work is gated. Explicit scope flags are still respected; ship stays print-only.

[7.83.0] - 2026-06-20

UX: less friction, more wow

Six experience improvements that cut the number of commands and flags you need to learn, make output worth screenshotting, and tell you the next step everywhere. All additive and opt-out; default behavior is unchanged.

Full suite green (1422 pytest + bun + shell suites); completion-coverage drift check passing.

[7.82.0] - 2026-06-20

Hardening sweep: 36 verified bug fixes, real-cluster-proven deployment, UX polish

A large correctness + trust release. Every bug below was found by an adversarial hunt and independently confirmed by a second reviewer before any fix (about half the raw findings were refuted and dropped, so what shipped is real). Two of the deployment fixes were caught only by running the published image on a real Kubernetes cluster. Council 3/3 after a round that caught (and this release corrects) an auth-hardening over-reach.

Trust and security

Deployment (found by running the real image on Kubernetes)

Correctness (selected from the 36)

Experience

Removed

About 14 new regression tests cover the above; full suite green (1422 pytest + 67 shell suites), helm lint clean across all worker modes.

[7.81.1] - 2026-06-19

Fix: CI Shell-tests green (V2 python test runner-contract)

[7.81.0] - 2026-06-19

Gap-closure: honest-gaps and known issues from the two-release arc, closed

Every item is opt-in; local-first and default behavior are unchanged. Council 3/3 after an adversarial bug-hunt that found (and this release fixes) a queue-item flag injection and a retry double-launch race.

Security and hardening (from the bug-hunt)

Honesty

local-ci green; full test suite (1408 pytest + bash suites) green; helm lint clean across all worker modes.

[7.80.1] - 2026-06-19

Post-release hardening (UX + test integrity)

[7.80.0] - 2026-06-19

Fleet + retrieval + scale: the second enterprise foundation release

Builds on v7.79.0. Everything new is opt-in; local-first and default behavior are unchanged. Council 3/3 after an adversarial bug-hunt that caught and closed real defects (a Helm autoscaler collision and a latent recursion DoS) before ship.

Features

Fixes

[7.79.0] - 2026-06-19

Enterprise + local foundation: one config knob, pluggable storage, hardened webhooks, real auth gating

The first of two enterprise-hardening releases. Everything here is local-first by default (zero new dependencies, behavior byte-identical when you set nothing); enterprise backends and controls are strictly opt-in. Council 3/3 after a two-round adversarial review that caught and closed real defects before ship.

Features

Security and hardening

Honest scope (NOT overclaimed)

Every claim above maps to enforced, tested behavior: full local test suite green (1376 pytest + the new bash suites), local-ci 85/0, bash/Bun parity intact.

[7.78.0] - 2026-06-19

Dead-code removal + ALLOWED_PATHS honesty

Cleanup release. No behavior change for any live path.

local-ci 85/0. Full bash + memory + dashboard + loki-ts test sweep green.

[7.77.0] - 2026-06-19

WAVE13-TAIL hardening: council timeout, /lab auth, memory + durability

Final wave-13 bug-fix batch. Council 3/3.

local-ci 85/0. Memory 28 + 9 tests, loki-ts 1098 tests, council quorum 7/7 all green; dashboard auth behavior verified (off = pass-through, on = 401/403/200).

[7.76.0] - 2026-06-19

Memory data-integrity fixes + dashboard UX

Two HIGH memory data-integrity fixes and three dashboard usability improvements. Council 3/3. Memory: 28 + 9 existing tests pass plus adversarial concurrency probes (200-800 threads, zero lost updates).

Memory:

Dashboard:

[7.75.0] - 2026-06-19

Enterprise container deployment: pod-loss-safe run-to-completion

Loki Mode now deploys as a pod-loss-safe run-to-completion build in Kubernetes (and the same model maps to ECS run-task and docker-run + systemd). A single build is a k8s Job, not a long-lived Deployment, leaning on the platform’s native restart primitives instead of a custom retry loop. The architecture was decided by a 3-reviewer council against the real code after the first proposal was rejected for overclaiming resilience the code did not have. All new runtime behavior is OPT-IN via LOKI_DURABLE_STATE=1 (set automatically by the Helm chart). Local and CI runs are byte-for-byte unaffected (verified by the council and the test suite).

Honest scope (NOT claimed): the worker audit is plain JSONL on the durable volume (survives pod loss); it is not tamper-evident and not shipped to a SIEM (the hash-chained audit is the control-plane chain; full SIEM ingestion is roadmap). The /workspace volume is empty on a fresh install (the chart does not clone the repo or place the PRD; the operator pre-seeds it or passes an issue ref). A Job’s template is immutable, so changing image/spec/resources means a new release, not an in-place upgrade. One build per release (one Job + one RWO volume); concurrent builds need separate releases. SSO/SAML, SCIM, app-level RBAC, and SOC2 remain roadmap, not present. The object-store checkpoint sync, the queue-consumer and serverless fleet modes, and the unified --config file loader are deferred to later releases.

[7.74.0] - 2026-06-18

Trust-gate hardening (completion + override + registry)

A deep-hunt wave found and fixed trust-surface defects on the live loki start path.

[7.73.0] - 2026-06-18

Feature-branch by default + advisory PR (enterprise-ready git flow)

loki start now works out of a feature branch by default. It branches off the branch you ran it from (no longer hardcoded to main), commits the work at session end, and PRINTS the exact git push + gh pr create commands for YOU to open the pull request. This is the enterprise path: for teams whose existing CI/CD pipeline deploys on push/merge, “git commit + open a PR” IS the deploy, and Loki now leaves you on a committed branch ready to PR.

loki deploy: advisory, print-only deploy guidance (CI/CD-aware)

New loki deploy detects your project type and your installed cloud CLI, and PRINTS the exact deploy command for you to run. It NEVER deploys, NEVER runs a cloud CLI (not even --version), and NEVER runs git push – Loki does not access your cloud account. When a CI/CD pipeline config is detected (GitHub Actions, GitLab CI, Jenkins, CircleCI, Azure Pipelines, Bitbucket), the primary advice is the git push + pull-request path, because your pipeline deploys on merge; cloud-CLI options (vercel/netlify/fly/wrangler) are shown as secondary. Best-effort clipboard copy of the idiomatic command.

[7.72.0] - 2026-06-18

loki preview --public creates a PUBLIC URL for the app Loki built and is running locally, by wrapping YOUR OWN tunnel client (cloudflared or ngrok), so you can send a teammate or client a link to the live build. Loki never proxies the traffic and never bundles or downloads a tunnel binary – your tunnel account is what egresses, by your explicit choice, preserving the “your keys, nothing leaves your network” model.

loki preview --public                 # auto-detect cloudflared, then ngrok
loki preview --public --provider ngrok
loki preview --public --yes           # skip the consent prompt (warning still shown)

[7.71.0] - 2026-06-18

Live in-terminal build HUD

loki start ./prd.md now shows a live per-iteration status line in an interactive terminal, so the build is no longer silent between the start banner and the end summary. Each iteration prints one clean line:

[HUD] REASON | iter 3/1000 | $0.42 | +180/-24 (4 files) | took 37s | elapsed 2m11s

[7.70.0] - 2026-06-18

Deep bug-hunt sweep (wave-11): cross-route trust guards + fail-closed healing gate + 6 more

A 12-agent file-sharded verify-then-fix hunt covering the deferred cross-file follow-ups from wave-10 plus fresh surfaces. Every finding reproduced before fixing; STALE findings refuted with evidence (the rarv/autonomous/state Bun runner was hunted and found parity-correct, no changes). Council 3/3; local-ci 84/84 (now including a dist-freshness gate); bun 1093, python 1337.

Trust surfaces:

Provider cost ceiling:

PRD reuse:

Memory:

Honesty:

CI

[7.69.0] - 2026-06-18

Deep bug-hunt sweep (wave-10): 9 fixes across council, app-runner, MCP, dashboard, memory, Bun runner, CLI

A 12-agent file-sharded verify-then-fix hunt over previously deep-un-hunted surfaces. Every finding was reproduced before fixing; STALE findings were refuted with evidence. Reviewed by a 3-reviewer council (2 Opus + 1 Sonnet) to unanimous approve. local-ci 84/84; bun 1087, python 1336.

Trust surfaces (the headline fixes):

Memory subsystem:

Server surfaces:

App runner + CLI:

Refuted (verified STALE, no change): app-runner BSD grep \s portability and the image-tag edge case, the completion-council no-provider heuristic-APPROVE mode (supported degraded mode), and several memory/Bun candidates. Deferred as separate scoped follow-ups (cross-file): codex LOKI_MAX_TIER case-normalization parity, the quality_gates.ts stub-judge artifact check, and read-scope on GET dashboard routes in enterprise mode.

[7.68.0] - 2026-06-18

Queued bug-hunt tail (wave-9) + CI test-isolation repair

A file-sharded verify-then-fix fleet cleared the WAVE8 queued MED/LOW findings. Each finding was reproduced before fixing; STALE findings were refuted with evidence rather than “fixed”. Reviewed by a 3-reviewer council (2 Opus + 1 Sonnet) to unanimous approve. local-ci 84/84 on a quiet box.

CI / test-isolation:

Memory subsystem:

Dashboard / migration:

CLI / runtime (bash):

Refuted (verified STALE, no change): a PERPETUAL_MODE gate in run.sh (compounding it would disable the completion council on every default run) and the crash.ts global unhandledRejection handler (intentional, documented fail-fast).

[7.67.0] - 2026-06-17

Trust-surface + data-integrity hardening (wave-8 bug-hunt)

A 12-agent read-only hunt across previously un-hunted surfaces, then a fix fleet. The headline is a CRITICAL false-pass in the LIVE bash code-review gate. Reviewed by a 3-reviewer council (2 Opus + 1 Sonnet) to unanimous approve (a council CONCERN caught a regression in the first fix and was resolved before ship). Full Python suite: 1261 passed, 7 skipped; bun: 1080 passed.

Code-review gate (live bash route, the CRITICAL fix):

App runner (live):

CLI:

Memory data-integrity:

[7.66.1] - 2026-06-17

Memory file-lock concurrency fix + CI test deflake

Hotfix for two CI failures surfaced by the v7.66.0 test matrix (the shipped v7.66.0 runtime is unaffected; both issues are in the memory locking primitive and a test harness, not in v7.66.0’s completion-council changes).

[7.66.0] - 2026-06-17

Completion-council trust-surface hardening (wave-7 bug-hunt)

Hardening pass on the completion-detection trust surface (autonomy/completion-council.sh, autonomy/run.sh) plus forward-looking fixes on the dormant Bun-route quality gates (loki-ts/src/runner/quality_gates.ts). Reviewed by a 3-reviewer council (2 Opus + 1 Sonnet) to unanimous approve, with a non-vacuous behavioral test (before/after proof).

Live trust fix (the real HIGH):

Reliability fixes (live bash route):

Forward-looking fixes (Bun route, currently dormant / tree-shaken until loki start routes through runAutonomous):

[7.65.0] - 2026-06-17

Memory subsystem hardening (wave-6 bug-hunt)

A dedicated hardening pass on the memory subsystem (storage.py, engine.py, retrieval.py, consolidation.py, token_economics.py). The headline is a real data-integrity fix; the rest is defensive robustness against corrupt or hand-edited records. Reviewed by a 3-reviewer council (2 Opus + 1 Sonnet) to unanimous approve. Full Python test suite: 1248 passed, 11 skipped.

Data-integrity (the real HIGH):

Defensive robustness (corrupt / hand-edited records; no live writer emits these values, schema validation enforces ranges):

Correctness:

Test infrastructure:

[7.64.0] - 2026-06-17

Wave-5 bug-hunt fixes (server.py, mcp, app-runner) + coverage gate repair

Eleven bugs found by a read-only adversarial sweep of previously un-hunted live surfaces, each re-verified against source and fixed with a non-vacuous regression test (proven to fail pre-fix). Plus the every-release Coverage check, red since it gained a threshold, root-caused and fixed.

Gate green: pytest 1196 passed, bun typecheck clean, bun –coverage 1067 pass/0 fail.

[7.63.1] - 2026-06-17

Hotfix: doctor install-hint bash/Bun parity (GH Bun Parity went red on v7.63.0)

v7.63.0 added the per-provider doctor install hint on the Bun route’s STDOUT to match bash. That matched the local-ci parity harness (which captures 2>&1) but broke the canonical GH bun-parity gate, which captures STDOUT only (>out 2>/dev/null): the bash route emits the hint to STDERR, so on a runner with all providers absent the Bun route showed four extra stdout lines and the gate went red. Fix: emit the hint on STDERR (matching bash run.sh >&2), so it is invisible to the stdout-only gate and aligned under 2>&1. Added a regression test that captures stdout only (mirroring the gate) and asserts the Bun route emits no per-provider Install line on stdout. Verified parity under BOTH capture modes (stdout-only and 2>&1).

[7.63.0] - 2026-06-17

PRD-reuse on rerun + Docker UX (features) plus wave-4 crash/data fixes

Two coherent batches, each developed in parallel with non-vacuous regression tests and each cleared by an independent 3-of-3 reviewer council. Full local gate green: pytest 1181 passed, bun typecheck clean, bun 1064 pass (1 known non-hermetic flake that passes in isolation), all bash syntax OK.

Features

Wave-4 crash and data-integrity fixes (read-only hunt on previously un-hunted live surfaces)

[7.62.0] - 2026-06-17

Parallel-worktree cleanup batch (9 isolated fixes, file-disjoint)

Nine fixes developed in parallel isolated git worktrees, each with a non-vacuous regression test, merged conflict-free (zero file overlap). Full pytest gate: 1176 passed; all 6 standalone tests pass.

CI hardening:

Reliability:

Memory:

Audit + docs:

[7.61.0] - 2026-06-17

Verification-gate honesty + correctness (6 MEDIUM findings)

Lead theme: gates that silently pass when they should fail (the on-brand-critical class).

Each fix ships with a non-vacuous regression test. Full pytest gate: 1176 passed.

[7.60.0] - 2026-06-17

Security hardening: 5 HIGH findings from the adversarial sweep

Each fix ships with a non-vacuous regression test. Full pytest gate: 1172 passed.

[7.59.2] - 2026-06-17

Fix: release gate broken by test-isolation pollution (v7.59.0/.1 stranded)

[7.59.1] - 2026-06-17

Fix: v7.59.0 release gate broke on numpy-less CI env

[7.59.0] - 2026-06-17

Reliability hardening wave (20-agent fleet: bug-hunt + fixes across all subsystems)

Real bugs found and fixed, each with a non-vacuous regression test:

New features: App Runner now detects Next.js standalone + reverse-proxy URLs and Spring Boot multi-port stacks (autonomy/app-runner.sh, dashboard/server.py); PID liveness uses process start-time identity to avoid PID-reuse false positives (dashboard/server.py); run-start cost estimate + budget-cap disclosure on the non-TTY path (autonomy/run.sh). Docs accuracy + license-honesty corrections.

[7.58.1] - 2026-06-17

Fix: doctor bash-route crash off a TTY (Bun Parity green)

[7.58.0] - 2026-06-17

Reliability + data-durability + repo hygiene wave (12 fixes)

This release is a quality/reliability pass: dashboard concurrency hardening, memory data-integrity fixes, durable SWE-bench instance persistence, doctor UX, healing-hook safety, and test-suite hygiene (no more leaked branches/temp dirs). No user-facing behavior changes; everything is additive or a bug fix.

Memory data integrity

SWE-bench instance durability

Dashboard concurrency hardening + heredoc CI guard (reliability)

Doctor UX

Healing-hook safety + test hygiene

[7.57.0] - 2026-06-17

App Runner shows running apps + URLs, intelligent tasks, default-on advisory gates

Gates: local-ci 83/83, bash/Bun parity 6/6, 97 Bun gate tests + full pytest green, 3-reviewer council unanimous APPROVE (after rounds that correctly caught a built-vs-source divergence and stale docs, both fixed).

[7.56.0] - 2026-06-17

Reachable audit helpers + invariant-findings prompt surfacing

Gates: local-ci 83/83, bun tests + parity green, 2-reviewer council APPROVE (both verified the CLI commands work empirically and the prompt injection reaches the rendered prompt).

[7.55.0] - 2026-06-16

Verification depth: invariant gate + anti-sycophancy parity + manifest tamper-evidence

Gates: local-ci 83/83, bash/Bun parity 6/6 (LOKI_GATE_INVARIANTS now a genuine shared toggle, no allow-list exception), 595 Bun tests + 75 audit tests + full pytest green, 3-reviewer council unanimous APPROVE.

[7.54.0] - 2026-06-16

Tech-debt elimination + provider honesty + regression hardening

Gates: local-ci 83/83, full pytest + bun test (1009 Bun tests) + bash/Bun parity green, 3-reviewer council unanimous APPROVE (after a round that correctly caught a self-contradicting code comment, now fixed).

[7.53.0] - 2026-06-16

Verification depth + enterprise compliance surface (P1-2, P1-3, P3-6, P3-11)

Gates: local-ci 78/78, full pytest + bun test (78/78) + bash/Bun parity (11 shared toggles, 0 fail) green, held-out evals 34/34, 3-reviewer council unanimous APPROVE.

[7.52.0] - 2026-06-16

Codex CLI compatibility: –full-auto deprecation swept repo-wide + budget quoting hardening

Gates: local-ci 78/78, full pytest + bun test + bash/Bun parity green, 3-reviewer council unanimous APPROVE (2 Opus adversarial + 1 Sonnet) after a first round that correctly rejected on a test-assertion regression (now fixed).

[7.51.0] - 2026-06-16

Tech-debt closure: four inert/half-wired features made real (both routes)

This release removes scaffolding that read like a feature but did nothing, honoring the no-tech-debt mandate. Each activation is advisory-first and deny-filters clean results, so an activated gate can never false-fire and deadlock the autonomous loop. Defaults are unchanged for existing users.

Gates: local-ci 78/78, full pytest + bun test + bash/Bun parity matrix green, 3-reviewer council unanimous APPROVE (2 Opus adversarial + 1 Sonnet).

[7.50.0] - 2026-06-16

Spec-robustness, enterprise depth, and verification breadth (10-stream batch)

All default-on or zero-config-intelligent; opt-out knobs only. Each stream built in an isolated worktree, council-reviewed.

[7.49.0] - 2026-06-16

Verification depth + enterprise isolation + parity guard

Each stream built in an isolated worktree, council-reviewed, local-ci green.

[7.48.0] - 2026-06-16

Fix: shellcheck SC2120 on spec-interrogation.sh (v7.47.0 workflow red)

Enterprise: telemetry is now opt-in (P3-2)

Verification depth: semantic test-authenticity detector (P1-3)

Verification depth: real coverage measurement + run manifest (P0-1 Fix A, P3-5)

Each stream built in an isolated worktree, council-reviewed, local-ci green.

[7.47.0] - 2026-06-16

Security: OIDC RBAC bypass fixed (P3-1)

Spec-robustness: interrogation gate + assumption ledger (P2-1, P2-2)

Honesty: license claims corrected (source-available, not open source)

Reviewed by a unanimous completion council. Each stream was built in an isolated worktree and verified independently (OIDC 17/17, spec-robustness 22/22, P0 gates non-regressing, local-ci green).

[7.46.0] - 2026-06-16

Verification-credibility sweep (the trust layer is now honest and real)

This release makes Loki’s verification layer match its claims. An internal audit found gates that were advertised but hollow, unwired, or described inaccurately. The fix: make every gate real or stop claiming it. The deterministic gate set is now an honest 8.

Reviewed by a 3-of-3 unanimous completion council. No runtime behavior changes beyond the gates now actually blocking on the conditions documented.

[7.45.1] - 2026-06-15

Fixed

Changed

Notes

[7.45.0] - 2026-06-15

Docker is now a first-class way to run Loki. The published image could not run a single build (it shipped the runtime but no AI provider CLI), so this release makes the image buildable, adds a zero-friction loki docker host wrapper, and a docker compose .env workflow, plus a security fix for a tracked empty .env.

Fixed

Added

Changed

[7.44.0] - 2026-06-15

Web-surface consolidation + local-ci parallelization. One local web surface (the dashboard), Purple Lab deprecated with a value-preserving migration path, and the dashboard gains browser PRD-input so nobody loses the ability to start a build from a browser.

Changed

Added

Docs

[7.43.0] - 2026-06-14

Bug-hunt + greenfield-E2E hardening wave. A real first-time-user greenfield fullstack E2E run (the first since v7.41.1) exposed three verified-completion trust holes; a parallel adversarial bug-fleet found data-loss and concurrency defects across the memory, event, MCP, app-runner, and provider subsystems. All fixes are additive and reversible; the build_prompt bash<->Bun parity lock is unchanged (60/60 golden fixtures byte-identical).

Fixed

Fixed (greenfield-E2E trust holes)

Tests

[7.42.0] - 2026-06-14

Ten-team parallel hardening + documentation completeness wave (worktree-isolated, file-sharded, merged after independent review). Closes the remaining deferred bug-hunt findings and documents the v7.41.x default-on knobs across all surfaces.

Fixed

Added

[7.41.5] - 2026-06-14

Defensive hardening from the bug-hunt fleet’s lower-severity findings. Each fix has a regression test proven non-vacuous.

Fixed

[7.41.4] - 2026-06-14

Bun-route parity hardening. The autonomous live route is bash today, so these are dormant in production, but the Bun route must agree with bash byte-for-byte before the Phase 6 bash sunset wires it in, and the cross-route parity matrix is an enforced gate. All fixes mirror the bash source of truth exactly.

Fixed

Notes

[7.41.3] - 2026-06-14

Accuracy + autonomy hardening from a 10-agent adversarial bug-hunt across the trust-gate, app-runner, dashboard, healing, and memory surfaces. Every fix has a concrete trigger and a regression test; latent / dormant-Bun-route / dead-code findings were deferred (tracked in internal/v7413-bughunt-findings.md).

Fixed

[7.41.2] - 2026-06-14

CI integrity patch. The v7.41.1 release shipped to npm but its Tests workflow went red on two test-only defects (the runtime code was unaffected). This patch makes the suite green again and adds a permanent guard for the class of regression that v7.41.1 introduced.

Fixed

Added

[7.41.1] - 2026-06-14

Accuracy + autonomy hardening. A real first-time-user E2E (published CLI, haiku, greenfield + brownfield fullstack builds) confirmed both flows produce working, tested apps, but surfaced verification-machinery gaps that are now closed. Every fix makes Loki more accurate and more autonomous; all are intelligent defaults with opt-out env knobs, never a required user decision.

Fixed

Changed

[7.41.0] - 2026-06-14

Added

[7.40.0] - 2026-06-14

Added

Fixed

Docs

[7.39.1] - 2026-06-14

Fixed

[7.39.0] - 2026-06-14

Added

Changed

[7.38.0] - 2026-06-14

Added

[7.37.1] - 2026-06-14

Fixed

[7.37.0] - 2026-06-13

Added

[7.36.0] - 2026-06-13

Added

Fixed

[7.35.0] - 2026-06-12

Added

Fixed

[7.34.1] - 2026-06-11

Fixed

[7.34.0] - 2026-06-11

Added

Fixed

[7.33.0] - 2026-06-11

Added

[7.32.3] - 2026-06-10

Fixed

[7.32.2] - 2026-06-10

Fixed

[7.32.1] - 2026-06-10

Fixed

[7.32.0] - 2026-06-10

Added

Fixed

Changed

[7.31.0] - 2026-06-10

Added

Fixed

Tests

[7.30.0] - 2026-06-10

Added

Fixed

Tests

[7.29.0] - 2026-06-10

Added (the quickstart trio, from UX-QUICKSTART-DESIGN)

Fixed

Tests

[7.28.2] - 2026-06-10

Changed (first-run UX, from the 2026-06 user-shoes friction audit)

Tests

[7.28.1] - 2026-06-10

Fixed

Tests

[7.28.0] - 2026-06-10

Added

Changed

Fixed

Tests

[7.27.0] - 2026-06-09

Added

Changed

[7.26.0] - 2026-06-09

Added

Notes

[7.25.0] - 2026-06-09

Added

Notes

[7.24.0] - 2026-06-09

Added

Notes

[7.23.1] - 2026-06-09

Fixed

[7.23.0] - 2026-06-09

Added

Fixed

[7.22.0] - 2026-06-09

Added

[7.21.0] - 2026-06-08

Added

Fixed

[7.20.0] - 2026-06-08

Changed

Fixed

Notes

[7.19.4] - 2026-06-07

Fixed

[7.19.3] - 2026-06-07

Added

NOT tested in this release (honest disclosure)

[7.19.2] - 2026-06-07

Added

NOT tested in this release (honest disclosure)

[7.19.1] - 2026-06-07

Added

Fixed

NOT tested in this release (honest disclosure)

[7.19.0] - 2026-06-07

Added

NOT tested in this release (honest disclosure)

[7.18.3] - 2026-06-06

Changed

Fixed

[7.18.2] - 2026-06-06

Added

Changed

NOT tested in this release (honest disclosure)

[7.18.1] - 2026-06-04

Fixed

[7.18.0] - 2026-06-03

Fixed

Internal

[7.17.1] - 2026-06-03

Fixed

Internal

[7.17.0] - 2026-05-30

Added

Security

Notes

[7.16.1] - 2026-05-30

Fixed

[7.16.0] - 2026-05-30

Added

Notes

[7.15.0] - 2026-05-30

Added

Security

Notes

[7.14.0] - 2026-05-30

Added

Fixed

Notes

[7.13.0] - 2026-05-30

Added

Notes

[7.12.0] - 2026-05-30

Added

Fixed

Notes

[7.11.0] - 2026-05-30

Added

Notes

[7.10.1] - 2026-05-30

Fixed

[7.10.0] - 2026-05-30

Added

Notes

[7.9.1] - 2026-05-30

Fixed

[7.9.0] - 2026-05-30

Added

Notes

[7.8.3] - 2026-05-30

Fixed

[7.8.2] - 2026-05-29

Added

Notes

[7.8.1] - 2026-05-29

PATCH release. Makes loki start with no PRD smarter: it reuses the previously generated PRD instead of regenerating it every run, and the first-run analysis is sharper.

Changed

Added

[7.8.0] - 2026-05-29

MINOR release. Two additive Claude Code feature adoptions. Both are gated on the installed CLI supporting the flag and fall back to the exact current behavior, so nothing changes for users on an older Claude Code.

A prior audit of Claude Code 2.1.157 vs Loki’s usage confirmed NOTHING is broken or missing-critical: all flags Loki passes are active with unchanged semantics and the stream-json output schema (which Loki’s parser consumes) is intact. These two adoptions are pure improvements.

Added

Deferred

[7.7.34] - 2026-05-29

PATCH release. Stop now actually stops the autonomous AGENT, not just the orchestrator. This is the real fix for the recurring “dashboard says STOPPED but the terminal keeps running and editing files” report.

Fixed

Safety / scope

[7.7.33] - 2026-05-29

PATCH release. Makes the dashboard Stop button actually stop the session.

Fixed

[7.7.32] - 2026-05-29

PATCH release. Fixes the dashboard task-detail modal showing only the title (no description, acceptance criteria, or logs).

Fixed

[7.7.31] - 2026-05-29

PATCH release. Makes the dashboard Stop button (and loki stop) take effect within ~1s instead of up to 60s, stops a dead session from showing as “running” in the switcher, and fixes the autonomous agent refusing to do work because it read the user’s global CLAUDE.md.

Fixed

Docs

[7.7.30] - 2026-05-28

PATCH release. Fixes loki stop killing every folder’s session machine-wide, adds per-project Stop controls to the multi-project dashboard switcher, and makes a deliberate Ctrl+C exit tear down its own dashboard and registry entry without touching other folders.

Note: the v7.7.29 changelog flagged a “RARV provider-exit-code finding” for this release. That audit finding was investigated and EMPIRICALLY DISPROVEN under set -o pipefail (the provider’s real exit propagates via pipefail in every failure case; no masking occurs). No RARV change ships. v7.7.30 instead addresses the verified multi-folder loki stop issue users reported.

Fixed

Added

Changed

Distribution

All channels (npm, Docker, Homebrew) ship the same fix: the change lives in autonomy/loki, autonomy/run.sh, dashboard/server.py, dashboard/registry.py, and the rebuilt dashboard/static/index.html. The Bun route inherits folder-scoping for free because loki stop falls through to the bash CLI (a parity test asserts it is never intercepted).

[7.7.29] - 2026-05-28

PATCH release. Dashboard <-> CLI <-> Docker integration fixes plus a multi-project dashboard switcher (run loki in several folders and see / switch between them in one dashboard). The RARV provider-exit-code finding from the same audit is deferred to v7.7.30 (highest blast radius, isolated review).

Fixed (dashboard integration)

Added (multi-project switcher)

Tests

[7.7.28] - 2026-05-28

PATCH release. Five verified functional bugs fixed, found by a parallel multi-subsystem source audit (memory + completion-council + healing). Each fix has a regression test with a functional repro. (Dashboard/CLI/Docker and RARV-loop findings from the same audit are deferred to v7.7.29 because they have a wider blast radius and need their own careful review.)

Fixed

Tests

[7.7.27] - 2026-05-28

PATCH release. A welcome opener (the “magic opener”) shown on first run and via loki welcome, styled in the dashboard design language.

Added

Privacy

Packaging

Tests

[7.7.26] - 2026-05-28

PATCH release. UX: “more running in the background, less input required.” Plus the docs reframe completion.

Added

Changed

[7.7.25] - 2026-05-28

PATCH release. Distribution + dashboard fixes found by exercising the shipped artifacts exactly as a real user does (npm install, Docker run, a full-stack PRD build, clicking through every dashboard panel).

Fixed

Changed

Verified (real-user E2E)

Tests

[7.7.24] - 2026-05-28

PATCH release. Cross-project knowledge “lift” proof + the retrieval fix that makes cross-project transfer actually work on natural-language goals. Eighth release in the v7.7.17-v7.7.24 memory arc; this one is the moat proof.

Fixed

Added

Honest scope (no fabrication)

[7.7.23] - 2026-05-28

PATCH release. Speed bench (excellence bar 7) + privacy opt-out (bar 6)

Added

Verified

Known limitation (stated honestly, not fabricated)

NOT tested

Privacy fail-closed (council Opus 2 fix)

[7.7.22] - 2026-05-28

PATCH release. loki memory replay – the wow feature. Wow feature 1 from the memory excellence bar: no competitor (Cursor, Claude Code, Cline, Aider, Codex, Devin, Windsurf) offers session replay. Sixth release in the v7.7.17-v7.7.24 memory arc.

Added

Design decision (deliberate scope)

Verified

NOT tested

[7.7.21] - 2026-05-28

PATCH release. Token economics UI (excellence bar 5). Fifth release in the v7.7.17-v7.7.24 memory arc. Surfaces per-retrieval cost + cache hit rate + top-accessed patterns in the dashboard + a normalized API.

Fixed

Added

Security / robustness (council fixes)

Verified

NOT tested

[7.7.20] - 2026-05-28

PATCH release. Wakes previously-dead memory code + ships the SessionEnd hook installer with the VERIFIED Claude Code schema (deferred from v7.7.18). Fourth release in the v7.7.17 through v7.7.24 memory arc.

Added

Verified

Known boundaries (non-blocking, follow-up candidates)

NOT tested

[7.7.19] - 2026-05-28

PATCH release. Hotfix for a v7.7.18 capture wedge regression discovered on real-user smoke from the npm package: the default Bun route did not propagate stdin to the bash fall-through, so echo '{}' | loki memory ingest --from-stdin silently failed unless LOKI_LEGACY_BASH=1 was set. Without this fix the capture wedge primary CLI path was broken for 99% of users.

NOTE: this release is the hotfix-only ship. The “wake dead code” arc originally planned for v7.7.19 (per ~/git/loki-plan/MEMORY-IMPROVEMENT- PLAN-v7.7.17.md) slides to v7.7.20.

Fixed

Verified

NOT tested

[7.7.18] - 2026-05-28

PATCH release. Memory capture wedge – the foundation unlock for the v7.7.17 through v7.7.23 memory improvement arc. Diagnosis at ~/git/loki-plan/MEMORY-DIAGNOSIS-2026-05-27.md root cause: auto_capture_ episode only fires inside run_autonomous() reached via `loki start

`. The 167 release sessions in 2026 happened in regular Claude Code, never producing episodes. This release adds two voluntary capture paths so EVERY developer session can populate `.loki/memory/`. ### Added - **`memory/ingest.py` (NEW, ~440 LOC)**: two ingest entry points: - `ingest_from_claude_transcript(transcript_path, memory_base)` -> reads a Claude Code session transcript JSONL, extracts tool_use traces, produces an EpisodeTrace with populated action_log + files_read + files_modified. 50 MB file size cap + 50,000 entry cap so a runaway transcript cannot OOM the ingester (council fix Opus 2). - `ingest_from_summary(memory_base, goal, outcome, ...)` -> builds an episode from explicit fields (used by the MCP capture tool). - Secret scrubber + path-aware scrubber on every persisted string (mirrors v7.7.10 + v7.7.17; redacts `.aws/`, `.ssh/`, `.env*`, `id_rsa`, `credentials.json`, etc. with `[REDACTED:sensitive-*]` markers preserving directory context). - Honors `LOKI_MEMORY_CAPTURE_DISABLED=true` escape hatch. - **MCP tool `loki_memory_capture_session_summary`** in `mcp/server.py`: agents call voluntarily at iteration close with goal + outcome + files_modified + files_read + tool_calls_summary. Returns `{"episode_path": ""}` JSON. - **CLI `loki memory ingest`** in `autonomy/loki`: - `loki memory ingest --from-claude-transcript ` reads a JSONL transcript and produces an Episode. - `loki memory ingest --from-stdin` accepts a JSON summary doc on stdin for shell-pipe integration. - **Sample hook script `claude/hooks/loki-session-end.sh`** (MANUAL install): supports BOTH stdin JSON payload (`transcript_path` key, Claude Code's documented format) AND `$CLAUDE_TRANSCRIPT_PATH` env var fallback. Install instructions document the correct Claude Code schema `{matcher, hooks:[{type:"command", command}]}` for users who want zero-touch capture. ### Verified - Council: 2 Opus + 1 Sonnet unanimous APPROVE after 1 fix cycle. Opus 1 CONCERN on hook command quoting + Sonnet APPROVE first pass + Opus 2 CONCERN on hook schema unverified + path-aware scrubber gap + transcript size cap missing -> all fixed -> Opus 1/2 re-review APPROVE. - WebSearch empirically validated Opus 2's claim: Claude Code SessionEnd schema is `{matcher, hooks:[{type:"command",command:...}]}`, fires only on `/clear` (not normal exits), payload is JSON on stdin (not env var). The originally-planned `loki memory enable-hook` installer was REMOVED from v7.7.18 to avoid silently shipping a broken hook config. Deferred to v7.7.19 with proper schema. - Test `tests/test-memory-capture-wedge.sh` 8/8 PASS: - ingest_from_summary writes episode with populated fields - ingest_from_claude_transcript extracts goal + files + action_log - LOKI_MEMORY_CAPTURE_DISABLED escape hatch blocks ingest - secret scrubber redacts sk- tokens + credential keywords - `loki memory ingest --from-stdin` CLI round-trip - path-aware scrub redacts .aws/.ssh/.env + keeps safe paths - transcript >50MB skipped with .errors.log entry - sample hook script supports both stdin JSON + env var formats - Local-CI: 23/23 PASS first attempt. ### Deferred to v7.7.19 - `loki memory enable-hook` / `disable-hook` installer: deferred until the Claude Code SessionEnd schema is verified empirically against a real install. Users wanting zero-touch capture can manually install per the instructions in `claude/hooks/loki-session-end.sh`. The MCP tool + ingest CLI are the verified primary value for v7.7.18. ### NOT tested - The sample hook script firing inside a real Claude Code SessionEnd event (the script is syntactically clean + locally tested in isolation; end-to-end with Claude Code's hook runner pending v7.7.19). - Privacy scrub coverage beyond v7.7.10 + new path-aware redaction. - Concurrency: two MCP capture calls firing simultaneously from different agents (storage layer uses atomic per-episode writes with unique IDs so collision is unlikely but not stress-tested). ## [7.7.17] - 2026-05-28 PATCH release. Memory subsystem observability. First release in the v7.7.17 through v7.7.23 memory improvement arc (plan at `~/git/loki-plan/MEMORY-IMPROVEMENT-PLAN-v7.7.17.md`). Foundation for v7.7.18 capture wedge. Diagnosis at `~/git/loki-plan/MEMORY-DIAGNOSIS- 2026-05-27.md` flagged "silent-fail everywhere" as the top issue masking any future memory regression. v7.7.17 surfaces those failures. ### Fixed - `autonomy/run.sh::store_episode_trace` (line 8724), `auto_capture_episode` (line 9087), `run_memory_consolidation` (line 9136): replaced the silent-fail `except Exception: pass` pattern with explicit structured logging to `.loki/memory/.errors.log` via the new `memory.error_log` module. Log itself never raises (outer try/except guards even against module import failure of the logger). - `loki doctor --json`: new `memory` field exposes `errors_log_path` + `recent_errors` (last 5) + `recent_error_count` + `status` (pass/warn). Bash + Bun routes emit byte-identical field values (council fix Opus 2: previously bash was relative path, Bun was absolute; locked in by test 9 parity assertion). ### Added - `memory/error_log.py` (~155 LOC): structured error log helper. Tab-separated single-line records: `timestamp \\t function \\t error_class \\t message \\t traceback_snippet`. Rotates at 10 MB (current -> `.log.1`, shift `.log.1` -> `.log.2` -> `.log.3`, drop oldest). Tail-only read (last 64 KB) so an oversize log cannot OOM the doctor command (council fix Opus 2). Secret scrubber (council fix Opus 2): credential keywords (api_key/secret/password/token/ private_key/credential/bearer) replaced with `[REDACTED]`; literal high-entropy token shapes (sk-, ghp_/ghs_, xox*, AIza, AKIA) redacted inline. Mirrors v7.7.10 USAGE.md regen scrubber. - `tests/test-memory-error-log.sh` (9/9 PASS): - log_memory_error writes + read_recent_errors reads 2 records - log_memory_error never raises on unwriteable path - read_recent_errors returns [] when no log present - rotation fires at 10 MB threshold - bash doctor --json surfaces memory.recent_errors with seeded record - bun doctor --json same (bash/bun parity) - scrubber redacts sk- tokens + credential keywords before write - tail-only read finds last records in 5 MB+ file (no OOM) - bash + Bun emit identical errors_log_path string ### Verified - Council: 2 Opus + 1 Sonnet unanimous APPROVE after 1 fix cycle. Opus 1 APPROVE first-pass. Opus 2 CONCERN on (a) doctor OOM risk, (b) path parity break, (c) missing secret scrubber -> all fixed with reproducer tests -> re-review APPROVE. Sonnet APPROVE first-pass. - Local-CI: 23/23 PASS first attempt. - Test 5/6 exercise the SHIPPED bash + Bun doctor surfaces against a seeded `.errors.log`. ### Deferred (not in v7.7.17 scope) - action_log + files_read field population on EpisodeTrace: original v7.7.17 plan included this, but empirical check showed `.loki/metrics/efficiency/iteration-N.json` only contains aggregate metrics (no tool_use traces) and `.loki/events.jsonl` has no file_read or tool_use entry types. Field population requires the agent itself to emit tool_use traces. Deferred to v7.7.18 where the new MCP capture tool can receive action_log directly from the agent. ### NOT tested - 10+ MB rotation cycle in a real long-running session (synthetic rotation test passes, but a real session producing 10 MB of errors would itself be a separate failure). - Cross-process concurrency on `_rotate_if_needed` (rotation race documented in code comment; falls back to truncate on collision). - Privacy scrub coverage beyond the v7.7.10 regex set (no JWT detection, no GCP service-account key, no Azure storage key, no Slack webhook URL specifically -- only the v7.7.10 patterns). ## [7.7.16] - 2026-05-27 PATCH release. CI workflow fix: v7.7.15's `tests/test-audit-chain-cross-file.sh` failed the GitHub Actions "Shell tests" job on shellcheck SC2164 (`cd /Users/lokesh/git/loki-mode` without `|| exit`). The hardcoded absolute path also meant Test 1 (production audit dir verification) would have failed even if shellcheck passed, since CI runners do not have `~/.loki/dashboard/audit/`. ### Fixed - `tests/test-audit-chain-cross-file.sh`: portable `cd` via `$(dirname "${BASH_SOURCE[0]}")/..` with `|| exit 1` (resolves shellcheck SC2164). Python interpreter resolved via `command -v python3.12 || command -v python3` instead of hardcoded `/opt/homebrew/bin/python3.12`. - Test 1 (production audit dir): now gracefully SKIPs when `~/.loki/dashboard/audit/` is absent or empty (CI runners, fresh installs) instead of failing. Result tally now reports `passed=N failed=N skipped=N` so CI can distinguish "real check passed" vs "no data available." ### Verified - `shellcheck tests/test-audit-chain-cross-file.sh` clean (zero warnings, zero errors). - Test still 5/5 PASS on this dev machine (real audit dir present). - On CI runners, expected outcome: Test 1 SKIP + Tests 2-5 PASS. ### NOT tested - Actual GitHub Actions Shell tests run on the v7.7.16 commit (will verify post-push). ## [7.7.15] - 2026-05-27 PATCH release. **Critical audit chain verification fix.** The wiki and README claim "SHA-256 chain-hashed tamper-evident audit entries." The write side correctly chained across rotated daily log files via `_recover_last_hash`. The verify side always started from genesis `"0"*64`, so any audit log beyond the first-ever silently failed verification (false negative). Empirically reproduced on real `~/.loki/dashboard/audit/audit-2026-05-04.jsonl` (595 entries; the verifier returned `valid=False` while the chain was actually intact). ### Fixed - **`dashboard/audit.py::verify_log_integrity`**: now accepts an optional `start_hash` parameter (defaults to genesis for backward compat); returns a new `last_hash` field for chain threading; fixed a pre-existing None-return bug on empty files. - **New `dashboard/audit.py::verify_all_logs()`**: walks `AUDIT_DIR/audit-*.jsonl` in mtime order (NOT lexicographic -- rotated files have name shape `audit-DATE.HHMMSS.jsonl` which sorts BEFORE `audit-DATE.jsonl` because `.1 < .j` in ASCII; the prior global glob would have broken chain ordering for any user who hit size-based rotation). Threads chain hash across files. Skips pre-integrity-era files (integrity hashing was enabled mid-history; pre-integrity logs have no `_integrity_hash` field). Returns aggregate dict with `valid`, `files_checked`, `files_skipped`, `entries_checked`, `first_tampered_file`, `first_tampered_line`, `genesis_file`. - **`dashboard/api_v2.py::verify_audit_integrity` endpoint**: was also calling `verify_log_integrity` per-file without threading `start_hash`, so the `/audit/verify` HTTP endpoint gave callers a false sense of security. Now delegates aggregate verdict to `verify_all_logs()`; per-file breakdown still returned for operator visibility but uses mtime sort + threaded `start_hash`. - **Pre-integrity skip**: `_file_has_integrity()` helper peeks line 1 for `_integrity_hash`; files without it are silently skipped from the chain. Reported in response as `files_skipped` count for transparency. ### Added - `tests/test-audit-chain-cross-file.sh` (5/5 PASS): - `verify_all_logs` on real production audit dir reports `valid=True, files_checked=23, files_skipped=2, entries_checked=8872, genesis=audit-2026-02-14.jsonl` - Synthetic 2-file chain validates across rotation - Tampering on line 2 detected at correct file + line - Single-file `verify_log_integrity` backward-compat (no `start_hash`) still works on the genesis-anchored first file - Rotated file with `audit-DATE.HHMMSS.jsonl` name sorts in mtime order (locks in the council-flagged regression) ### Verified - Council: 2 Opus + 1 Sonnet unanimous APPROVE after 1 fix cycle. Opus 1 APPROVE first-pass with non-blocking notes. Opus 2 CONCERN on (a) dead code at function tail, (b) rotated file lexicographic sort -> both fixed, re-review APPROVE. Sonnet CONCERN on (a) same dead code, (b) api_v2.py endpoint not using new function -> both fixed, re-review APPROVE. - Local-CI 23/23 PASS first attempt after fixes. ### NOT tested - Concurrent writers during `verify_all_logs` walk (no file locking; best-effort snapshot; `verify_all_logs` may false-negative on a partial last line if log_event fires mid-read). Document as "verify when quiescent" assumption. - Mid-stream pre-integrity boundary (file whose line 1 has no hash but later lines do, e.g. from operator toggling `LOKI_AUDIT_NO_INTEGRITY`). `_file_has_integrity` only peeks line 1 so such a file would be wrongly skipped entirely. Realistic only with manual env-var flipping. - Real npm-installed package end-to-end smoke (will run post-publish). ## [7.7.14] - 2026-05-27 PATCH release. **Critical LSP regression fix** that has been silently broken since v7.7.0. `lsp_get_diagnostics` returned an empty array unconditionally because `LSPClient` had no notification reader thread and `request()`'s busy-read loop dropped every `publishDiagnostics` notification. Surfaced by codebase audit (see `/Users/lokesh/git/loki- plan/AUTONOMI-100M-STRATEGY.md` section 1.1 and `docs/plans/UT2-6-LSP- DIAGNOSTIC-BROADCAST.md` section 3). ### Fixed - **`mcp/lsp_proxy.py::LSPClient`**: spawned a dedicated notification reader thread (daemon) at the end of `start()` that owns `proc.stdout` and routes JSON-RPC responses to per-request `Queue` instances keyed by request id. `request()` now writes to stdin and parks on its Queue with timeout, instead of reading stdout directly (which dropped notifications). `textDocument/publishDiagnostics` notifications populate `self.pending_diagnostics: Dict[uri, list]`. - **Reader thread lifecycle**: re-spawn after subprocess crash now cleanly stops + joins the old reader, closes stale stdout to unblock the read, drains any pending request waiters with an error sentinel, and clears `pending_diagnostics` + `_opened_uris` + `_initialized`. No thread leak. - **Silent reader death drain**: on any reader exit path (stop signal, EOF, exception), the `finally` block drains all outstanding request waiters with `{'error': {'message': 'LSP reader thread exited: ...', 'request_id': rid}}` so callers fail fast (was: hang full 5s timeout) and logs a warning with exit reason + drained count. - **`lsp_get_diagnostics` consumer**: poll budget bumped from 250ms (5×50ms) to 1s (20×50ms) matching the docstring claim. Removed defensive `hasattr` fallback now that `pending_diagnostics` is guaranteed on the class. ### Added - `tests/test-lsp-diagnostics-regression.sh`: 5/5 PASS including structural check, end-to-end fake-LSP publishDiagnostics flow via reader thread, request routing via per-id Queue, reader-death drain test (kills subprocess mid-request, asserts error returned <1.5s instead of hanging full 5s+), re-spawn thread-leak test (asserts old reader replaced by new on subprocess crash). ### Verified - Council: 2 Opus + 1 Sonnet unanimous APPROVE. Opus 2 initial CONCERN on (a) re-spawn thread leak and (b) silent reader death → both remediated with reproducer tests → re-review APPROVE. - Test 5/5 PASS locally; AST clean; bash -n clean. ### NOT tested - Live pyright/typescript-language-server/gopls/rust-analyzer/jdtls end-to-end with real diagnostics (LSP binaries not all on local PATH; synthetic fake-LSP subprocess in test proves the wire protocol routing works correctly). - Pyright cold-start (3-6s per upstream docs) vs the 1s poll budget in `lsp_get_diagnostics`. First call on cold pyright may return empty; re-calling after warm should return diagnostics. Not a regression (v7.7.0-v7.7.13 returned empty unconditionally regardless of warmth). ### Strategy context Strategy doc at `/Users/lokesh/git/loki-plan/AUTONOMI-100M-STRATEGY.md` identifies this as the credibility-tax fix that must ship before any sales demo. Repo location is intentionally outside the repo to avoid public commit; the fix itself is shipped here as a normal PATCH. ## [7.7.13] - 2026-05-27 PATCH release. Two user-reported P0 bugs on real-user smoke from npm and Docker. ### Fixed - **`loki start` (no PRD) crashes with `args[@]: unbound variable`** on bash 3.2 (macOS default). User repro on `~/git/anonima`: ``` $ loki start Generate PRD from codebase and start? [Y/n] Y Starting Loki Mode... .../autonomy/loki: line 1551: args[@]: unbound variable ``` Root cause: under bash 3.2 + `set -u`, `"${args[@]}"` on an empty array throws "unbound variable". The "no PRD" code path never populated `args`, so the exec at line 1551 (and the sandbox exec at line 1451, and the trigger server nohup at line 11302) fired the bug. Fixed by switching to the safe expansion pattern `${args[@]+"${args[@]}"}` which only expands when the array is defined-with-elements. Sites without potential-empty risk (run_args initialized non-empty, etc.) left unchanged. - **`docker run --rm asklokesh/loki-mode start` exits without taking input.** User repro: ``` $ docker run --rm asklokesh/loki-mode start Generate PRD from codebase and start? [Y/n] % ``` Root cause: user did not pass `-it` so stdin is closed. The `read -r confirm` returned EOF with empty string; the cmd_start fell through to the same args[@] bug above. Now the cmd_start prompt detects `[ ! -t 0 ]` (non-TTY stdin) and auto-confirms with a clear warning that suggests `docker run -it` or `LOKI_AUTO_CONFIRM=true`. ### Added - `tests/test-empty-args-no-prd.sh` (6/6 PASS): asserts the safe expansion pattern works on bash 3.2 semantics; asserts the old pattern correctly fails (proving the bug exists); greps for any remaining unsafe exec/nohup/eval array expansions in autonomy/loki; verifies the non-TTY auto-confirm branch + `-t 0` check are present; bash -n syntax validation. ### Verified - End-to-end reproducer in /tmp/loki-bug-repro: both the user's TTY scenario AND Docker scenario (`< /dev/null` to simulate closed stdin) now get past the previously-crashing point and show the Loki ASCII banner instead of the args[@] error. - 6/6 unit assertions on bash 3.2. - Real-user repro path manually executed: `loki start` from a fresh directory with no PRD on bash 3.2 + `< /dev/null` (docker emul) = no crash, clear warning, auto-confirmed. ### NOT tested - Live Docker pull + run of v7.7.13 image (Docker daemon not running on the local machine; relies on Release workflow publish-docker job to push the image, and on the user re-running `docker run --rm asklokesh/loki-mode:7.7.13 start` from their own Docker setup to confirm). - `loki start ` and other error paths that may have similar empty-array exec paths (only the user-reported sites were audited). ## [7.7.12] - 2026-05-24 PATCH release. UT2-13 bash-vs-bun route parity fix surfaced by real-user validation of v7.7.11 from npm. Without this fix, --provider flag write semantics worked but `loki status --json` silently downgraded provider_source to "default" for all npm-installed users (because the default route is Bun, not bash). ### Fixed - `loki-ts/src/commands/status.ts` embedded Python now reads `.loki/state/cli-provider` BEFORE the saved/env/default cascade, matching `autonomy/loki::cmd_status_json::_read_cli_provider` byte for byte. Includes provider name validation against {claude, codex, cline, aider} canonical set + PID liveness check via os.kill(pid, 0). Legacy 2-field marker format still parses for backward compat. - Real-user validation: `bun install -g loki-mode@7.7.11` + manual cli-provider marker check showed bash=cli but bun=default. v7.7.12 restores parity: both routes report provider_source=cli identically. ### Added - `tests/test-status-cli-provider-parity.sh` (9/9 PASS): asserts bash and bun routes report identical provider + provider_source for valid cli-provider marker, invalid provider name (xyz), and dead PID (999999) cases. Runs in local-ci; prevents future bash/bun drift. ### Verified - Real-user CLI smoke from npm registry: loki --version, loki doctor --json (11 checks valid), loki status --json (parses, fields present), loki provider list (4 providers: claude/codex/cline/aider; Gemini correctly absent per v7.5.18 removal). - Both bash route (LOKI_LEGACY_BASH=1) and bun route (BUN_FROM_SOURCE=1) report identical provider_source=cli after v7.7.12 parity fix. ### NOT tested - Docker image (Docker daemon not running on local machine; relies on Release workflow publish-docker job which the post-release watcher will verify). - Real `loki start --provider codex` end-to-end session (would write the cli-provider marker via cmd_start dispatch + run a session; not exercised since the unit + parity tests cover the reader cascade). ## [7.7.11] - 2026-05-24 PATCH release. Bundle of UT2-12 (USAGE markdown render with XSS guard), UT2-13 (provider_source "cli" plumbing with name validation + PID liveness), bun-parity flake instrumentation, and Forge plan doc extraction from closed PR #161. Three-reviewer council (2 Opus + 1 Sonnet) unanimous APPROVE after one fix iteration on real defects surfaced by Opus reviewers. ### Added - **UT2-12 USAGE markdown rendering** (`dashboard-ui/scripts/build-standalone.js`): `renderUsageMarkdown()` function converts USAGE.md fenced code blocks, headings, lists, blockquotes, inline code, bold/italic, and links to HTML. Replaces the `
` text-only panel with a styled `
`. CSS scoped to `.usage-md` so zero style bleed elsewhere. Test: `tests/test-usage-markdown-render.sh` 11/11 PASS. - **UT2-12 XSS guard** (council fix from Opus 1 REJECT iteration): link href construction now validates URL against scheme allowlist `^(https?://|mailto:|#|/(?!/))`. `javascript:`, `data:`, `vbscript:`, `file:`, and `//evil.com` protocol-relative URLs render as inert `label (url)` instead of clickable links. USAGE.md absorbs agent output and PRD text; treat as untrusted. - **UT2-13 provider_source "cli"** (`autonomy/loki`): `--provider ` flag dispatch in `cmd_start` and `cmd_run` writes `.loki/state/cli-provider` so `loki status --json` reports `provider_source: "cli"` for the session. Cascade order: cli > saved > env > default. 24-hour heuristic + PID liveness check. Cleanup hooks in autonomy/run.sh STOP/double-Ctrl+C/normal-end paths (shipped in v7.7.10 via staging race; harmless rm of file that didn't exist yet). Test: `tests/test-provider-source-cli.sh` 8/8 PASS. - **UT2-13 provider name validation** (council fix from Opus 2 CONCERN): write sites now wrap in `case "$provider" in claude|codex|cline|aider) ... ;; esac` so a typo like `--provider xyz` cannot pollute status output. Reader (`_read_cli_provider`) also validates against the canonical set before returning, defense in depth. - **UT2-13 PID liveness** (council fix from Opus 1 CONCERN): cli-provider marker format extended from `:` to `::`. Reader runs `os.kill(pid, 0)` and treats dead PIDs as stale. Bounds the SIGKILL/crash leak case where 24h heuristic alone misleads. Legacy 2-field format still parses for backward compat. - **bun-parity flake instrumentation** (`scripts/local-ci.sh`): on any diff between bash and Bun routes, persist raw + normalized outputs + unified diff to `.loki/local-ci-flake//